Codex CLI Hooks Reference — hooks.json, PreToolUse & PostToolUse
Just want to govern Codex? This is the deep reference. If you're here to actually see, control, and price every Codex tool call, that's one command — hooks for Bash plus an MCP connector for everything else, in a single installer:
curl -sf https://agenticcontrolplane.com/install.sh | bash
Full Codex install guide → · see your first governed call → · free for individuals
OpenAI’s Codex CLI has a PreToolUse / PostToolUse hook surface modeled on Claude Code’s. Same architectural shape: scripts run before and after tool dispatch, the script’s stdout decides the outcome, the agent never sees what’s intercepted. Familiar to anyone who’s wired up Claude Code hooks.
Two things to know upfront, because they shape what hook-based governance can do:
- Hooks are off by default. Codex marks the hook engine
Stage::UnderDevelopment— you opt in with[features].codex_hooks = truein~/.codex/config.toml. Without the flag, hooks are silent no-ops. PreToolUseintercepts theshell(Bash) tool only — by design. Per OpenAI’s Codex hooks docs, PreToolUse currently supports Bash interception only;apply_patch,Edit/Write/Read, web fetch, and MCP tool calls do not fire it. This isn’t a bug to wait out — it’s the shape of the surface today, so non-Bash tools need a second governance path (the MCP connector, below).
This post walks through what the hook surface actually does, the install path, and how to govern the tools hooks don’t cover.
Where hooks live
Codex CLI reads hook configuration from ~/.codex/hooks.json. Same per-user shape as Claude Code’s ~/.claude/settings.json, different filename. The contents register a script for each event:
{
"hooks": {
"PreToolUse": [{ "command": "node /Users/me/.acp/govern.mjs --pre" }],
"PostToolUse": [{ "command": "node /Users/me/.acp/govern.mjs --post" }]
}
}
Each entry’s command is the executable Codex spawns when the event fires. Multiple entries per event are allowed and run in declaration order.
The hook script can be any executable that reads JSON from stdin and writes JSON to stdout — Node, Python, Bash with jq, whatever. Codex doesn’t care about the runtime as long as the contract holds.
For full payload schema details (field names, types, supported decision values), the canonical reference is OpenAI’s Codex agent-approvals & security docs. Don’t trust third-party reproductions; the upstream docs are accurate and current.
What hooks govern today
For shell (Bash) tool calls, every command the agent dispatches passes through PreToolUse before execution. One thing to be precise about: the only decision Codex acts on is deny. Its parser reads but rejects allow, ask, and updatedInput (output_parser.rs), so the operational shape of a PreToolUse hook is:
- Deny the call — the agent sees
tool_error: <reason>and adapts. - Allow it (anything other than
deny) — the command runs as-is.
You cannot rewrite the input or interpose a human “ask” through the hook today — emit permissionDecision: "deny" with a reason, or let it run. PostToolUse fires after the call returns with stdout/stderr/exit-code for audit; note Codex also rejects updatedMCPToolOutput, so PostToolUse is observe-only (no output rewriting/redaction via the hook).
For an agent that’s primarily shell-mediated — running tests, executing builds, calling curl, managing files via cat/sed/mv — deny-based PreToolUse governance is enough to block what shouldn’t run.
A minimal working hook
The smallest useful PreToolUse hook is a deny-list: block the commands you never want an agent running unattended, let everything else through. This version is deliberately schema-agnostic — it string-scans the whole event rather than assuming field names, because the payload schema is Codex’s to define (see OpenAI’s docs for the real field names when you want precise matching):
#!/usr/bin/env node
// ~/.codex/deny-hook.mjs — minimal PreToolUse hook
let raw = "";
process.stdin.on("data", (d) => (raw += d));
process.stdin.on("end", () => {
const event = raw; // full schema: OpenAI's Codex hooks docs
if (/\brm -rf\b|--force\b|\bDROP TABLE\b|\bgh repo delete\b/i.test(event)) {
console.log(JSON.stringify({
permissionDecision: "deny",
permissionDecisionReason: "Blocked by policy: destructive command",
}));
return;
}
console.log("{}"); // anything but deny → the call proceeds
});
Register it in ~/.codex/hooks.json and it governs every Bash call. The ACP version of this script does the same thing with server-side policy instead of a hardcoded regex — same contract, but the deny list lives in a dashboard, applies per agent tier, and every decision lands in an audit log.
Troubleshooting
Hook isn’t firing at all. Check ~/.codex/config.toml has [features].codex_hooks = true. The flag is off by default and hooks are silent without it — no error, no log line.
Hook fires for Bash but not for Read/Edit/apply_patch. Expected — PreToolUse covers the shell tool only. Govern non-Bash tools at the MCP boundary (below).
updatedInput rejection error in Codex logs. Don’t return updatedInput from PreToolUse — Codex’s parser strict-rejects unsupported fields. Deny with a reason is the only operational output.
Two hook layers conflicting. User-level (~/.codex/hooks.json) and repo-level (<repo>/.codex/hooks.json) both load and merge, with repo entries winning for matching tools. There’s no namespacing — identify your entries with a stable statusMessage string.
What PreToolUse doesn’t cover (and how to govern it anyway)
Because PreToolUse intercepts the Bash tool only, these calls never reach a hook:
apply_patchedits. Codex’s native diff-application tool. PreToolUse doesn’t fire for it, so a hook-only layer never sees the structured patches the agent applies.- MCP tool calls (and
Read/Edit/Write/web fetch). Anything dispatched outside the shell tool is invisible to PreToolUse.
For agents that lean on apply_patch or MCP toolsets, hook-only governance has a real blind spot. Two ways to close it:
- Express edits as shell. Instead of relying on
apply_patch, instruct the agent to usesed,cat > file <<EOF, orgit applyvia shell. These commands route through theshelltool, whichPreToolUsedoes cover. The Framework Scout reference agent in our examples repo does exactly this. - Govern at the MCP server boundary. For MCP-mediated calls, run the MCP server itself behind a control plane (e.g., point Codex at
mcp.agenticcontrolplane.com/mcpfor hosted-MCP governance). The interception happens server-side rather than client-side, so it’s not affected by the Codex hook gap.
These workarounds aren’t permanent; the upstream issue may resolve, at which point hook coverage will expand. Until then, they’re how production deployments handle the gap.
The --full-auto flag — strictly preferable to --dangerously-bypass-approvals-and-sandbox
For unattended Codex deployments (CI agents, scheduled jobs, headless coding agents), the right flag is codex exec --full-auto, not --dangerously-bypass-approvals-and-sandbox. OpenAI’s own documentation labels the latter “Elevated Risk / not recommended.”
--full-auto (= --sandbox workspace-write + non-interactive execution):
- Suppresses the Y/N approval prompt — the agent runs without per-step confirmation
- Keeps hooks firing — same
PreToolUse/PostToolUseinvocations as interactive mode - Sandboxes filesystem writes to the workspace
- Blocks network access by default
For agents that need outbound network (calling APIs, hitting external services), add -c sandbox_workspace_write.network_access=true to grant network while keeping filesystem sandboxing intact. From the Codex CLI scout:
codex exec \
--full-auto \
-c 'sandbox_workspace_write.network_access=true' \
"$(cat scout.prompt.md)"
This is the governance differentiator vs. Claude Code worth knowing: where Claude Code’s --dangerously-skip-permissions disables hooks entirely, Codex’s --full-auto keeps them running. Unattended agent deployments retain audit + policy enforcement; only the human-in-loop confirmation prompt is suppressed.
Installing the ACP Codex plugin (one command)
The fastest path to a working hook config is the ACP installer — the same one command as the top of this page:
curl -sf https://agenticcontrolplane.com/install.sh | bash
The installer detects your Codex installation and:
- Enables
[features].codex_hooks = truein~/.codex/config.toml(idempotent — it won’t touch the flag if already set) - Writes
~/.acp/govern.mjs(the same hook script Claude Code uses, invoked withACP_CLIENT=codex) - Registers it under
~/.codex/hooks.jsonfor bothPreToolUseandPostToolUse - Wires the ACP MCP connector for the tools hooks don’t cover, and adds the governance directive to
~/.codex/AGENTS.md - Walks you through workspace provisioning + API key save
Restart Codex after install. Every shell tool call now flows through ACP’s /govern/tool-use endpoint before dispatch, with audit rows landing under client=codex in your dashboard. To also meter what your sessions cost, add the proxy plane — see Ways to set up ACP → I use Codex.
For environments where you’d rather build your own integration: the protocol is documented at /integrations/codex and the running code is in the starter folder linked above.
Tier-aware policy
The hook payload includes Codex’s current approval mode (interactive, auto, full-auto). Map this to ACP’s tier model for tier-aware policy:
| Codex mode | ACP tier | Typical policy |
|---|---|---|
interactive |
interactive |
Permissive — human is watching |
auto |
subagent |
Restrictive — no human, session-bound |
full-auto |
background |
Most restrictive — no human, no session anchor |
A typical policy block for destructive shell verbs:
{
"tools": {
"Bash.curl": { "background": "deny", "subagent": "ask", "interactive": "allow" },
"Bash.rm": { "background": "deny", "subagent": "ask", "interactive": "allow" },
"Bash.kubectl": { "background": "deny", "subagent": "deny", "interactive": "ask" },
"Bash.aws": { "background": "deny", "subagent": "deny", "interactive": "ask" }
}
}
Sub-command classification (Bash.curl vs Bash.rm) happens in the hook by parsing the leading token of the shell command. ACP’s ~/.acp/govern.mjs does this automatically.
Known limitation worth flagging
The ACP Codex plugin currently sends agent_tier but not agent_name in its hook payload — events land in the dashboard under client=codex without an agent-name breakdown. Same gap exists in the Claude Code plugin. Functional impact is limited (per-tool policy still works, audit still records), but per-agent attribution is sparser than for SDK-based integrations like the Anthropic Agent SDK or the OpenAI Agents SDK starter.
Batch-fix candidate; not a blocker for governance.
Comparing to Claude Code hooks
Both clients implement the hook pattern. Where they differ:
| Dimension | Claude Code | Codex CLI |
|---|---|---|
| Hook config path | ~/.claude/settings.json |
~/.codex/hooks.json |
| Hook events | PreToolUse, PostToolUse |
PreToolUse, PostToolUse |
| Coverage of native tools | All tools (Bash, Edit, Read, Write, MCP) |
shell (Bash) only by design; non-Bash tools via the MCP connector |
| Operational decisions | allow / deny / ask, input rewrite |
deny only (allow/ask/updatedInput parsed but rejected) |
| Enabled by default | Yes | No — requires [features].codex_hooks = true |
| Unattended-mode flag | --dangerously-skip-permissions (disables hooks) |
--full-auto (keeps hooks running) — strictly preferable for governed deployments |
| Hook timeout | ~4s default | ~30s (giving hooks more headroom for remote policy lookups) |
Both hook surfaces are usable for governance. Codex’s hook-keeping behavior in --full-auto is a meaningful win for unattended deployments. The trade-off is narrower reach: deny-only, Bash-only — which is exactly why the ACP installer pairs the hook with an MCP connector to cover everything else.
Practical takeaways
- Use
--full-autofor unattended Codex agents, not--dangerously-bypass-approvals-and-sandbox. Hooks keep running, governance stays enforced, network can be selectively granted. - Pre-fer shell-mediated tool patterns for agents that need governance coverage today. Express file edits via shell when possible; the hook reliably catches Bash-routed calls.
- Govern non-Bash tools at the MCP boundary, not the Codex hook — PreToolUse covers the shell tool only, so the MCP connector is how you cover
apply_patch, MCP calls, and the rest. - Emit
denyonly.allow/ask/updatedInputare parsed but rejected by Codex today, so design your hook around a single deny decision with a reason.
Frequently asked questions
How do I enable hooks in Codex CLI?
Add [features].codex_hooks = true to ~/.codex/config.toml. Hooks are off by default (the engine is marked under-development), and without the flag they’re silent no-ops — no error, no log line. Then register your hook script in ~/.codex/hooks.json under PreToolUse / PostToolUse.
Why isn't my Codex hook firing?
Two usual causes. First: the codex_hooks feature flag isn’t set in ~/.codex/config.toml — hooks are silently ignored without it. Second: the call isn’t a Bash call. PreToolUse currently intercepts the shell tool only; apply_patch, Read/Edit/Write, web fetch, and MCP tool calls never fire it.
Can a Codex hook block a tool call?
Yes — emit permissionDecision: "deny" with a permissionDecisionReason, and the agent sees a tool error instead of the command running. Deny is the only decision Codex acts on: allow, ask, and updatedInput are parsed but rejected by the output parser.
Do Codex hooks work in --full-auto mode?
Yes. codex exec --full-auto suppresses the per-step approval prompt but keeps PreToolUse and PostToolUse firing — unlike Claude Code’s --dangerously-skip-permissions, which disables hooks entirely. That makes --full-auto the right flag for unattended, governed Codex deployments.
Do Codex hooks cover apply_patch or MCP tool calls?
No. PreToolUse intercepts the Bash (shell) tool only, by design. To govern the rest, either express file edits as shell commands (which hooks do catch) or route non-Bash tools through an MCP connector governed server-side, where the Codex hook gap doesn’t apply.
Where does Codex read hook configuration from?
~/.codex/hooks.json for the user level, merged with <repo>/.codex/hooks.json at the repo level. Both load, with repo entries overriding user entries for matching tools; there’s no namespacing, so keep a stable statusMessage on entries you need to find again.
Where to read more
- What is runtime authorization? — the per-call policy model these hooks plug into
- AI agent tool allowlists — deny-by-default across Claude Code, Codex CLI, and MCP
- Codex CLI starter — runnable reference with the workarounds applied
- Codex CLI scout — full headless agent example
- Codex integration page — install instructions and dashboard verification
- Ways to set up ACP — every stack’s setup path, each ending with the coverage you’ll have
- OpenAI Codex agent-approvals & security docs — canonical upstream reference
- Govern Claude Code in 60 seconds — comparable pattern, fewer caveats
- The Tool Surface Index — Codex’s 17 declared tools next to Claude Code’s 76, captured from live traffic and grouped by blast radius