Codex hooks are commands that Codex runs at fixed points in a session, such as before a tool call (PreToolUse) or when a turn ends (Stop). You declare them in a hooks.json file next to your config.toml, or under a hooks table inside config.toml, and a new or changed hook has to be reviewed and trusted in /hooks before Codex runs it.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^Bash$",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /tmp/pre.py",
            "timeout": 10,
            "statusMessage": "checking"
          }
        ]
      }
    ]
  }
}

Applies to: Codex CLI, openai/codex main branch as of October 2, 2026 (latest release tag rust-v0.160.0). Last checked: October 2, 2026.

The example above is the hooks.json shape from the repository’s own config tests: a PreToolUse hook that runs a Python script before every shell command. Every detail on this page links to the file in the openai/codex repository it comes from.

On this page

How Codex CLI hooks are switched on

Hooks are a stable feature with the key hooks, enabled by default; the older key codex_hooks is kept as an alias (features/src/lib.rs, features/src/legacy.rs). The feature’s description in the source reads “Enable Claude-style lifecycle hooks loaded from hooks.json files”, and the slash command list describes /hooks as “view and manage lifecycle hooks” (slash_command.rs).

Where do Codex hooks go?

Codex reads hooks from every configuration layer, from either a hooks.json file in that layer’s folder or a hooks table in that layer’s config.toml (discovery.rs, state.rs, config loader README):

Layer hooks.json location config.toml location
User ~/.codex/hooks.json (or under CODEX_HOME) [hooks] in ~/.codex/config.toml
Project <project>/.codex/hooks.json [hooks] in <project>/.codex/config.toml
System (managed) hooks.json next to /etc/codex/config.toml [hooks] in /etc/codex/config.toml
Admin requirements — Hook events in requirements.toml, with optional managed_dir
Plugin hooks/hooks.json inside the plugin —

If one layer has hooks in both places, Codex loads both and warns: “loading hooks from both … prefer a single representation for this layer”. In a linked Git worktree, hook discovery can use the .codex/ folder of the root checkout while the rest of the project config comes from the worktree (state.rs).

The same hook in config.toml

In TOML each event is an array of tables, and each handler is a nested array of tables. This is the form the repository’s tests load from a config.toml hooks table (mod_tests.rs, config_requirements.rs):

[[hooks.PreToolUse]]
matcher = "^Bash$"

[[hooks.PreToolUse.hooks]]
type = "command"
command = "python3 /tmp/pre.py"
timeout = 10
statusMessage = "checking"

In hooks.json, events must sit inside the top-level "hooks" object. A file with "SessionStart" at the root is rejected as an unknown field (hooks_tests.rs). The file may also carry an optional "description" string.

Which hook events does Codex support?

Codex defines twelve events (hooks/src/lib.rs, hook_config.rs). The /hooks browser describes each one (hooks browser snapshot); the matcher input and control columns come from the event handlers in hooks/src/events.

Event When it runs Matcher compared with What a hook can do
PreToolUse Before a tool executes Tool name Block the call, rewrite its input, add context
PermissionRequest When permission is requested Tool name Return allow or deny
PostToolUse After a tool executes Tool name Block with feedback, stop, add context
PreCompact / PostCompact Before / after context compaction manual or auto Stop processing, show a message
SessionStart When a new session starts startup, resume, clear, compact, fork Add context, stop the session
SessionEnd Right before a session ends End reason Side effects only
UserPromptSubmit When you submit a prompt No matcher Block the prompt, add context
SubagentStart / SubagentStop A subagent starts / stops Agent type Add context / ask for another pass
Stop When a turn ends No matcher Ask Codex to keep working
Interrupt When a turn is interrupted No matcher Show a message

A matcher on UserPromptSubmit, Stop or Interrupt is ignored: the source states that these events “do not dispatch against a tool, compaction trigger, session-start source, or session-end reason” (hooks/src/lib.rs).

How do matchers work?

A matcher is read in one of three ways (matcher.rs):

Matcher value Read as
Omitted, "" or "*" Matches everything
Only ASCII letters, digits, _ and | Exact names, split on |: Edit|Write matches Edit or Write, and Bash does not match BashOutput
Anything else A regular expression, such as ^Bash$

A matcher that is not a valid regular expression is skipped with an “invalid matcher” warning, and its handlers do not load (discovery.rs).

Tool names in Codex

The tool names a hook sees differ from Claude Code’s, and Codex adds matcher aliases so Claude Code-style matchers still select the right tools (hook_names.rs):

Codex tool tool_name in hook input Also matched by
Shell-like tools Bash —
File edits apply_patch Write, Edit
Spawning subagents spawn_agent Agent

The alias only selects the hook; the JSON on stdin keeps the canonical name, so a script that checks tool_name == "Write" will not see a match. For shell tools tool_input is { "command": ... }; MCP tools pass their resolved JSON arguments (pre_tool_use.rs).

Handler types and their keys

Each matcher group holds a hooks list of handlers with a type (hook_config.rs, discovery.rs):

type Keys Notes
command command, commandWindows (or command_windows), timeout, async, statusMessage, additionalContextLimit Runs a shell command with the event JSON on stdin
mcp_tool server, tool, input, timeout, statusMessage Calls a tool on a configured MCP server; not supported on SessionEnd
prompt, agent — Accepted in the file but skipped: “prompt hooks are not supported yet”, “agent hooks are not supported yet”

