Codex CLI Hooks Reference

Updated by

Independent reference, not affiliated with OpenAI. Tables are generated from the Codex CLI source at rust-v0.159.2 (v0.159.2). Official documentation: learn.chatgpt.com.

Codex CLI runs hooks on 12 lifecycle events, from PreToolUse to Interrupt. Each hook is a shell command (or an MCP tool call) that receives a JSON payload on stdin and can answer with JSON on stdout. Hooks load from hooks.json or a [hooks] table in config.toml, and only run after you trust them.

Which hook events does Codex CLI support?#

EventMatcher compared againstDefault timeoutWhat stdout JSON can doInput fields
PreToolUsetool name600sdecision: block, permissionDecision: allow | deny | ask, rewrite tool input, add context, continue: false11
PermissionRequesttool name600sdecision.behavior: allow | deny, rewrite tool input, update permissions, continue: false10
PostToolUsetool name600sdecision: block, add context, continue: false12
PreCompacttrigger (manual or auto)600scontinue: false8
PostCompacttrigger (manual or auto)600scontinue: false8
SessionStartsession source600sadd context, continue: false6
SessionEndend reason (always "other")1s (max 3s)no output schema (observe only)4
UserPromptSubmitignored600sdecision: block, add context, continue: false9
SubagentStartagent type600sadd context, continue: false8
SubagentStopagent type600sdecision: block, continue: false11
Stopignored600sdecision: block, continue: false8
Interruptignored1s (max 3s)none (observe only)6

matcher narrows a hook to specific inputs. An empty matcher or * matches everything; a value of only letters, digits, _ and | is an exact list (Bash|apply_patch); anything else is a regular expression. On events marked "ignored" the matcher has no effect and every hook for that event runs.

Tool events serialize Codex's own tool name, but matchers also accept the Claude Code names below, so a hook written as "matcher": "Write|Edit" fires on Codex file edits.

tool_name in stdinAlso matched by
apply_patchWrite, Edit
spawn_agentAgent
Bash—

Where do Codex hooks live: hooks.json or config.toml?#

Every config layer can carry hooks: the user layer (~/.codex/), a project layer (.codex/ in the repository), and system or managed layers set by an administrator. In each layer Codex reads a hooks.json file in the config folder and a [hooks] table in that layer's config.toml. Both load if both exist, with a warning to keep one representation per layer.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "~/.codex/hooks/check-shell.sh", "timeout": 30 }
        ]
      }
    ]
  }
}

The same hook in config.toml:

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

[[hooks.PreToolUse.hooks]]
type = "command"
command = "~/.codex/hooks/check-shell.sh"
timeout = 30

Each handler has a type. Two run in this release:

typeFieldsRuns in this release
commandcommand, commandWindows, timeout, async, statusMessage, additionalContextLimityes
mcp_toolserver, tool, input, timeout, statusMessageyes
prompt—no (skipped with a warning)
agent—no (skipped with a warning)

timeout is in seconds. async: true starts the command and moves on without waiting, which also means an async hook cannot block, rewrite input, or deny anything: only synchronous hooks apply control effects. commandWindows replaces command on Windows.

How does a Codex hook block a tool call?#

The exit code decides first, then stdout:

  • Exit 0, empty stdout: success, no effect.
  • Exit 0, JSON stdout: parsed against the event's output schema below. "decision": "block" with a reason blocks; on PreToolUse, hookSpecificOutput.permissionDecision answers allow, deny or ask, and updatedInput replaces the tool arguments. Output that looks like JSON but fails the schema marks the hook failed ("hook returned invalid pre-tool-use JSON output").
  • Exit 2: blocks, using stderr as the reason shown to the model. Exit 2 with empty stderr is treated as a failure, not a block.
  • Any other exit code: the hook failed and Codex reports hook exited with code N. The tool call proceeds.

On Stop and SubagentStop, "decision": "block" means "do not stop yet": the reason goes back to the agent as its next instruction, which is how a hook keeps Codex working until tests pass.

Why does Codex say my hooks need review?#

Codex hashes each hook definition and stores the hash of the version you approved under [hooks.state] in config.toml. A hook with no stored hash is untrusted; one whose definition changed since you approved it is modified. At startup Codex lists both under "Hooks need review" with three choices: review them one by one, trust all and continue, or continue without trusting, in which case those hooks do not run. Trusted hooks can run outside the sandbox, which is why the prompt exists. Hooks from system and managed layers skip review, and a hook can be switched off with enabled = false in its state entry.

When does the Codex Interrupt hook run?#

After cancellation, never before. When you interrupt a turn, Codex gives the running task 100 ms to finish, aborts it, records the interrupted marker in the transcript and flushes the transcript file. Only then does the Interrupt hook run, and TurnAborted is emitted after it returns. The hook therefore cannot cancel or veto the interrupt; its only output is a systemMessage. Its default timeout is 1 second and any configured value is clamped to 3 seconds, so a slow hook delays the abort by at most that long. It does not run for subagent sessions or when you interrupt with no turn in progress.

Is the hooks feature on by default?#

Feature keyStageDefaultLegacy alias
hooksstableoncodex_hooks

