# Codex Hooks Reference: PreToolUse, PostToolUse, Examples

OpenAI Codex CLI hooks: PreToolUse and PostToolUse events, hooks.json setup, and working examples for blocking, logging, and rewriting tool calls.

<figure style="margin:20px 0 28px;">
  <img src="/assets/img/screenshots/tool-surface-dark.png" alt="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" style="width:100%;height:auto;border:1px solid var(--line-2);border-radius:10px;box-shadow:0 20px 50px -24px rgba(0,0,0,0.9);" />
  <figcaption style="font-size:12.5px;color:var(--acp-text-faint);text-align:center;margin-top:10px;">What the hooks below feed: every tool the agent can call, policy-checked — allow or deny, per tool.</figcaption>
</figure>

*Last updated: September 16, 2026. Codex's hook surface has changed materially since this reference was first written — the update log below says what moved. This is the one page we keep current on Codex hooks; [the Codex control model page](/controls/codex-cli) covers the controls around them — approvals, the Guardian reviewer, permission profiles, the sandbox, and `requirements.toml`.*

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 output 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 policy checks can do:

1. **Hooks are on by default now.** Current Codex releases enable the hook engine out of the box; the canonical feature key is `hooks` (the old `codex_hooks` opt-in flag survives as a deprecated alias). What replaced the flag as the gate is trust: Codex requires you to review and trust the exact definition of a non-managed hook before it runs.
2. **`PreToolUse` coverage is broad now.** Per [OpenAI's Codex hooks docs](https://developers.openai.com/codex/hooks), PreToolUse can intercept Bash, file edits performed through `apply_patch`, MCP tool calls, and other local function tools; unified exec (`exec_command`) matches as `Bash`. The remaining blind spot is hosted tools, which don't use the local function-tool hook path.

This post walks through what the hook surface actually does, the install path, and the pieces a hook alone still doesn't give you.

## What changed in recent Codex releases

If your hooks worked and then didn't — or a guide you're following doesn't match what Codex does — this is the short list:

- **Hooks flipped from opt-in to on-by-default.** The `[features].codex_hooks = true` incantation older guides give you is now a deprecated alias; `hooks` is the canonical key. If your hooks stopped running after an update, re-check the feature key and the trust prompt rather than re-adding the old flag.
- **Coverage expanded from shell-only to most local tools.** Earlier releases fired PreToolUse for the `shell` tool only; `apply_patch` and MCP calls were invisible to it. Current docs list all of them as interceptable. The shell-expressed-edits workaround this page used to recommend is no longer necessary for coverage.
- **More decision surface.** `deny` used to be the only decision Codex acted on. Current releases accept the deny JSON, a legacy `{"decision": "block"}` shape, exit code 2 with the reason on stderr, and `allow` with `updatedInput` to rewrite a call. There's also a `PermissionRequest` event where any matching hook's `deny` wins.
- **Async hooks exist — and can't enforce.** Setting `async: true` runs a command hook in the background while Codex continues. Background hooks can't block, approve, or rewrite the operation that triggered them, so policy has to stay on the synchronous path.
- **`--full-auto` is deprecated.** It still works as a compatibility flag with a warning; new scripts should say what they mean with `--sandbox workspace-write` (or stay in the default read-only sandbox).

## 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:

```json
{
  "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 hooks docs](https://developers.openai.com/codex/hooks). Don't trust third-party reproductions; the upstream docs are accurate and current.

## What hooks control today

Every local tool call the agent dispatches — shell commands, unified exec, `apply_patch` edits, MCP tool calls — passes through `PreToolUse` before execution. The operational shapes:

- **Deny** the call — return `permissionDecision: "deny"` with a `permissionDecisionReason` (or exit code 2 with the reason on stderr). The agent sees a tool error and adapts.
- **Allow and rewrite** — `permissionDecision: "allow"` with `updatedInput` substitutes your version of the call.
- **Let it run** — anything else, and the command executes as-is.

`PostToolUse` fires after the call returns with stdout/stderr/exit-code. Treat it as audit: it can't undo side effects from a tool that already ran.

One boundary to design around: **background hooks observe, synchronous hooks enforce.** An `async: true` hook is useful for shipping events somewhere without adding latency, and useless for stopping anything.

<figure style="margin:24px 0 28px;">
  <img src="/assets/img/screenshots/console-activity-decisions.png" alt="ACP Activity view: one row per policy-checked tool call — Bash subcommand, decision, verified identity, and per-call latency" loading="lazy" style="width:100%;height:auto;border:1px solid var(--line-2);border-radius:10px;box-shadow:0 20px 50px -24px rgba(0,0,0,0.9);" />
  <figcaption style="font-size:12.5px;color:var(--acp-text-faint);text-align:center;margin-top:10px;">What a policy-checked 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.</figcaption>
</figure>

## 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](https://developers.openai.com/codex/hooks) for the real field names when you want precise matching):

```js
#!/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 a deny → the call proceeds
});
```

Register it in `~/.codex/hooks.json` and it checks every local tool call. Worth being honest about the ceiling here, because it's the ceiling of every regex hook: string-matching a shell command is not a shell parser. Destructive behavior hides in compound commands, scripts, and aliases that no pattern list anticipates. The [ACP version of this script](/integrations/codex) keeps the same contract but resolves the decision against server-side policy — the deny list lives in a dashboard, applies per agent tier, and every decision (including every deny) lands in an audit log you can read later.

## Trust review and managed hooks

Two mechanics sit around the contract, both per [OpenAI's hooks docs](https://developers.openai.com/codex/hooks):

- **A non-managed hook does not run until you trust it.** Codex asks you to review and trust the exact hook definition; the trust is recorded against the hook's hash, so an edited hook goes back to review. `/hooks` inside Codex inspects, trusts, or disables what's loaded.
- **Enterprises can make managed hooks the only hooks.** `allow_managed_hooks_only = true` in `requirements.toml` skips hooks from user, project, session, and plugin sources and loads only the managed ones, which can't be disabled from the user hook browser. [The Codex enterprise rollout page](/docs/enterprise/codex) has the complete file.

And the line from the same docs worth keeping in view: treat tool hooks as "a useful guardrail, not a complete enforcement boundary." Some specialized tool paths can opt out of the default hook path — one more reason the decision behind the hook should live somewhere the hook's absence is visible.

## Troubleshooting

**Hook isn't firing at all.** In current releases, check the trust prompt first — Codex requires you to review and trust the exact hook definition before a non-managed hook runs. On older versions, the `[features].codex_hooks = true` flag was required and hooks were silent no-ops without it.

**Hooks stopped firing after an update.** The feature key changed (`codex_hooks` → `hooks`, old key kept as a deprecated alias) and the hook engine has moved across recent releases. Re-check your `config.toml` feature block and re-trust the hook if prompted.

**Hook fires for Bash but not for a hosted tool.** Expected — hosted tools don't use the local function-tool hook path. Everything local (shell, unified exec, `apply_patch`, MCP calls) should fire it.

**A background hook isn't blocking anything.** It can't. `async: true` hooks run while Codex continues; only synchronous hooks can deny, approve, or rewrite.

**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.

## Sandbox flags for unattended runs (`--full-auto` is deprecated)

For unattended Codex deployments (CI agents, scheduled jobs, headless coding agents), say what you mean with the sandbox flags:

- `codex exec "<task>"` — the default is a read-only sandbox.
- `codex exec --sandbox workspace-write "<task>"` — allow edits inside the workspace.
- `codex exec --sandbox danger-full-access "<task>"` — only in a controlled environment.

`codex exec --full-auto` still works as a deprecated compatibility flag and prints a warning; OpenAI's docs say to prefer the explicit `--sandbox workspace-write` in new scripts. Hooks keep firing in all of these modes.

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](https://github.com/agentic-control-plane/acp-governance-sdks/tree/main/examples/framework-scout/codex-cli):

```bash
codex exec \
  --sandbox workspace-write \
  -c 'sandbox_workspace_write.network_access=true' \
  "$(cat scout.prompt.md)"
