A hook is a command that Claude Code or Codex runs at a fixed point in a session. It gets one JSON object on stdin, and it answers with its exit code and, optionally, one JSON object on stdout. This page lists, for each hook event, when it fires, the fields it receives, what stdout and exit code 2 do, and the timeouts. It is an unofficial summary of the Claude Code hooks reference and the Codex hooks documentation as published on 6 November 2026; field names are copied from them.

For how to register hooks, matchers and handler types, see Claude Code hooks: events, matchers, settings and handlers. For a worked example that turns hook events into a live session list, see Build your own status display with Claude Code hooks.
How to read this page
- Two levels of detail: Claude Code has 33 hook events. The 18 events a status display or approval tool usually needs get a full entry below. The other 15 are in the summary table, with when they fire, their matcher and what exit code 2 does. Codex has 14 events, and each gets a full entry.
- Command hooks: everything here is for
"type": "command"handlers, which read stdin and write stdout. Claude Code sends HTTP hooks the same JSON as the POST body. - Examples: the Claude Code examples are the official examples. The Codex documentation publishes field tables and one full example (SessionEnd), so the other Codex examples here are built from those tables with placeholder values.
- Only what is documented: if a field, exit code or timeout is not in the official documentation, it is not on this page.
Claude Code: fields every hook receives
| Field | Type | Notes |
|---|---|---|
session_id | string | Current session identifier |
prompt_id | string | UUID of the prompt being processed. Absent until the first user input |
transcript_path | string | Path to the conversation JSON. Written asynchronously, so it can lag the current turn. On Stop and SubagentStop, read last_assistant_message instead |
cwd | string | Working directory when the hook runs |
scratchpad_dir | string | The session's scratchpad directory. Absent when there is none. Claude Code v2.1.257 or later |
permission_mode | string | default, plan, acceptEdits, auto, dontAsk or bypassPermissions. Manual mode arrives as default. Not every event receives it |
effort | object | level is low, medium, high, xhigh or max. On PreToolUse, PostToolUse, Stop, SubagentStop and other events with a tool-use context |
hook_event_name | string | Name of the event that fired |
agent_id | string | Present when the hook fires in a subagent or in-process teammate |
agent_type | string | Agent name, such as Explore. Present inside a subagent or with --agent |
Only SessionStart can carry a model field, and not always. There is no $CLAUDE_MODEL environment variable.
Claude Code: exit codes and stdout
Claude Code combines the exit code with what the hook printed on stdout:
| Stdout | Exit 0 | Exit 2 | Any other exit code |
|---|---|---|---|
| One JSON object that passes validation | Success, the fields apply | Blocking error. The fields are read but cannot override the block | Success. The exit code is ignored and the fields decide |
| JSON that cannot be parsed or fails validation | Non-blocking error | Blocking error, stderr is the reason | Non-blocking error |
| Plain text, or nothing | Success | Blocking error, stderr is the reason | Non-blocking error |
- Exit 0: stdout counts as JSON only when it is one JSON object with nothing but whitespace around it. A line that your shell profile echoes is enough to break it.
- Exit 2: blocks the action on events that can block. What that means per event is in the tables below.
- Exit 1 and others without valid JSON: a non-blocking error. The action goes ahead. To enforce a policy, exit 2.
- Plain stdout that reaches Claude: only on UserPromptSubmit, UserPromptExpansion, SessionStart and PostModelSwitch, where it is added as context. On PreCompact it becomes instructions for the compaction summary. On other events it goes to the debug log.
- stderr: Claude never sees stderr from a hook that exits 0.
- Failure that blocks: set
"onFailure": "block"on a command or HTTP hook (Claude Code v2.1.295 or later) to make a crash, a timeout or invalid output do what exit 2 does on that event. On PermissionRequest it denies the request.
Claude Code: JSON output fields
Every event accepts these top-level fields, although some events discard them:
| Field | Default | Effect |
|---|---|---|
continue | true | false stops Claude after the hook. Takes precedence over event-specific decisions |
stopReason | none | Shown to the user when continue is false |
suppressOutput | false | Accepted, no effect |
systemMessage | none | Warning shown to the user |
terminalSequence | none | A desktop notification, window title or bell, emitted by Claude Code. Only OSC 0, 1, 2, 9, 99, 777 and BEL are allowed |
Event-specific results go either in a top-level decision (whose only value is "block") with a reason, or in a hookSpecificOutput object that must name the event in hookEventName. additionalContext, systemMessage, initialUserMessage and plain stdout are each capped at 10,000 characters. Longer output is saved to a file and Claude gets the path and a 2,000-character preview.
Claude Code: timeouts
| Handler or event | Default timeout |
|---|---|
command, http, mcp_tool | 600 seconds |
prompt | 30 seconds |
agent | 60 seconds |
UserPromptSubmit, PreModelSwitch, PostModelSwitch (command, http, mcp_tool) | 30 seconds |
MessageDisplay (command, http, mcp_tool) | 10 seconds |
| SessionEnd | 1.5 seconds shared budget, raised by a per-hook timeout up to 60 seconds, or set with CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS |
Set timeout in seconds on a handler to change it. A hook that times out is cancelled and its output is discarded. On PreToolUse, a timed-out hook does not block the tool call unless it sets "onFailure": "block". Timeouts are not enforced on command hooks with async: true.
Claude Code: event reference
SessionStart
Fires when a session starts or resumes. The matcher filters source: startup, resume, clear, compact or fork. Only command and mcp_tool handlers are supported.
- Fields:
source(string). Optionalmodel(string, check before reading),agent_type(string, withclaude --agent) andsession_title(string, when a custom title is set). Onresumeorforkwith at least one earlier response (v2.1.251 or later):seconds_since_last_response,context_tokens,prompt_cache_likely_expiredandestimated_cache_write_usd. - Stdout: plain text is added to Claude's context. JSON can set
hookSpecificOutput.additionalContext,initialUserMessage,watchPaths,sessionTitleandreloadSkills. - Exit 2: cannot block. stderr is shown to the user only.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "SessionStart",
"source": "resume",
"model": "claude-opus-5",
"seconds_since_last_response": 5400,
"context_tokens": 182340,
"prompt_cache_likely_expired": true,
"estimated_cache_write_usd": 1.1396
}UserPromptSubmit
Fires when a prompt is submitted, before Claude processes it. It also fires for a session-scoped scheduled task, a background subagent reporting back and a message from another session. No matcher support.
- Fields:
prompt(string, pasted text arrives expanded) andsession_title(string, when a custom title is set). A skill or command you typed, such as/deploy staging, arrives as typed. - Stdout: plain text is added to Claude's context. JSON:
decision: "block"withreason(shown to the user, not to Claude), and inhookSpecificOutput:additionalContext,sessionTitleandsuppressOriginalPrompt. The prompt itself cannot be rewritten. - Exit 2: blocks the prompt. stderr is shown to the user.
- Timeout: 30 seconds by default. A timed-out hook's context is discarded and the prompt still reaches Claude.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "UserPromptSubmit",
"prompt": "Write a function to calculate the factorial of a number"
}PreToolUse
Fires after Claude creates tool parameters and before the tool call runs. The matcher filters the tool name, such as Bash, Edit, Write, Read, Agent, AskUserQuestion or an MCP tool name of the form mcp__<server>__<tool>. It does not fire for EndConversation or for files you reference with @ in a prompt.
- Fields:
tool_name(string),tool_input(object, depends on the tool) andtool_use_id(string). MCP tools also getmcp_server, an object withnameandsource(v2.1.274 or later). - Bash
tool_input:command,description,timeoutandrun_in_background. - File paths: for
Write,EditandRead,tool_input.file_pathis always absolute, and on Windows it uses backslashes even under Git Bash. Normalise separators before matching on a path. - Stdout: JSON in
hookSpecificOutput:permissionDecision(allow,deny,askordefer),permissionDecisionReason,updatedInput(replaces the whole input object) andadditionalContext. When hooks disagree,denybeatsdefer, which beatsask, which beatsallow.deferneeds non-interactive mode (-p). The old top-leveldecisionvaluesapproveandblockstill map toallowanddeny. - Exit 2: blocks the tool call. Claude sees stderr as the reason.
- Exit 0 with no output: no decision. The normal permission flow applies.
{
"session_id": "abc123",
"prompt_id": "550e8400-e29b-41d4-a716-446655440000",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"scratchpad_dir": "/tmp/claude-1000/-home-user-my-project/abc123/scratchpad",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite",
"timeout": 120000,
"run_in_background": false
},
"tool_use_id": "toolu_01ABC123..."
}To deny the call with a reason:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database writes are not allowed"
}
}PermissionRequest
Fires when Claude Code is about to ask you for permission to use a tool. It fires at once, while the Notification type permission_prompt waits about six seconds. In sessions that cannot show a prompt, Claude Code denies the call when no hook decides. Same matcher values as PreToolUse.
- Fields:
tool_name(string) andtool_input(object), but notool_use_id. MCP tools also getmcp_server. Optionalpermission_suggestions(array of permission update entries such asaddRulesorsetMode). - Stdout: JSON in
hookSpecificOutput.decision:behavior(allowordeny). Withallow:updatedInputandupdatedPermissions. Withdeny:messageandinterrupt. Deny and ask rules still apply after anallow. - Exit 2: not honoured. The permission flow continues unchanged and stderr is discarded. Use the
decisionobject.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "PermissionRequest",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf node_modules",
"description": "Remove node_modules directory"
},
"permission_suggestions": [
{
"type": "addRules",
"rules": [{ "toolName": "Bash", "ruleContent": "rm -rf node_modules" }],
"behavior": "allow",
"destination": "localSettings"
}
]
}To allow the request:
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow"
}
}
}PostToolUse
Fires right after a tool completes successfully. Same matcher values as PreToolUse.
- Fields:
tool_name,tool_input,tool_response(the tool's structured output, shape depends on the tool),tool_use_idand optionalduration_ms(number, excludes permission prompts and PreToolUse hooks). MCP tools also getmcp_server. - Stdout: JSON:
decision: "block"withreasonadds the reason next to the result. InhookSpecificOutput:additionalContext,updatedToolOutput(replaces the result) andclassifierContext(v2.1.236 or later). - Exit 2: cannot undo the tool. stderr is shown to Claude.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "file content"
},
"tool_response": {
"filePath": "/path/to/file.txt",
"type": "create"
},
"tool_use_id": "toolu_01ABC123...",
"duration_ms": 12
}PostToolUseFailure
Fires when a tool that started running fails, or an MCP tool returns an error result. It does not fire for calls rejected before they run, such as a permission denial. Same matcher values as PreToolUse.
- Fields:
tool_name,tool_input,tool_use_id,error(string), optionalis_interrupt(boolean) and optionalduration_ms. For Bash and PowerShell, the first line oferrorisExit code N. Treat the rest as display text, not a stable format. - Stdout: JSON
hookSpecificOutput.additionalContext. - Exit 2: stderr is shown to Claude. The tool already failed.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "PostToolUseFailure",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite"
},
"tool_use_id": "toolu_01ABC123...",
"error": "Exit code 1\nError: Cannot find module 'express'",
"is_interrupt": false,
"duration_ms": 4187
}PostToolBatch
Fires once after every tool call in a batch has resolved, before the next model request. PostToolUse fires once per tool, concurrently for parallel calls. No matcher support.
- Fields:
tool_calls(array). Each entry hastool_name,tool_input,tool_use_idandtool_response. Heretool_responseis the serialized content the model sees, not the structured object PostToolUse gets. - Stdout: JSON:
decision: "block"withreason, andadditionalContext. - Exit 2: stops the agentic loop before the next model call.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "PostToolBatch",
"tool_calls": [
{
"tool_name": "Read",
"tool_input": {"file_path": "/.../ledger/accounts.py"},
"tool_use_id": "toolu_01...",
"tool_response": "1\tfrom __future__ import annotations\n2\t..."
}
]
}PermissionDenied
Fires when auto mode denies a tool call. It does not fire when you deny a dialog yourself, when a PreToolUse hook blocks a call or when a deny rule matches. The matcher filters the tool name.
- Fields:
tool_name,tool_input,tool_use_idandreason(string, for a classifier verdict usually the rule in square brackets). MCP tools also getmcp_server. - Stdout: JSON
hookSpecificOutput.retry: truetells the model it may retry. Ignored for denials without a classifier verdict. - Exit 2: ignored. The denial already happened.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "auto",
"hook_event_name": "PermissionDenied",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build",
"description": "Clean build directory"
},
"tool_use_id": "toolu_01ABC123...",
"reason": "[Irreversible Local Destruction]"
}Notification
Fires when Claude Code sends a notification, even with desktop notifications turned off. The matcher filters notification_type, for example permission_prompt (after the prompt has waited about six seconds), idle_prompt (about 60 seconds after Claude finished, if you have not typed), elicitation_dialog, agent_needs_input, agent_completed or auth_success.
- Fields:
message(string), optionaltitle(string) andnotification_type(string). - Stdout: no decision control.
systemMessageandcontinueare discarded.terminalSequencestill fires, so a desktop notification can be raised from here. - Exit 2: exit code and stderr are ignored.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "Notification",
"message": "Claude needs your permission",
"title": "Permission needed",
"notification_type": "permission_prompt"
}MessageDisplay
Fires while an assistant message streams to the screen, once per batch of newly completed lines. A long message produces several calls. No matcher support.
- Fields:
turn_id(UUID),message_id(UUID, stable across batches, not the API message id),index(number, zero-based batch index),final(boolean,trueon the last batch) anddelta(string, the new lines). Usefinal, not an emptydelta, as the end of the message. - Stdout: JSON
hookSpecificOutput.displayContentreplaces the text on screen. Display only: the transcript and Claude keep the original. - Exit 2: the original text is displayed.
- Timeout: 10 seconds by default.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
"cwd": "/Users/my-project",
"hook_event_name": "MessageDisplay",
"turn_id": "0c9e6a2f-7d41-4f4e-9a15-3f4f7c2b8d10",
"message_id": "5b2a9c8e-1f63-4d8a-b7c4-9e0d2a6f1c3b",
"index": 0,
"final": false,
"delta": "Here is the plan:\n"
}SubagentStart
Fires when Claude spawns or resumes a subagent with the Agent tool, and each time an in-process agent team teammate handles a new message. The matcher filters the agent type, such as general-purpose, Explore, Plan, a custom agent's name, or a plugin-scoped name like my-plugin:reviewer.
- Fields:
agent_id(string) andagent_type(string). - Stdout: JSON
hookSpecificOutput.additionalContextis added to the subagent's context. It cannot block the subagent. - Exit 2: stderr is shown in the subagent's transcript. Claude does not see it.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "SubagentStart",
"agent_id": "agent-abc123",
"agent_type": "Explore"
}SubagentStop
Fires when a subagent has finished responding, and also when one of Claude Code's internal agents finishes. For those, agent_type can be an empty string, which a named matcher does not match. Same matcher values as SubagentStart.
- Fields:
stop_hook_active(boolean),agent_id,agent_type,agent_transcript_path(string, the subagent's own transcript),last_assistant_message(string), and the parent session'sbackground_tasksandsession_cronsarrays. - Stdout: same as Stop.
decision: "block"withreasonkeeps the subagent running and gives itreasonas its next instruction. - Exit 2: prevents the subagent from stopping. stderr becomes its next instruction.
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../abc123.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "SubagentStop",
"stop_hook_active": false,
"agent_id": "def456",
"agent_type": "Explore",
"agent_transcript_path": "~/.claude/projects/.../abc123/subagents/agent-def456.jsonl",
"last_assistant_message": "Analysis complete. Found 3 potential issues...",
"background_tasks": [],
"session_crons": []
}Stop
Fires when the main agent has finished responding. It does not fire after a user interrupt, and API errors fire StopFailure instead. No matcher support.
- Fields:
stop_hook_active(boolean,truewhen Claude is already continuing because of a Stop hook),last_assistant_message(string),background_tasks(array ofid,type,status,description, pluscommand,agent_type,server,toolornamedepending on the type) andsession_crons(array ofid,schedule,recurring,prompt). The two arrays tell "done" apart from "waiting for background work". - Stdout: JSON:
decision: "block"with a requiredreasonkeeps Claude working.hookSpecificOutput.additionalContextalso continues the turn, labelled as hook feedback instead of an error. - Exit 2: prevents Claude from stopping. stderr is the reason.
- Loop guard: after eight consecutive continuations, Claude Code ends the turn anyway.
CLAUDE_CODE_STOP_HOOK_BLOCK_CAPchanges the cap. Checkstop_hook_activebefore blocking.
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "Stop",
"stop_hook_active": true,
"last_assistant_message": "I've completed the refactoring. Here's a summary...",
"background_tasks": [
{
"id": "task-001",
"type": "shell",
"status": "running",
"description": "tail logs",
"command": "tail -f /var/log/syslog"
}
],
"session_crons": [
{
"id": "cron-001",
"schedule": "0 9 * * 1-5",
"recurring": true,
"prompt": "check the build"
}
]
}StopFailure
Fires instead of Stop when the turn ends because of an API error. The matcher filters error: rate_limit, overloaded, authentication_failed, oauth_org_not_allowed, account_on_hold, billing_error, invalid_request, model_not_found, server_error, max_output_tokens, cloud_credential_error or unknown.
- Fields:
error(string), optionalerror_detailsand optionallast_assistant_message, which here holds the API error text, not Claude's reply. - Stdout: output and exit code are ignored, apart from
terminalSequence.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "StopFailure",
"error": "rate_limit",
"error_details": "429 Too Many Requests",
"last_assistant_message": "API Error: Rate limit reached"
}PreCompact and PostCompact
PreCompact fires before a compaction and PostCompact after it. The matcher filters trigger: manual for /compact, auto for automatic and idle compaction.
- PreCompact fields:
trigger(string) andcustom_instructions(string, ornullforautoand for/compactwith no text). - PostCompact fields:
triggerandcompact_summary(string). - PreCompact stdout: on exit 0 without a block, stdout becomes instructions for the compaction summary.
decision: "block"blocks compaction.systemMessageandcontinueare discarded. - Exit 2: blocks compaction on PreCompact. On PostCompact, stderr is shown to the user only.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "PostCompact",
"trigger": "manual",
"compact_summary": "Summary of the compacted conversation..."
}Elicitation
Fires when an MCP server asks for user input mid-task. A hook can answer instead of the dialog. The matcher filters the MCP server name.
- Fields:
mcp_server_name(string),message(string), and optionalmode,url,elicitation_idandrequested_schema. - Stdout: JSON
hookSpecificOutputwithaction(accept,declineorcancel) andcontent(the form values). Applied only on exit 0.decision: "block"also declines. - Exit 2: declines the request, and no dialog appears.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "Elicitation",
"mcp_server_name": "my-mcp-server",
"message": "Please provide your credentials",
"mode": "form",
"requested_schema": {
"type": "object",
"properties": {
"username": { "type": "string", "title": "Username" }
}
}
}SessionEnd
Fires when a session ends. The matcher filters reason: clear, resume, logout, prompt_input_exit or other.
- Fields:
reason(string). - Stdout: no decision control. JSON output fields such as
systemMessageare discarded. - Exit 2: stderr is shown to the user only.
- Timeout: a 1.5-second budget shared by all SessionEnd hooks. Raise it with a per-hook
timeout(up to 60 seconds) orCLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS.
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "SessionEnd",
"reason": "other"
}Claude Code: the other 15 events at a glance
| Event | When it fires | Matcher | Exit code 2 |
|---|---|---|---|
Setup | Only with --init-only, or --init or --maintenance with -p | init, maintenance | Ignored |
InstructionsLoaded | When a CLAUDE.md or .claude/rules/*.md file is loaded into context | session_start, nested_traversal, path_glob_match, include, compact | Ignored |
UserPromptExpansion | When a typed command expands into a prompt | Command or skill name | Blocks the expansion |
TaskCreated | When a task is created with TaskCreate | None | Rolls back the task creation |
TaskCompleted | When a task is marked completed | None | Prevents the completion |
TeammateIdle | When an agent team teammate is about to go idle | None | Keeps the teammate working |
ConfigChange | When a configuration file changes during a session | user_settings, project_settings, local_settings, policy_settings, skills | Blocks the change, except policy_settings |
CwdChanged | When a shell command changes the working directory | None | stderr shown to the user |
DirectoryAdded | After /add-dir or an SDK register_repo_root request | slash_command, register_repo_root | stderr goes to the debug log |
FileChanged | When a watched file changes on disk, whatever changed it | Literal file names | stderr shown to the user |
WorktreeCreate | When Claude Code creates a worktree | None | Any non-zero exit fails the creation |
WorktreeRemove | When Claude Code removes a worktree a WorktreeCreate hook created | None | Any non-zero exit fails the removal if the directory remains |
PreModelSwitch | Before a requested model switch | Canonical model name | Blocks the switch, stderr shown to the user |
PostModelSwitch | After the session's model changes | Canonical model name | stderr shown to the user |
ElicitationResult | After a user answers an MCP elicitation | MCP server name | Blocks the response, which becomes a decline |
A WorktreeCreate command hook prints the worktree path on stdout, so it cannot return JSON.
Codex: fields every hook receives
Codex reads hooks from ~/.codex/hooks.json, ~/.codex/config.toml, and the same files in a project's .codex/ folder, plus enabled plugins. Hooks from all sources run, and a hook that is not managed must be reviewed and trusted in /hooks before it runs. Trust is recorded against the hook's hash, so a changed hook is skipped until you trust it again.
| Field | Type | Notes |
|---|---|---|
session_id | string | Current Codex session id. Subagent hooks get the parent session id |
transcript_path | string or null | Path to the session transcript. Its format is not a stable interface |
cwd | string | Working directory for the session |
hook_event_name | string | Current hook event name |
model | string | Codex-specific. Active model slug |
Turn-scoped events also get turn_id (string). SessionStart, PreToolUse, PermissionRequest, PostToolUse, UserPromptSubmit, SubagentStart, SubagentStop, Stop and Interrupt also get permission_mode: default, acceptEdits, plan, dontAsk or bypassPermissions.
Codex: output, exit codes and timeouts
- Exit 0 with no output: success, and Codex continues.
- Common JSON fields:
continue,stopReason,systemMessageandsuppressOutputon SessionStart, PreCompact, PostCompact, UserPromptSubmit, SubagentStop and Stop.suppressOutputis parsed but not implemented. - Exit 2: on PreToolUse, PostToolUse, UserPromptSubmit, SubagentStop and Stop, exit 2 with the reason on stderr works like the event's block decision. The documentation gives no exit 2 behaviour for the other events.
- Unsupported fields: on PreToolUse,
continue,stopReason,suppressOutput,permissionDecision: "ask"and the legacydecision: "approve"mark the hook run as failed, and the tool call continues. - Matchers: a
matcheris always a regular expression."*",""or no matcher match every occurrence. - Handlers:
commandandmcp_toolrun.promptandagenthandlers are parsed but skipped. - Timeouts:
timeoutis in seconds. The default is 600 seconds, also formcp_tool. SessionEnd and Interrupt default to 1 second and allow at most 3. - Background hooks:
"async": trueruns a command hook in the background, up to eight at once per session. A background hook cannot block, approve or rewrite anything. SessionEnd hooks always run synchronously. - Large output:
additionalContextover about 2,500 tokens is saved to a file and the model gets a preview.additionalContextLimiton a handler changes the threshold.
Codex: event reference
SessionStart (Codex)
Fires when a session starts. The matcher filters source: startup, resume, clear, compact or fork. After a compaction, hooks that match compact run before the next model request.
- Fields:
source(string) andpermission_mode. - Stdout: plain text is added as developer context. JSON: common fields and
hookSpecificOutput.additionalContext.
{
"session_id": "thr_123",
"transcript_path": "/workspace/.codex/rollout.jsonl",
"cwd": "/workspace",
"hook_event_name": "SessionStart",
"model": "<model slug>",
"permission_mode": "default",
"source": "startup"
}UserPromptSubmit (Codex)
Fires before a prompt is sent. Any matcher is ignored.
- Fields:
turn_id(string),prompt(string) andpermission_mode. - Stdout: plain text is added as developer context. JSON:
decision: "block"withreasonblocks the prompt, andhookSpecificOutput.additionalContextadds context. - Exit 2: blocks the prompt, with the reason on stderr.
PreToolUse (Codex)
Fires before Bash, apply_patch file edits, MCP tool calls and other local function tools. Hosted tools such as web search do not use this path. The matcher filters tool_name. exec_command matches as Bash, apply_patch also matches Edit and Write, and spawn_agent also matches Agent.
- Fields:
turn_id,tool_name(string, such asBash,apply_patchormcp__fs__read),tool_use_id(string),tool_input(JSON:commandfor Bash andapply_patch, the arguments for other tools) andpermission_mode. - Stdout: plain text is ignored. JSON:
hookSpecificOutput.permissionDecision: "deny"withpermissionDecisionReasonblocks.permissionDecision: "allow"withupdatedInputrewrites the call.additionalContextadds context. The olderdecision: "block"withreasonstill blocks. - Exit 2: blocks, with the reason on stderr.
{
"session_id": "thr_123",
"transcript_path": "/workspace/.codex/rollout.jsonl",
"cwd": "/workspace",
"hook_event_name": "PreToolUse",
"model": "<model slug>",
"permission_mode": "default",
"turn_id": "<turn id>",
"tool_name": "Bash",
"tool_use_id": "<call id>",
"tool_input": {
"command": "npm test"
}
}PermissionRequest (Codex)
Fires when Codex is about to ask for approval, such as a shell escalation or a managed network approval. It does not run for commands that need no approval. The matcher filters tool_name.
- Fields:
turn_id,tool_name,tool_inputandpermission_mode.tool_input.description(string or null) holds a readable reason when Codex has one. - Stdout: plain text is ignored. JSON
hookSpecificOutput.decision.behavior:allow, ordenywithmessage. Anydenywins over anallow. With no decision, the normal approval prompt appears.updatedInput,updatedPermissionsandinterruptare reserved and fail closed.
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "deny",
"message": "Blocked by repository policy."
}
}
}PostToolUse (Codex)
Fires after a supported tool produces output. For Bash it also fires after a command that exits non-zero. Codex has no separate failure event.
- Fields:
turn_id,tool_name,tool_use_id,tool_input,tool_response(JSON, the MCP call result or the tool's model-facing output) andpermission_mode. - Stdout: plain text is ignored. JSON:
decision: "block"withreasonreplaces the tool result with your feedback.continue: falsewithstopReasonstops normal processing of the result.hookSpecificOutput.additionalContextadds context. - Exit 2: feedback on stderr, as with
decision: "block". The command already ran.
PreModelRequest and PostModelRequest (Codex)
Fire before each model request attempt and after it, retries included. No matcher support. Output is diagnostic only and cannot change context or block a request.
- Fields: the model,
turn_id, andagent_idandagent_typewhen available. - PostModelRequest adds:
status(completed,failedorcancelled),error(ornull) andtoken_usage(ornull).
PreCompact and PostCompact (Codex)
Fire before and after Codex compacts the chat. The matcher filters trigger: manual or auto.
- Fields:
turn_idandtrigger(string). - Stdout: plain text is ignored. JSON: common fields.
continue: falseon PreCompact stops before compacting, and on PostCompact stops after it.
SubagentStart (Codex)
Fires when a subagent starts. The matcher filters agent_type.
- Fields:
turn_id,agent_id(string),agent_type(string) andpermission_mode. - Stdout: plain text and
hookSpecificOutput.additionalContextare added as developer context for the subagent.continue: falsedoes not stop it.
SubagentStop (Codex)
Fires when a subagent stops. The matcher filters agent_type.
- Fields:
turn_id,agent_id,agent_type,agent_transcript_path(string or null),stop_hook_active(boolean),last_assistant_message(string or null) andpermission_mode. - Stdout: must be JSON on exit 0. Plain text is invalid.
decision: "block"withreasoncontinues the subagent. Acontinue: falsefrom any matching hook wins. - Exit 2: continues the subagent, with the reason on stderr.
Stop (Codex)
Fires when a turn stops. Any matcher is ignored.
- Fields:
turn_id,stop_hook_active(boolean),last_assistant_message(string or null) andpermission_mode. - Stdout: must be JSON on exit 0. Plain text is invalid.
decision: "block"withreasonmakes Codex continue, withreasonsent as a new user prompt. Acontinue: falsefrom any matching hook wins. - Exit 2: continues the turn, with the reason on stderr.
{
"session_id": "thr_123",
"transcript_path": "/workspace/.codex/rollout.jsonl",
"cwd": "/workspace",
"hook_event_name": "Stop",
"model": "<model slug>",
"permission_mode": "default",
"turn_id": "<turn id>",
"stop_hook_active": false,
"last_assistant_message": "All tests pass."
}Interrupt (Codex)
Fires when you interrupt an active turn on the main thread. It does not run for idle threads or subagents. Any matcher is ignored.
- Fields:
turn_id(the interrupted turn) andpermission_mode. - Stdout: exit 0 with no output, or JSON with an optional
systemMessage. Plain text is invalid. A hook cannot prevent the interruption. - Timeout: 1 second by default, configurable from 1 to 3 seconds, also in the background.
SessionEnd (Codex)
Fires for the main thread when you archive or delete an open conversation, when Codex closes normally, or after 30 minutes idle with no connected client. It does not run for subagents. The matcher filters reason, which is always other for now.
- Fields:
reason(string). - Stdout: advisory. Output does not steer Codex. A timeout or error is reported as a hook failure.
- Timeout: 1 second by default, at most 3. Always synchronous, even with
async: true. MCP tool hooks are not supported here.
{
"session_id": "thr_123",
"transcript_path": "/workspace/.codex/rollout.jsonl",
"cwd": "/workspace",
"hook_event_name": "SessionEnd",
"reason": "other"
}Claude Code and Codex side by side
| Topic | Claude Code | Codex |
|---|---|---|
| Config files | settings.json at user, project and local level, plugins, skills, agents | hooks.json or [hooks] in config.toml, user and project, plugins |
| Matcher | Exact names or lists, regex only when other characters appear | Always a regex |
| Tool failure | Separate PostToolUseFailure event | PostToolUse also fires after a non-zero Bash exit |
PreToolUse ask | Supported | Marks the hook run as failed, the call continues |
| Permission modes | Includes auto | default, acceptEdits, plan, dontAsk, bypassPermissions |
| Model in the payload | Only on SessionStart, not always | model on every event |
| Subagent hooks | agent_id and agent_type on the event | Parent session_id, plus agent_id on subagent events |
| Default timeout | 600 s, with shorter defaults per event | 600 s, SessionEnd and Interrupt 1 s (max 3) |
| Handler types | command, http, mcp_tool, prompt, agent | command, mcp_tool (others skipped) |
Watch Claude Code and Codex sessions with Orbtile
Orbtile shows every Claude Code, Codex, ZCode, Antigravity and Kimi Code session live on an Android phone, an iPhone (beta) or a Logitech MX Creative Keypad, with the waiting sessions first.
- Setup: Orbtile installs the hooks for you and backs up the files it changes.
- Local only: local hook events go over a private Unix socket on the Mac and a private named pipe on Windows.
- Platforms: Mac and Windows, with a 15-day trial.
FAQ
Which hook event tells me a Claude Code session is waiting for permission?
PermissionRequest fires as soon as Claude Code is about to ask. The Notification event with the permission_prompt type fires only after the prompt has waited about six seconds.
Why does my hook's JSON output have no effect?
Claude Code reads stdout as JSON only when it is one JSON object with nothing else around it, and Codex Stop and SubagentStop hooks must print JSON on exit 0. A line printed by a shell profile, or a second JSON line, turns the output into plain text or a parse error.
Does exit code 1 block a tool call?
No. In Claude Code, exit 1 without valid JSON is a non-blocking error and the action goes ahead. Exit 2 blocks on events that can block, or set onFailure to block on the hook.
How long can a SessionEnd hook run?
In Claude Code, all SessionEnd hooks share a 1.5-second budget unless you raise it, up to 60 seconds. In Codex, SessionEnd hooks default to 1 second and allow at most 3.