Hooks no longer need a feature flag. The old codex_hooks key still works as an alias for hooks under [features], so hooks = false there is the way to switch every hook off.

Hook input and output fields#

Every hook receives session_id, cwd, transcript_path, model and hook_event_name, plus the event-specific fields below. permission_mode uses Claude Code's vocabulary (default, acceptEdits, plan, dontAsk, bypassPermissions) so existing hook scripts can read it unchanged.

PreToolUse#

FieldTypeRequiredNotes
agent_idstringno—
agent_typestringno—
cwdstringyes—
modelstringyes—
permission_modedefault | acceptEdits | plan | dontAsk | bypassPermissionsyes—
session_idstringyes—
tool_inputanyyes—
tool_namestringyes—
tool_use_idstringyes—
transcript_pathstring (nullable)yes—
turn_idstringyesCodex extension: expose the active turn id to internal turn-scoped hooks.
FieldTypeDefaultNotes
continuebooleantrue—
decisionapprove | block——
hookSpecificOutputobject——
hookSpecificOutput.additionalContextstring——
hookSpecificOutput.hookEventName"PreToolUse"——
hookSpecificOutput.permissionDecisionallow | deny | ask——
hookSpecificOutput.permissionDecisionReasonstring——
hookSpecificOutput.updatedInputany——
reasonstring——
stopReasonstring——
suppressOutputbooleanfalse—
systemMessagestring——

PermissionRequest#

FieldTypeRequiredNotes
agent_idstringno—
agent_typestringno—
cwdstringyes—
modelstringyes—
permission_modedefault | acceptEdits | plan | dontAsk | bypassPermissionsyes—
session_idstringyes—
tool_inputanyyes—
tool_namestringyes—
transcript_pathstring (nullable)yes—
turn_idstringyesCodex extension: expose the active turn id to internal turn-scoped hooks.
FieldTypeDefaultNotes
continuebooleantrue—
hookSpecificOutputobject——
hookSpecificOutput.decisionobject——
hookSpecificOutput.decision.behaviorallow | deny——
hookSpecificOutput.decision.interruptbooleanfalseReserved for future short-circuiting semantics. PermissionRequest hooks currently fail closed if this field is `true`.
hookSpecificOutput.decision.messagestring——
hookSpecificOutput.decision.updatedInputany—Reserved for a future input-rewrite capability. PermissionRequest hooks currently fail closed if this field is present.
hookSpecificOutput.decision.updatedPermissionsany—Reserved for a future permission-rewrite capability. PermissionRequest hooks currently fail closed if this field is present.
hookSpecificOutput.hookEventName"PermissionRequest"——
stopReasonstring——
suppressOutputbooleanfalse—
systemMessagestring——

PostToolUse#

FieldTypeRequiredNotes
agent_idstringno—
agent_typestringno—
cwdstringyes—
modelstringyes—
permission_modedefault | acceptEdits | plan | dontAsk | bypassPermissionsyes—
session_idstringyes—
tool_inputanyyes—
tool_namestringyes—
tool_responseanyyes—
tool_use_idstringyes—
transcript_pathstring (nullable)yes—
turn_idstringyesCodex extension: expose the active turn id to internal turn-scoped hooks.
FieldTypeDefaultNotes
continuebooleantrue—
decisionblock——
hookSpecificOutputobject——
hookSpecificOutput.additionalContextstring——
hookSpecificOutput.hookEventName"PostToolUse"——
hookSpecificOutput.updatedMCPToolOutputany——
reasonstring——
stopReasonstring——
suppressOutputbooleanfalse—
systemMessagestring——

PreCompact and PostCompact#

FieldTypeRequiredNotes
agent_idstringno—
agent_typestringno—
cwdstringyes—
modelstringyes—
session_idstringyes—
transcript_pathstring (nullable)yes—
triggermanual | autoyes—
turn_idstringyesCodex extension: expose the active turn id to internal turn-scoped hooks.
FieldTypeDefaultNotes
continuebooleantrue—
stopReasonstring——
suppressOutputbooleanfalse—
systemMessagestring——

SessionStart#

FieldTypeRequiredNotes
cwdstringyes—
modelstringyes—
permission_modedefault | acceptEdits | plan | dontAsk | bypassPermissionsyes—
session_idstringyes—
sourcestartup | resume | clear | compact | forkyes—
transcript_pathstring (nullable)yes—
FieldTypeDefaultNotes
continuebooleantrue—
hookSpecificOutputobject——
hookSpecificOutput.additionalContextstring——
hookSpecificOutput.hookEventName"SessionStart"——
stopReasonstring——
suppressOutputbooleanfalse—
systemMessagestring——

SessionEnd#

FieldTypeRequiredNotes
cwdstringyes—
reason"other"yes—
session_idstringyes—
transcript_pathstring (nullable)yes—

UserPromptSubmit#

