Skip to content
Agentic Control Plane

Codex CLI Hooks Reference — hooks.json, PreToolUse & PostToolUse

David Crowe David Crowe · · Updated · 16 min read
codex openai cli hooks governance reference
Share X HN LinkedIn

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

The ACP tool-surface control table for a live coding agent: every tool it can call as a row, with Allow / Deny per tool, invoked-vs-never-invoked status, and a one-click suggested posture
What the hooks below feed: every tool the agent can call, governed — allow or deny, per tool.

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:

  1. Hooks are off by default. Codex marks the hook engine Stage::UnderDevelopment — you opt in with [features].codex_hooks = true in ~/.codex/config.toml. Without the flag, hooks are silent no-ops.
  2. PreToolUse intercepts the shell (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.

ACP Activity view: one row per governed tool call — Bash subcommand, decision, verified identity, and per-call latency
What a governed session looks like once the hook is wired: every call one row — the tool, the decision, the identity behind it, and the latency of the check. Real workspace, live traffic.

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:

  1. apply_patch edits. 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.
  2. 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 use sed, cat > file <<EOF, or git apply via shell. These commands route through the shell tool, which PreToolUse does 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/mcp for 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 / PostToolUse invocations 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:

  1. Enables [features].codex_hooks = true in ~/.codex/config.toml (idempotent — it won’t touch the flag if already set)
  2. Writes ~/.acp/govern.mjs (the same hook script Claude Code uses, invoked with ACP_CLIENT=codex)
  3. Registers it under ~/.codex/hooks.json for both PreToolUse and PostToolUse
  4. Wires the ACP MCP connector for the tools hooks don’t cover, and adds the governance directive to ~/.codex/AGENTS.md
  5. 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

  1. Use --full-auto for unattended Codex agents, not --dangerously-bypass-approvals-and-sandbox. Hooks keep running, governance stays enforced, network can be selectively granted.
  2. 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.
  3. 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.
  4. Emit deny only. allow/ask/updatedInput are 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

Share X HN LinkedIn
Get the next data drop
What agents actually cost, new tool-surface captures, and the occasional incident post-mortem — sent when we publish something worth your inbox, not on a schedule. Unsubscribe anytime.
Share: Twitter LinkedIn
Related posts

← back to blog