Skip to main content

Codex Hooks API

Reference for how Codex CLI hooks work and the JSON contracts used by this project.

hooks.json

Codex discovers hooks next to active config layers (all matching hooks run):

ScopePath
Project<repo>/.codex/hooks.json (loads only when the project layer is trusted)
User~/.codex/hooks.json
Managedrequirements.toml [hooks] (enterprise)

Format

The installer writes this shape (timeout is in seconds):

{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "\"/usr/local/bin/node\" \"$(git rev-parse --show-toplevel)/.codex/hooks/user-prompt-submit.mjs\"",
"timeout": 15,
"statusMessage": "Prisma AIRS prompt scan"
}
]
}
],
"PreToolUse": [
{
"matcher": "mcp__.*",
"hooks": [
{
"type": "command",
"command": "\"/usr/local/bin/node\" \"$(git rev-parse --show-toplevel)/.codex/hooks/pre-tool-use.mjs\"",
"timeout": 15,
"statusMessage": "Prisma AIRS MCP input scan"
}
]
}
],
"PostToolUse": [
{
"matcher": "mcp__.*",
"hooks": [
{
"type": "command",
"command": "\"/usr/local/bin/node\" \"$(git rev-parse --show-toplevel)/.codex/hooks/post-tool-use.mjs\"",
"timeout": 15,
"statusMessage": "Prisma AIRS MCP output audit"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"/usr/local/bin/node\" \"$(git rev-parse --show-toplevel)/.codex/hooks/stop.mjs\"",
"timeout": 15,
"statusMessage": "Prisma AIRS response scan"
}
]
}
]
}
}
FieldDescription
matcherRegex applied per event (tool name for PreToolUse/PostToolUse). Omit to match all.
typeOnly "command" handlers run today
commandShell command to execute. The installer embeds the absolute node binary (process.execPath at install time) because Codex runs hooks with a system PATH — nvm/asdf-managed node is not on it and a bare node fails with exit 127.
timeoutMax execution time in seconds (Codex default: 600)
statusMessageOptional status text shown while the hook runs
Hooks must be trusted

Codex requires you to review and trust non-managed hooks via /hooks before they run. Trust is recorded against each definition's hash — any change requires re-trusting.

Node version switches

Because the node path is baked in at install time, re-run pnpm install-hooks (then re-trust via /hooks) after switching node versions with nvm/asdf.

Hook Input (stdin)

Every hook receives one JSON object on stdin with common base fields:

{
"session_id": "...",
"transcript_path": "...",
"cwd": "...",
"hook_event_name": "UserPromptSubmit",
"permission_mode": "default",
"model": "...",
"turn_id": "..."
}

UserPromptSubmit

Additional fields:

FieldTypeDescription
promptstringThe user's prompt text

PreToolUse

Additional fields:

FieldTypeDescription
tool_namestringCanonical tool name — MCP tools use mcp__server__tool
tool_use_idstringTool-call id for this invocation
tool_inputJSON valueTool-specific input; MCP tools send all arguments

PostToolUse

Additional fields:

FieldTypeDescription
tool_namestringCanonical tool name (mcp__server__tool)
tool_use_idstringTool-call id for this invocation
tool_inputJSON valueTool-specific input
tool_responseJSON valueTool output — for MCP tools, the MCP call result

Stop

Additional fields:

FieldTypeDescription
stop_hook_activebooleanWhether this turn was already continued by a Stop hook
last_assistant_messagestring | nullLatest assistant message text, if available

Hook Output (stdout)

UserPromptSubmit

Allow:

{ "continue": true }

Block:

{
"decision": "block",
"reason": "Prisma AIRS blocked this prompt: ..."
}

PreToolUse

Allow: exit 0 with no output.

Deny:

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Prisma AIRS blocked MCP tool call: ..."
}
}
Never emit continue / stopReason / suppressOutput from PreToolUse

Codex does not support those fields for PreToolUse. If a hook returns them, Codex marks the hook run as failed and lets the tool call proceed — the opposite of blocking.

PostToolUse (observe-only by policy)

This project emits nothing (exit 0). Violations are logged to the scan log and warned on stderr.

Codex could block here — we choose not to

Codex supports {"decision": "block", "reason": "..."} for PostToolUse (it replaces the tool result with your feedback). This project runs PostToolUse observe-only by design; the tool has already executed and side effects cannot be undone.

Stop

Stop expects JSON on stdout when it exits 0 — plain text is invalid for this event.

Continue:

{ "continue": true }

Terminate the turn (AIRS returned action: "block"):

{
"continue": false,
"stopReason": "Prisma AIRS blocked this response: ..."
}

The hook skips scanning when stop_hook_active is true (loop guard) and always fails open on errors.

Exit Codes

CodeHookMeaning
0AllSuccess — output (if any) is parsed as JSON
2UserPromptSubmitBlock, with reason from stderr (alternative to JSON; not used by this project)
2PreToolUseDeny, with reason from stderr (alternative to JSON; not used by this project)
OtherAllHook error — Codex continues (hooks are not enforcement boundaries)

Codex Limitation: No Streaming Interception

Codex has no hook that runs while the response streams. Stop fires only after the final assistant message is already visible.

HookWhat it gates
UserPromptSubmitUser prompt → agent (can block)
PreToolUseBash, apply_patch, and MCP tool calls (can deny)
PermissionRequestApproval prompts (can allow/deny)
PostToolUseTool results before the agent processes them (can block the result, not the side effects)
StopTurn completion (can terminate, not retract)

No hook intercepts the assistant's text before display. Practical guidance:

  • Lean on prompt-side blocking — if AIRS catches a DLP pattern going in, the agent never sees it to echo back
  • Use Stop for containment — a block verdict terminates the turn so the session doesn't keep building on flagged content
  • Use response scanning for audit — violations are logged for compliance evidence and security team alerting

Scope Notes

  • MCP-only tool scanning: this project registers mcp__.* matchers only. Bash commands and apply_patch file edits are intentionally not scanned.
  • Non-shell, non-MCP tools (e.g. web search) are not interceptable by current Codex hooks; final responses are still scanned by Stop.
  • Full wire format: see the Codex hooks reference and the generated schemas in the Codex repository.