Hooks are an extensibility framework that inject custom scripts into the Codex agentic loop — enabling deterministic automation for logging, security scanning, validation, conversation summarization, and context-aware prompting.
| ← Back to Codex CLI Best Practice |
Status: Experimental — under active development. Windows support temporarily disabled.
Hooks require enabling in config.toml:
[features]
codex_hooks = trueCodex discovers hooks.json files at two levels — both load simultaneously; higher-precedence layers don't replace lower-precedence hooks:
| Priority | Location | Scope |
|---|---|---|
| 1 | .codex/hooks.json |
Project (team-shared) |
| 2 | ~/.codex/hooks.json |
Global (personal) |
| Event | Matcher | Description |
|---|---|---|
SessionStart |
startup | resume |
Runs at session initialization |
PreToolUse |
Bash |
Intercepts tool execution before running (Bash only) |
PostToolUse |
Bash |
Reviews tool results after execution (Bash only) |
UserPromptSubmit |
Not supported | Runs when user submits a prompt |
Stop |
Not supported | Runs when a turn completes — determines whether to continue |
Hooks organize into three levels: event → matcher group → hook handlers
{
"hooks": {
"EventName": [
{
"matcher": "pattern|regex",
"hooks": [
{
"type": "command",
"command": "script_path",
"statusMessage": "optional UI feedback",
"timeout": 600
}
]
}
]
}
}| Option | Default | Description |
|---|---|---|
timeout / timeoutSec |
600s | Execution time limit in seconds |
statusMessage |
— | Optional UI feedback during execution |
matcher |
Match all | Regex to filter event firing ("*", "", or omit for all) |
- Matching hooks from multiple files all execute
- Multiple command hooks for the same event launch concurrently
- One hook cannot prevent another from running
- Commands run with session
cwdas working directory
Injects context at session initialization.
Input fields: source, session_id, transcript_path, cwd, hook_event_name, model
Output: Plain text on stdout is added as developer context. JSON output supports:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "text added as context"
}
}Intercepts tool execution before running (currently Bash only).
Note: The model can circumvent this by writing and executing scripts directly — treat as a useful guardrail rather than a complete enforcement boundary.
Input fields: turn_id, tool_name, tool_use_id, tool_input.command, plus common fields
Deny execution:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "reason text"
}
}Alternative: Exit code 2 with blocking reason on stderr.
Reviews tool results after execution (Bash only). Cannot undo side effects but can replace the tool result with feedback.
Input fields: turn_id, tool_name, tool_use_id, tool_input.command, tool_response, plus common fields
Block and replace result:
{
"decision": "block",
"reason": "feedback reason",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "context text"
}
}Alternative: Exit code 2 with feedback reason on stderr.
Runs when user submits a prompt. Matcher not supported.
Input fields: turn_id, prompt, plus common fields
Block submission:
{
"decision": "block",
"reason": "reason text"
}Add context:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "context text"
}
}Runs when a turn completes — determines whether to continue automatically. Matcher not supported.
Input fields: turn_id, stop_hook_active, last_assistant_message, plus common fields
Continue with automatic prompt:
{
"decision": "block",
"reason": "continuation reason text"
}
decision: "block"tells Codex to continue (not reject). The reason becomes the next prompt text. If any matching hook returnscontinue: false, that takes precedence.
Every command hook receives JSON on stdin:
| Field | Type | Description |
|---|---|---|
session_id |
string | Session/thread ID |
transcript_path |
string | null | Path to session transcript |
cwd |
string | Working directory |
hook_event_name |
string | Current event name |
model |
string | Active model slug |
turn_id |
string | Turn-scoped hooks only |
SessionStart, UserPromptSubmit, and Stop support:
| Field | Type | Description |
|---|---|---|
continue |
boolean | false marks hook as stopped |
stopReason |
string | Recorded as stop reason |
systemMessage |
string | Surfaced as UI warning |
suppressOutput |
boolean | Parsed, not yet implemented |
Exit 0 with no output is treated as success — Codex continues normally.
For repo-local hooks, prefer git-root-based paths to avoid issues when Codex starts from subdirectories:
/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/script.py"
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "python3 ~/.codex/hooks/session_start.py",
"statusMessage": "Loading session notes"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py\"",
"statusMessage": "Checking Bash command"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/post_tool_use_review.py\"",
"statusMessage": "Reviewing Bash output"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/user_prompt_submit.py\""
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_continue.py\"",
"timeout": 30
}
]
}
]
}
}| Anti-Pattern | Fix |
|---|---|
Relying on PreToolUse as a security boundary |
Treat as a guardrail — the model can write scripts to bypass it |
| Using relative paths in hook commands | Use $(git rev-parse --show-toplevel) for stability |
Missing the [features] flag |
Always enable codex_hooks = true in config.toml |
| Setting very long timeouts on blocking hooks | Keep timeouts short to avoid stalling the agent loop |
Assuming hooks can undo PostToolUse side effects |
They can only replace the result, not reverse the action |
| Not handling JSON stdin properly | Every hook receives JSON on stdin — parse it correctly |