Key details from the source:

  • timeout is in seconds. The default is 600. SessionEnd and Interrupt hooks default to 1 second and are capped at 3 (discovery.rs, session_end.rs).
  • async: true runs the command without waiting. Async hooks cannot block or rewrite anything; only synchronous hooks apply control effects. An async SessionEnd hook runs synchronously, with a warning (engine/mod.rs).
  • additionalContextLimit is an approximate token threshold for moving a hook’s additionalContext to disk. Unset means 2,500 tokens; 0 turns spilling off. It applies only to PreToolUse, PostToolUse, SessionStart, UserPromptSubmit and SubagentStart.
  • commandWindows replaces command on Windows. Commands run through $SHELL -lc (falling back to /bin/sh), or COMSPEC (falling back to cmd.exe) on Windows, in the session’s working directory (command_runner.rs).

An mcp_tool handler from the tests, which passes the edited file’s path to a scanning tool after apply_patch (matched here through the Write|Edit aliases):

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "mcp_tool",
        "server": "security",
        "tool": "scan",
        "input": {
          "file_path": "${tool_input.file_path}",
          "include_ignored": false
        },
        "timeout": 30,
        "statusMessage": "Scanning file"
      }]
    }]
  }
}

${field.nested} placeholders in input are filled from the hook event’s JSON before the call; a placeholder that is not found is an error (mcp_runner.rs). Values must be representable in TOML, so a literal null is rejected.

What JSON does a hook receive?

Command hooks get one JSON object on stdin. The fields are defined in the generated schemas in codex-rs/hooks/schema/generated:

Field Present on
session_id, cwd, hook_event_name, transcript_path (string or null) Every event
turn_id, model Every event except SessionStart (no turn_id) and SessionEnd
permission_mode: default, acceptEdits, plan, dontAsk or bypassPermissions Tool, prompt, session-start, stop, subagent and interrupt events
tool_name, tool_input PreToolUse, PermissionRequest, PostToolUse
tool_use_id PreToolUse, PostToolUse
tool_response PostToolUse
prompt UserPromptSubmit
source SessionStart
trigger PreCompact, PostCompact
reason SessionEnd
stop_hook_active, last_assistant_message Stop, SubagentStop
agent_id, agent_type Subagent events, and tool and prompt events inside a subagent
agent_transcript_path SubagentStop

What do exit codes mean?

The event handlers read the exit code like this (pre_tool_use.rs, stop.rs, post_tool_use.rs):

Exit code Result
0, empty stdout Nothing to do
0, JSON on stdout Parsed as the hook’s output; stdout that looks like JSON but does not parse marks the hook as failed
0, plain text on stdout Becomes model context for SessionStart and SubagentStart; ignored for PostToolUse
2 with a reason on stderr Blocks: the tool call (PreToolUse), the prompt (UserPromptSubmit), or gives feedback (PostToolUse); on Stop the stderr text becomes the prompt for another pass
2 with empty stderr The hook is reported as failed, for example “PreToolUse hook exited with code 2 but did not write a blocking reason to stderr”
Any other code Reported as “hook exited with code N”; the action goes ahead

What JSON can a hook return?

Every output schema accepts systemMessage, a message shown as a warning. Most also accept continue, stopReason and suppressOutput, but not every event honors them (output_parser.rs).

Deny a tool call (PreToolUse). The reason is required; a deny without one is treated as invalid output:

{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"do not run that"}}

Rewrite a tool call (PreToolUse). updatedInput works only together with permissionDecision: "allow":

{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","updatedInput":{"command":"echo rewritten"}}}

On PreToolUse, these outputs are reported as unsupported and ignored, so the call goes ahead: allow without updatedInput, ask, continue: false, stopReason, suppressOutput, and the older {"decision":"approve"}. The older {"decision":"block","reason":"..."} form still blocks (pre_tool_use.rs).

Add context for the model (PostToolUse; also PreToolUse, SessionStart, UserPromptSubmit, SubagentStart):

{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"Remember the bash cleanup note."}}

Keep Codex working (Stop). The reason becomes the continuation prompt:

{"decision":"block","reason":"retry with tests"}

Decide a permission request (PermissionRequest). hookSpecificOutput.decision.behavior is allow or deny, with an optional message. When several hooks answer, any deny wins; otherwise the highest-precedence allow is used. updatedInput, updatedPermissions and interrupt are not supported (permission_request.rs).

Why a hook needs review before it runs

Hooks from user, project and plugin files run only after you trust them. Codex hashes each hook’s normalized definition; a hook with no trusted hash is Untrusted, and one whose definition changed since you trusted it is Modified. Neither runs until you trust it again. Managed hooks (system config and requirements.toml) always run (discovery.rs).

Type /hooks to open the browser. It lists installed, active and to-review counts per event, shows a notice such as “1 hook needs review before it can run”, and offers t to trust all and enter to review. Inside an event you can turn single hooks on or off; “Your changes are saved automatically” (hooks browser snapshots). The choices are stored per hook in a state table:

[state."/tmp/hooks.json:pre_tool_use:0:0"]
enabled = false
trusted_hash = "sha256:abc123"

That snippet comes from the config tests. The key is the source file, the event, the matcher group index and the handler index, so the /hooks browser is the practical way to manage it.

Managed hooks only

For organizations, docs/config.md in the repository says: “Admins can set top-level allow_managed_hooks_only = true in requirements.toml to ignore user, project, and session hook configs while still allowing managed hooks from requirements and managed config layers. This setting is only supported in requirements.toml; putting it in config.toml does not enable managed-hooks-only mode.”

For the rest of Codex setup, see what Codex is and how to install it and the Codex guides. If you also use Claude Code, the Claude Code hooks guide covers its version of the same idea.