FieldTypeRequiredNotes
agent_idstringno—
agent_typestringno—
cwdstringyes—
modelstringyes—
permission_modedefault | acceptEdits | plan | dontAsk | bypassPermissionsyes—
promptstringyes—
session_idstringyes—
transcript_pathstring (nullable)yes—
turn_idstringyesCodex extension: expose the active turn id to internal turn-scoped hooks.
FieldTypeDefaultNotes
continuebooleantrue—
decisionblock——
hookSpecificOutputobject——
hookSpecificOutput.additionalContextstring——
hookSpecificOutput.hookEventName"UserPromptSubmit"——
reasonstring——
stopReasonstring——
suppressOutputbooleanfalse—
systemMessagestring——

SubagentStart#

FieldTypeRequiredNotes
agent_idstringyes—
agent_typestringyes—
cwdstringyes—
modelstringyes—
permission_modedefault | acceptEdits | plan | dontAsk | bypassPermissionsyes—
session_idstringyes—
transcript_pathstring (nullable)yes—
turn_idstringyesCodex extension: expose the active turn id to internal turn-scoped hooks.
FieldTypeDefaultNotes
continuebooleantrue—
hookSpecificOutputobject——
hookSpecificOutput.additionalContextstring——
hookSpecificOutput.hookEventName"SubagentStart"——
stopReasonstring——
suppressOutputbooleanfalse—
systemMessagestring——

SubagentStop#

FieldTypeRequiredNotes
agent_idstringyes—
agent_transcript_pathstring (nullable)yes—
agent_typestringyes—
cwdstringyes—
last_assistant_messagestring (nullable)yes—
modelstringyes—
permission_modedefault | acceptEdits | plan | dontAsk | bypassPermissionsyes—
session_idstringyes—
stop_hook_activebooleanyes—
transcript_pathstring (nullable)yes—
turn_idstringyesCodex extension: expose the active turn id to internal turn-scoped hooks.
FieldTypeDefaultNotes
continuebooleantrue—
decisionblock——
reasonstring—Claude requires `reason` when `decision` is `block`; we enforce that semantic rule during output parsing rather than in the JSON schema.
stopReasonstring——
suppressOutputbooleanfalse—
systemMessagestring——

Stop#

FieldTypeRequiredNotes
cwdstringyes—
last_assistant_messagestring (nullable)yes—
modelstringyes—
permission_modedefault | acceptEdits | plan | dontAsk | bypassPermissionsyes—
session_idstringyes—
stop_hook_activebooleanyes—
transcript_pathstring (nullable)yes—
turn_idstringyesCodex extension: expose the active turn id to internal turn-scoped hooks.
FieldTypeDefaultNotes
continuebooleantrue—
decisionblock——
reasonstring—Claude requires `reason` when `decision` is `block`; we enforce that semantic rule during output parsing rather than in the JSON schema.
stopReasonstring——
suppressOutputbooleanfalse—
systemMessagestring——

Interrupt#

FieldTypeRequiredNotes
cwdstringyes—
modelstringyes—
permission_modedefault | acceptEdits | plan | dontAsk | bypassPermissionsyes—
session_idstringyes—
transcript_pathstring (nullable)yes—
turn_idstringyesCodex extension: expose the active turn id to internal turn-scoped hooks.
FieldTypeDefaultNotes
systemMessagestring——

Frequently asked questions#

Does Codex CLI have hooks like Claude Code?#

Yes. Codex uses the same event names as Claude Code (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionStart and others), the same matcher plus hooks array shape, the same exit code 2 blocking rule and Claude Code's permission_mode values. Matchers accept Write, Edit and Agent as aliases for Codex's apply_patch and spawn_agent, so many Claude Code hook scripts run unchanged. Codex adds PermissionRequest and Interrupt.

How do I enable hooks in Codex?#

Hooks are on by default: the hooks feature is stable and enabled. Add a hooks.json to ~/.codex/ or a [hooks] table to config.toml, start Codex, and approve the new hooks in the "Hooks need review" prompt. The legacy codex_hooks = true setting is no longer needed.

Where do I put hooks.json for Codex?#

In a config folder: ~/.codex/hooks.json for your user, or .codex/hooks.json in a repository for project hooks. The same definitions can instead go in a [hooks] table in that folder's config.toml.

What does "hook exited with code 1" mean in Codex?#

The hook command failed with a non-zero exit code other than 2, so Codex marked it failed and continued. Exit 127 means the shell could not find the command: check the path in command and that the script is executable. Only exit code 2 blocks.

Can a Codex hook block a tool call?#

Yes, from a synchronous PreToolUse hook: exit with code 2 and write the reason to stderr, or exit 0 and print {"decision": "block", "reason": "..."}. permissionDecision: "deny" in hookSpecificOutput also denies the call. Async hooks cannot block.

Is there a Codex hook that runs when I interrupt a turn?#

Yes, the Interrupt event. It runs after the turn is cancelled and the transcript is flushed, before Codex emits TurnAborted. It cannot stop the interrupt, has a 1 second default timeout capped at 3 seconds, and does not fire for subagents.

Why are my Codex hooks not running?#

The usual causes are an unapproved hook (choose "Review hooks" or "Trust all" at startup), an async hook expected to block, a matcher that does not match the tool name (Codex serializes apply_patch, not Write), or hooks = false under [features].

Sources

Release-by-release changes: Codex CLI version tracker. All Codex CLI pages: Codex CLI reference index.