```

Worth knowing: Codex keeps hooks running in non-interactive sandboxed modes, and Claude Code does the same under `--dangerously-skip-permissions` (the flag suppresses prompts, not hooks). Unattended deployments on either retain audit + policy enforcement; only the human confirmation prompt is gone. What non-interactive mode still doesn't give you is a way to *approve* — a paused MCP call with nobody attached to stdin has no one to answer it, which is a different problem than blocking (we cover it in the [headless approvals post](/blog/codex-headless-mcp-approvals)).

## 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:

<pre data-track="CodexHooksRef: Installer Section Copy"><code>curl -sf https://agenticcontrolplane.com/install.sh | bash</code></pre>

The installer detects your Codex installation and:
1. Ensures the hooks feature is enabled (a no-op on current Codex, where hooks are on by default; on older versions it sets the legacy flag)
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 and adds the control directive to `~/.codex/AGENTS.md`
5. Walks you through workspace provisioning + API key save

Restart Codex after install. Every local 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](/docs/setup#i-use-codex).

For environments where you'd rather build your own integration: the protocol is documented at [`/integrations/codex`](/integrations/codex) and the running code is in the starter folder linked above.

## Tier-aware policy

The hook payload carries Codex's execution context (interactive session vs. non-interactive `codex exec`). Map this to ACP's tier model for tier-aware policy:

| Codex context | ACP tier | Typical policy |
|---|---|---|
| interactive session | `interactive` | Permissive — human is watching |
| non-interactive, session-bound | `subagent` | Restrictive — no human, session-bound |
| non-interactive, scheduled/daemon | `background` | Most restrictive — no human, no session anchor |

A typical policy block for destructive shell verbs:

```json
{
  "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](/blog/anthropic-agent-sdk-governance-reference) or the [OpenAI Agents SDK starter](/integrations/openai-agents-sdk).

Batch-fix candidate; not a blocker for rollout.

## Comparing to Claude Code hooks

Both clients implement the hook pattern ([Claude Code hooks reference](/blog/claude-code-hooks-reference)). Where they differ:

| Dimension | Claude Code | Codex CLI |
|---|---|---|
| Hook config path | `~/.claude/settings.json` | `~/.codex/hooks.json` |
| Hook events | `PreToolUse`, `PostToolUse` | `PreToolUse`, `PostToolUse`, `PermissionRequest`, `SessionEnd` |
| Coverage of native tools | All tools (`Bash`, `Edit`, `Read`, `Write`, MCP) | Local tools: Bash, unified exec, `apply_patch`, MCP; hosted tools excluded |
| Operational decisions | `allow` / `deny` / `ask`, input rewrite | `deny` (JSON or exit code 2), `allow` + `updatedInput` rewrite; any matching `deny` wins on `PermissionRequest` |
| Enabled by default | Yes | Yes (current releases; older versions required `[features].codex_hooks = true`) |
| Unattended-mode behavior | `--dangerously-skip-permissions` removes prompts; hooks and deny rules still run | Sandbox modes keep hooks running (`--full-auto` deprecated in favor of `--sandbox workspace-write`) |
| Async hooks | No | Yes (`async: true`) — observe-only, can't block |

Both hook surfaces are genuinely usable for policy control now, and Codex's is much closer to Claude Code's than it was at launch. What neither gives you is the part around the hook: rules that hold across both tools, an audit trail of what was denied and why, and an "ask" that someone can actually answer when no human is attached. That's the layer the installer wires in.

## Practical takeaways

1. **Use the explicit sandbox flags** for unattended Codex agents — `--sandbox workspace-write`, not the deprecated `--full-auto`, and never `--dangerously-bypass-approvals-and-sandbox` when a sandboxed mode will do. Hooks keep running either way.
2. **Put enforcement on the synchronous path.** Async hooks can't block; they're for telemetry.
3. **Re-check hooks after Codex updates.** The feature key, trust flow, and coverage have all moved in 2026; a hook that silently stopped firing looks identical to a hook with nothing to report.
4. **Don't stop at a regex.** A deny-list hook is a tripwire, not a policy — compound commands walk past it. Put real rules behind the hook and keep the decisions in an audit log.

## Frequently asked questions



## Where to read more

- [What is runtime authorization?](/what-is-runtime-authorization) — the per-call policy model these hooks plug into
- [AI agent tool allowlists](/blog/ai-agent-tool-allowlist) — deny-by-default across Claude Code, Codex CLI, and MCP
- [Codex CLI starter](https://github.com/agentic-control-plane/acp-governance-sdks/tree/main/examples/starters/codex-cli) — runnable reference
- [Codex CLI scout](https://github.com/agentic-control-plane/acp-governance-sdks/tree/main/examples/framework-scout/codex-cli) — full headless agent example
- [Codex integration page](/integrations/codex) — install instructions and dashboard verification
- [Codex CLI's control model, explained](/controls/codex-cli) — the controls around the hooks: approval policies, the Guardian reviewer, permission profiles, the sandbox, `requirements.toml`
- [Ways to set up ACP](/docs/setup) — every stack's setup path, each ending with the coverage you'll have
- [OpenAI Codex hooks docs](https://developers.openai.com/codex/hooks) — canonical upstream reference
- [Govern Claude Code in 60 seconds](/blog/governance-for-claude-code) — comparable pattern
- [The Tool Surface Index](/tool-surfaces) — Codex's declared tools next to Claude Code's, captured from live traffic and grouped by blast radius
