Guide

Claude Code and Codex hooks: payload reference

Updated

On this page

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.

Logitech MX Creative Keypad running Orbtile in Focus mode for a Claude Code permission request, with Deny on key 7 and Allow on key 9

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

FieldTypeNotes
session_idstringCurrent session identifier
prompt_idstringUUID of the prompt being processed. Absent until the first user input
transcript_pathstringPath to the conversation JSON. Written asynchronously, so it can lag the current turn. On Stop and SubagentStop, read last_assistant_message instead
cwdstringWorking directory when the hook runs
scratchpad_dirstringThe session's scratchpad directory. Absent when there is none. Claude Code v2.1.257 or later
permission_modestringdefault, plan, acceptEdits, auto, dontAsk or bypassPermissions. Manual mode arrives as default. Not every event receives it
effortobjectlevel is low, medium, high, xhigh or max. On PreToolUse, PostToolUse, Stop, SubagentStop and other events with a tool-use context
hook_event_namestringName of the event that fired
agent_idstringPresent when the hook fires in a subagent or in-process teammate
agent_typestringAgent 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:

StdoutExit 0Exit 2Any other exit code
One JSON object that passes validationSuccess, the fields applyBlocking error. The fields are read but cannot override the blockSuccess. The exit code is ignored and the fields decide
JSON that cannot be parsed or fails validationNon-blocking errorBlocking error, stderr is the reasonNon-blocking error
Plain text, or nothingSuccessBlocking error, stderr is the reasonNon-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:

FieldDefaultEffect
continuetruefalse stops Claude after the hook. Takes precedence over event-specific decisions
stopReasonnoneShown to the user when continue is false
suppressOutputfalseAccepted, no effect
systemMessagenoneWarning shown to the user
terminalSequencenoneA 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 eventDefault timeout
command, http, mcp_tool600 seconds
prompt30 seconds
agent60 seconds
UserPromptSubmit, PreModelSwitch, PostModelSwitch (command, http, mcp_tool)30 seconds
MessageDisplay (command, http, mcp_tool)10 seconds
SessionEnd1.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). Optional model (string, check before reading), agent_type (string, with claude --agent) and session_title (string, when a custom title is set). On resume or fork with at least one earlier response (v2.1.251 or later): seconds_since_last_response, context_tokens, prompt_cache_likely_expired and estimated_cache_write_usd.
  • Stdout: plain text is added to Claude's context. JSON can set hookSpecificOutput.additionalContext, initialUserMessage, watchPaths, sessionTitle and reloadSkills.
  • 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) and session_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" with reason (shown to the user, not to Claude), and in hookSpecificOutput: additionalContext, sessionTitle and suppressOriginalPrompt. 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) and tool_use_id (string). MCP tools also get mcp_server, an object with name and source (v2.1.274 or later).
  • Bash tool_input: command, description, timeout and run_in_background.
  • File paths: for Write, Edit and Read, tool_input.file_path is 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, ask or defer), permissionDecisionReason, updatedInput (replaces the whole input object) and additionalContext. When hooks disagree, deny beats defer, which beats ask, which beats allow. defer needs non-interactive mode (-p). The old top-level decision values approve and block still map to allow and deny.
  • 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) and tool_input (object), but no tool_use_id. MCP tools also get mcp_server. Optional permission_suggestions (array of permission update entries such as addRules or setMode).
  • Stdout: JSON in hookSpecificOutput.decision: behavior (allow or deny). With allow: updatedInput and updatedPermissions. With deny: message and interrupt. Deny and ask rules still apply after an allow.
  • Exit 2: not honoured. The permission flow continues unchanged and stderr is discarded. Use the decision object.
{
  "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_id and optional duration_ms (number, excludes permission prompts and PreToolUse hooks). MCP tools also get mcp_server.
  • Stdout: JSON: decision: "block" with reason adds the reason next to the result. In hookSpecificOutput: additionalContext, updatedToolOutput (replaces the result) and classifierContext (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), optional is_interrupt (boolean) and optional duration_ms. For Bash and PowerShell, the first line of error is Exit 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 has tool_name, tool_input, tool_use_id and tool_response. Here tool_response is the serialized content the model sees, not the structured object PostToolUse gets.
  • Stdout: JSON: decision: "block" with reason, and additionalContext.
  • 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_id and reason (string, for a classifier verdict usually the rule in square brackets). MCP tools also get mcp_server.
  • Stdout: JSON hookSpecificOutput.retry: true tells 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), optional title (string) and notification_type (string).
  • Stdout: no decision control. systemMessage and continue are discarded. terminalSequence still 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, true on the last batch) and delta (string, the new lines). Use final, not an empty delta, as the end of the message.
  • Stdout: JSON hookSpecificOutput.displayContent replaces 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) and agent_type (string).
  • Stdout: JSON hookSpecificOutput.additionalContext is 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's background_tasks and session_crons arrays.
  • Stdout: same as Stop. decision: "block" with reason keeps the subagent running and gives it reason as 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, true when Claude is already continuing because of a Stop hook), last_assistant_message (string), background_tasks (array of id, type, status, description, plus command, agent_type, server, tool or name depending on the type) and session_crons (array of id, schedule, recurring, prompt). The two arrays tell "done" apart from "waiting for background work".
  • Stdout: JSON: decision: "block" with a required reason keeps Claude working. hookSpecificOutput.additionalContext also 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_CAP changes the cap. Check stop_hook_active before 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), optional error_details and optional last_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) and custom_instructions (string, or null for auto and for /compact with no text).
  • PostCompact fields: trigger and compact_summary (string).
  • PreCompact stdout: on exit 0 without a block, stdout becomes instructions for the compaction summary. decision: "block" blocks compaction. systemMessage and continue are 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 optional mode, url, elicitation_id and requested_schema.
  • Stdout: JSON hookSpecificOutput with action (accept, decline or cancel) and content (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 systemMessage are 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) or CLAUDE_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

EventWhen it firesMatcherExit code 2
SetupOnly with --init-only, or --init or --maintenance with -pinit, maintenanceIgnored
InstructionsLoadedWhen a CLAUDE.md or .claude/rules/*.md file is loaded into contextsession_start, nested_traversal, path_glob_match, include, compactIgnored
UserPromptExpansionWhen a typed command expands into a promptCommand or skill nameBlocks the expansion
TaskCreatedWhen a task is created with TaskCreateNoneRolls back the task creation
TaskCompletedWhen a task is marked completedNonePrevents the completion
TeammateIdleWhen an agent team teammate is about to go idleNoneKeeps the teammate working
ConfigChangeWhen a configuration file changes during a sessionuser_settings, project_settings, local_settings, policy_settings, skillsBlocks the change, except policy_settings
CwdChangedWhen a shell command changes the working directoryNonestderr shown to the user
DirectoryAddedAfter /add-dir or an SDK register_repo_root requestslash_command, register_repo_rootstderr goes to the debug log
FileChangedWhen a watched file changes on disk, whatever changed itLiteral file namesstderr shown to the user
WorktreeCreateWhen Claude Code creates a worktreeNoneAny non-zero exit fails the creation
WorktreeRemoveWhen Claude Code removes a worktree a WorktreeCreate hook createdNoneAny non-zero exit fails the removal if the directory remains
PreModelSwitchBefore a requested model switchCanonical model nameBlocks the switch, stderr shown to the user
PostModelSwitchAfter the session's model changesCanonical model namestderr shown to the user
ElicitationResultAfter a user answers an MCP elicitationMCP server nameBlocks 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.

FieldTypeNotes
session_idstringCurrent Codex session id. Subagent hooks get the parent session id
transcript_pathstring or nullPath to the session transcript. Its format is not a stable interface
cwdstringWorking directory for the session
hook_event_namestringCurrent hook event name
modelstringCodex-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, systemMessage and suppressOutput on SessionStart, PreCompact, PostCompact, UserPromptSubmit, SubagentStop and Stop. suppressOutput is 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 legacy decision: "approve" mark the hook run as failed, and the tool call continues.
  • Matchers: a matcher is always a regular expression. "*", "" or no matcher match every occurrence.
  • Handlers: command and mcp_tool run. prompt and agent handlers are parsed but skipped.
  • Timeouts: timeout is in seconds. The default is 600 seconds, also for mcp_tool. SessionEnd and Interrupt default to 1 second and allow at most 3.
  • Background hooks: "async": true runs 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: additionalContext over about 2,500 tokens is saved to a file and the model gets a preview. additionalContextLimit on 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) and permission_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) and permission_mode.
  • Stdout: plain text is added as developer context. JSON: decision: "block" with reason blocks the prompt, and hookSpecificOutput.additionalContext adds 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 as Bash, apply_patch or mcp__fs__read), tool_use_id (string), tool_input (JSON: command for Bash and apply_patch, the arguments for other tools) and permission_mode.
  • Stdout: plain text is ignored. JSON: hookSpecificOutput.permissionDecision: "deny" with permissionDecisionReason blocks. permissionDecision: "allow" with updatedInput rewrites the call. additionalContext adds context. The older decision: "block" with reason still 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_input and permission_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, or deny with message. Any deny wins over an allow. With no decision, the normal approval prompt appears. updatedInput, updatedPermissions and interrupt are 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) and permission_mode.
  • Stdout: plain text is ignored. JSON: decision: "block" with reason replaces the tool result with your feedback. continue: false with stopReason stops normal processing of the result. hookSpecificOutput.additionalContext adds 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, and agent_id and agent_type when available.
  • PostModelRequest adds: status (completed, failed or cancelled), error (or null) and token_usage (or null).

PreCompact and PostCompact (Codex)

Fire before and after Codex compacts the chat. The matcher filters trigger: manual or auto.

  • Fields: turn_id and trigger (string).
  • Stdout: plain text is ignored. JSON: common fields. continue: false on 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) and permission_mode.
  • Stdout: plain text and hookSpecificOutput.additionalContext are added as developer context for the subagent. continue: false does 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) and permission_mode.
  • Stdout: must be JSON on exit 0. Plain text is invalid. decision: "block" with reason continues the subagent. A continue: false from 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) and permission_mode.
  • Stdout: must be JSON on exit 0. Plain text is invalid. decision: "block" with reason makes Codex continue, with reason sent as a new user prompt. A continue: false from 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) and permission_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

TopicClaude CodeCodex
Config filessettings.json at user, project and local level, plugins, skills, agentshooks.json or [hooks] in config.toml, user and project, plugins
MatcherExact names or lists, regex only when other characters appearAlways a regex
Tool failureSeparate PostToolUseFailure eventPostToolUse also fires after a non-zero Bash exit
PreToolUse askSupportedMarks the hook run as failed, the call continues
Permission modesIncludes autodefault, acceptEdits, plan, dontAsk, bypassPermissions
Model in the payloadOnly on SessionStart, not alwaysmodel on every event
Subagent hooksagent_id and agent_type on the eventParent session_id, plus agent_id on subagent events
Default timeout600 s, with shorter defaults per event600 s, SessionEnd and Interrupt 1 s (max 3)
Handler typescommand, http, mcp_tool, prompt, agentcommand, 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.