Skip to content

Claude Code Hooks: Events & JSON

Last updated:

Complete reference for all 33 Claude Code hook events: matcher fields, input schemas, decision control, and timeout defaults. Source: official Anthropic documentation (hooks reference), checked against Claude Code v2.1.284.

For the audit skill, see examples/skills/eval-hooks/SKILL.md.

See also: Event-Driven Agent Automation for building hook-driven workflows, and Security Hardening: Startup Hooks for the risks of executing code via hooks.


EventFires whenMatcher fieldCan block?Default timeout
SessionStartSession begins or resumessourceNo600s
Setup--init-only or -p --init/--maintenancetriggerNo600s
UserPromptSubmitUser submits a promptnoneYes30s
UserPromptExpansionSlash command expands to promptcommand_nameYes600s
PreToolUseBefore tool call executestool_nameYes600s
PermissionRequestTool call needs a permission decisiontool_nameNot by exit 2; JSON decision object only600s
PermissionDeniedAuto-mode classifier denies a calltool_nameNo (retry only)600s
PostToolUseAfter tool call succeedstool_nameNo (stderr to Claude)600s
PostToolUseFailureAfter tool call failstool_nameNo600s
PostToolBatchAfter full parallel batch resolvesnoneYes (stops loop)600s
NotificationClaude sends a notificationnotification_typeNo600s
MessageDisplayAssistant message text streamsnoneNo10s
SubagentStartSubagent spawned via Agent toolagent_typeNo600s
SubagentStopSubagent finishesagent_typeYes600s
TaskCreatedTask being created via TaskCreatenoneYes600s
TaskCompletedTask being marked as completednoneYes600s
StopClaude finishes respondingnoneYes (continues turn)600s
StopFailureTurn ends due to API errorerrorNo (ignored)600s
TeammateIdleAgent team teammate goes idlenoneYes600s
InstructionsLoadedCLAUDE.md or rules file loadedload_reasonNo600s
ConfigChangeConfig file changes during sessionsourceYes (not policy_settings)600s
CwdChangedWorking directory changesnoneNo600s
DirectoryAddedDirectory added via /add-dir or SDK register_repo_rootsourceNo600s
FileChangedWatched file changes on diskfilename (literal)No600s
WorktreeCreateWorktree being creatednoneYes (any non-zero)600s
WorktreeRemoveWorktree being removednoneYes (any non-zero fails removal if the directory remains)600s
PreCompactBefore context compactiontriggerYes600s
PostCompactAfter compaction completestriggerNo600s
PreModelSwitchBefore a requested model switch is appliedcanonical target modelYes30s
PostModelSwitchAfter the session model changes, including automatic changescanonical target modelNo30s
ElicitationMCP server requests user inputmcp_server_nameYes600s
ElicitationResultUser responds to MCP elicitationmcp_server_nameYes600s
SessionEndSession terminatesreasonNo1.5s budget

Timeout exceptions: the command, http, and mcp_tool default of 600s drops to 30s on UserPromptSubmit, PreModelSwitch, and PostModelSwitch, and to 10s on MessageDisplay. prompt hooks default to 30s. agent hooks default to 60s. SessionEnd has a 1.5s total budget; a per-hook timeout raises it to the highest configured value, up to 60s. Timeouts on plugin hooks do not raise the budget. A timed-out command, http, or mcp_tool hook on PreToolUse does not block the tool call; on PreModelSwitch it blocks the switch.


The matcher field filters on a different field depending on the event type.

Events: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied

Values: Bash, Edit, Write, Read, Glob, Grep, Agent, WebFetch, WebSearch, AskUserQuestion, ExitPlanMode, and MCP tools as mcp__<server>__<tool>.

Matching rules (all events with matcher support):

  • Only letters, digits, _, -, spaces, ,, and |: exact string, or a list of exact strings separated by | or , (Edit|Write, Edit, Write). Hyphens in the exact set require v2.1.195 or later.
  • Contains any other character: unanchored JavaScript regex (mcp__memory__.*). Edit.* also matches NotebookEdit; anchor with ^...$ for a whole-string match.
  • "*", "", or absent: matches all.
  • FileChanged and StopFailure use a narrower exact set (letters, digits, _, | only); a hyphen, space, or comma there keeps the matcher on the regex path.

To match every tool from an MCP server: mcp__memory__.* (the .* is required; mcp__memory without it is an exact string and matches no tool).

ValueWhen
startupNew session
resume--resume, --continue, or /resume
clear/clear
compactAuto or manual compaction
forkForked session
ValueWhen
initclaude --init-only or claude -p --init
maintenanceclaude -p --maintenance
ValueWhen
clear/clear command
resumeInteractive /resume switch
logoutUser logged out
prompt_input_exitExited while prompt input was visible
otherOther exit reasons

bypass_permissions_disabled was removed in v2.1.234; drop it from SessionEnd matchers.

Notification: matcher filters on notification_type

Section titled “Notification: matcher filters on notification_type”

Values: permission_prompt, idle_prompt, auth_success, elicitation_dialog, elicitation_url_dialog, elicitation_complete, elicitation_response, agent_needs_input, agent_completed, quota_auto_resume_fired, quota_auto_resume_stale, quota_auto_resume_disabled

SubagentStart / SubagentStop: matcher filters on agent_type

Section titled “SubagentStart / SubagentStop: matcher filters on agent_type”

Values: general-purpose, Explore, Plan, or custom agent names (the name field from the agent’s frontmatter, not the filename).

PreCompact / PostCompact: matcher filters on trigger

Section titled “PreCompact / PostCompact: matcher filters on trigger”

Values: manual (from /compact), auto (automatic)

InstructionsLoaded: matcher filters on load_reason

Section titled “InstructionsLoaded: matcher filters on load_reason”

Values: session_start, nested_traversal, path_glob_match, include, compact

Values: user_settings, project_settings, local_settings, policy_settings, skills

Values: 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 (v2.1.267+), unknown

UserPromptExpansion: matcher filters on command_name

Section titled “UserPromptExpansion: matcher filters on command_name”

Your skill or command name as typed by the user (without the leading /).

Elicitation / ElicitationResult: matcher filters on mcp_server_name

Section titled “Elicitation / ElicitationResult: matcher filters on mcp_server_name”

Your configured MCP server name.

Values: slash_command (/add-dir), register_repo_root (SDK control request)

PreModelSwitch / PostModelSwitch: matcher filters on the canonical target model

Section titled “PreModelSwitch / PostModelSwitch: matcher filters on the canonical target model”

The matcher is compared with the canonical name derived from to_model, ignoring any [1m] suffix: claude-opus-5, claude-opus-4-6|claude-opus-5, .*opus.*. When Claude Code cannot derive a canonical name (for example a gateway-only custom ID), every PreModelSwitch hook runs regardless of matcher, so a blocking hook should also check to_model.

Split on |, each segment is registered as a literal filename watched in the current directory. Example: ".envrc|.env". Unlike other events, regex patterns are not applied here. The same value builds the watch list AND filters which hooks run.

UserPromptSubmit, PostToolBatch, Stop, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, CwdChanged, MessageDisplay

Adding a matcher field to these events is silently ignored.


Exit code 2 is the only exit code that blocks through the code alone. Claude Code reads JSON output fields from stdout on every exit code, not only 0: for events that use the standard decision model, a valid JSON decision (for example permissionDecision: "deny" or decision: "block") takes effect with exit 0 or any other non-2 code, and exit 2 still blocks even if the JSON says allow. Exit code 1 without valid JSON is a non-blocking error: the action proceeds and the first line of stderr appears in the transcript. WorktreeCreate and WorktreeRemove are the exceptions: any non-zero code fails them.

EventWhat happens on exit 2
PreToolUseBlocks the tool call; stderr fed to Claude
PermissionRequestNot honored: the permission flow proceeds unchanged and stderr is discarded. Deny through the JSON decision object
UserPromptSubmitBlocks prompt and erases it from context
UserPromptExpansionBlocks the expansion
StopPrevents stopping; continues the turn
SubagentStopPrevents subagent from stopping
TeammateIdleTeammate continues working; stderr fed back
TaskCreatedRolls back task creation; stderr fed back
TaskCompletedPrevents completion; stderr fed back
ConfigChangeBlocks config change (not policy_settings)
PostToolBatchStops agentic loop before next model call
PreCompactBlocks compaction
ElicitationDenies the elicitation
ElicitationResultBlocks response (effective action becomes decline)
WorktreeCreateAny non-zero exit code fails creation
WorktreeRemoveAny non-zero exit code fails removal if the directory still exists afterward
PreModelSwitchBlocks the model switch and shows stderr to the user
PostModelSwitchShows stderr to user only; the model already switched
PostToolUseShows stderr to Claude (tool already ran)
PostToolUseFailureShows stderr to Claude
StopFailureIgnored entirely (output and exit code ignored)
SessionEndShows stderr to user only
SessionStart, Setup, SubagentStartShows stderr to user only
NotificationShows stderr to user only
InstructionsLoadedExit code ignored
PermissionDeniedExit code and stderr ignored
CwdChanged, FileChangedShows stderr to user only
DirectoryAddedStderr goes to the debug log; the directory is already added
PostCompactShows stderr to user only
MessageDisplayOriginal text displayed unchanged

Used by: UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, ConfigChange, PreCompact

{ "decision": "block", "reason": "Explanation" }

Only "block" is valid. Omit decision to allow. For Stop and SubagentStop, the reason becomes Claude’s next instruction.

Uses hookSpecificOutput for richer control. Precedence when hooks conflict: deny > defer > ask > allow.

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow | deny | ask | defer",
"permissionDecisionReason": "Shown to user (allow/ask) or to Claude (deny)",
"updatedInput": { "field": "new value" },
"additionalContext": "Context injected next to tool result"
}
}

defer only works in -p (non-interactive) mode. Process exits with stop_reason: "tool_deferred" and the calling process can resume later. defer does not work when Claude makes several tool calls at once.

Exit code 2 or top-level decision: "block" cancels the switch. For finer control use hookSpecificOutput.permissionDecision with allow, deny, or ask (no defer, updatedInput, or additionalContext).

Exit code 2 or top-level decision: "block" cancels the task creation. continue: false is ignored.

A hook that exits 2 without a decision object leaves the permission flow unchanged. Only the decision object grants or denies. agent hooks are skipped on this event. In sessions that cannot show a prompt, such as background subagents in -p mode, these hooks still run, and the call is denied if no hook decides.

{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow | deny",
"updatedInput": {},
"updatedPermissions": [],
"message": "Reason for deny",
"interrupt": false
}
}
}

Exit code and stderr are ignored. To signal retry:

{
"hookSpecificOutput": {
"hookEventName": "PermissionDenied",
"retry": true
}
}
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Context injected next to tool result",
"updatedToolOutput": { "stdout": "...", "stderr": "", "interrupted": false, "isImage": false }
}
}

updatedToolOutput changes what Claude sees, not what already executed. Must match the tool’s output shape exactly. Built-in tools with incorrect shapes fall back to the original.

Command hooks print the absolute path on stdout. HTTP hooks return:

{ "hookSpecificOutput": { "hookEventName": "WorktreeCreate", "worktreePath": "/abs/path" } }

Any failure or missing path fails worktree creation.

{
"hookSpecificOutput": {
"hookEventName": "MessageDisplay",
"displayContent": "Replacement text shown on screen"
}
}

Changes only what appears on screen. Claude and the transcript keep the original.

{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Context before first prompt",
"sessionTitle": "branch-or-feature-name",
"watchPaths": ["/absolute/path/to/watch"],
"reloadSkills": true,
"initialUserMessage": "First turn in -p mode"
}
}

reloadSkills: true rescans skill directories after hooks complete, so skills installed by the hook are available immediately in the same session.

{
"hookSpecificOutput": {
"hookEventName": "Elicitation",
"action": "accept | decline | cancel",
"content": { "field_name": "value" }
}
}

Same format as Elicitation output. Overrides what the user submitted.

{
"continue": false,
"stopReason": "Message shown to user (not Claude)",
"suppressOutput": true,
"systemMessage": "Warning shown to user",
"terminalSequence": "\033]777;notify;Title;Body\007"
}

continue: false stops Claude entirely, takes precedence over all decision fields.

terminalSequence: desktop notifications and window titles via OSC sequences. Restricted to OSC 0/1/2/9/99/777 and BEL. Writing to /dev/tty directly fails since v2.1.139 because hooks run without a controlling terminal.


All events receive: session_id, transcript_path, cwd, hook_event_name, and usually permission_mode. Subagent hooks also receive agent_id and agent_type.

EventEvent-specific extra input fields
SessionStartsource, model, optionally agent_type, session_title
Setuptrigger ("init" or "maintenance")
UserPromptSubmitprompt
UserPromptExpansionexpansion_type, command_name, command_args, command_source, prompt
PreToolUsetool_name, tool_input, tool_use_id
PermissionRequesttool_name, tool_input, permission_suggestions
PermissionDeniedtool_name, tool_input, tool_use_id, reason
PostToolUsetool_name, tool_input, tool_response, tool_use_id, duration_ms
PostToolUseFailuretool_name, tool_input, tool_use_id, error, is_interrupt, duration_ms
PostToolBatchtool_calls (array with tool_name, tool_input, tool_use_id, tool_response)
Notificationmessage, title, notification_type
MessageDisplayturn_id, message_id, index, final, delta
SubagentStartagent_id, agent_type
SubagentStopstop_hook_active, agent_id, agent_type, agent_transcript_path, last_assistant_message
TaskCreatedtask_id, task_subject, task_description, teammate_name, team_name
TaskCompletedtask_id, task_subject, task_description, teammate_name, team_name
Stopstop_hook_active, last_assistant_message, background_tasks, session_crons
StopFailureerror, error_details, last_assistant_message
TeammateIdleteammate_name, team_name
InstructionsLoadedfile_path, memory_type, load_reason, globs, trigger_file_path, parent_file_path
ConfigChangesource, file_path
CwdChangedold_cwd, new_cwd
DirectoryAddeddirectory, source
FileChangedfile_path, event ("change", "add", "unlink")
WorktreeCreatename (slug for the new worktree)
WorktreeRemoveworktree_path
PreCompacttrigger, custom_instructions
PostCompacttrigger, compact_summary
PreModelSwitchfrom_model, to_model, requested_model, source, context_tokens, prompt_cache_warm, cache_ttl, estimated_cache_write_usd, pricing
PostModelSwitchSame as PreModelSwitch; source adds auto and resume
Elicitationmcp_server_name, message, mode, url, elicitation_id, requested_schema
ElicitationResultmcp_server_name, action, mode, elicitation_id, content
SessionEndreason

FieldDescription
typecommand, http, mcp_tool, prompt, or agent
ifPermission rule syntax to narrow the handler. Tool events only (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied). On any other event, a hook with if set never runs
timeoutSeconds before canceling. Not enforced on a running async: true command hook; still enforced with asyncRewake
statusMessageCustom spinner message while hook runs
onceRun once per session then remove. Honored only in skill frontmatter, ignored in settings files
FieldDescription
commandShell command or executable path
argsArgument vector: triggers exec form (no shell involved)
asynctrue to run in background without blocking
asyncRewakeLike async: true but wakes Claude on exit 2, showing stderr (or stdout if stderr is empty) as a system reminder
shell"bash" (default) or "powershell" (Windows)

Shell form (args absent): command passed to sh -c. Supports pipes, &&, globs. Exec form (args present): command is the executable, each args element is one verbatim argument. Use for paths with spaces or when referencing ${CLAUDE_PROJECT_DIR}.

FieldDescription
urlPOST endpoint URL
headersAdditional headers (supports $VAR interpolation)
allowedEnvVarsEnv vars allowed to be interpolated in header values

Non-2xx responses are non-blocking. To block, return 2xx with decision: "block" in the JSON body.

FieldDescription
serverConnected MCP server name
toolTool name on that server
inputTool arguments. String values support ${path} substitution from hook input

Server must already be connected. SessionStart and Setup typically fire before servers finish connecting.

FieldDescription
promptPrompt text. Use $ARGUMENTS for the hook’s JSON input
modelModel override (default: fast model, typically Haiku)
continueOnBlockWhen ok: false, feed the reason back to Claude and continue instead of stopping

Returns { "ok": true/false, "reason": "..." }. See Hook type support per event for the events that accept prompt hooks.

FieldDescription
promptTask description. Use $ARGUMENTS for the hook’s JSON input
modelModel override

Spawns a subagent that can use Read, Grep, Glob (up to 50 turns), then returns the same { "ok": true/false } schema. Experimental.

Event groupSupported handler types
PermissionDenied, PostToolBatch, PostToolUse, PostToolUseFailure, PreToolUse, Stop, SubagentStop, TaskCompleted, TaskCreated, TeammateIdle, UserPromptExpansion, UserPromptSubmitcommand, http, mcp_tool, prompt, agent
PermissionRequestcommand, http, mcp_tool, prompt (an agent hook is skipped)
ConfigChange, CwdChanged, DirectoryAdded, Elicitation, ElicitationResult, FileChanged, InstructionsLoaded, MessageDisplay, Notification, PostCompact, PostModelSwitch, PreCompact, PreModelSwitch, SessionEnd, StopFailure, SubagentStart, WorktreeCreate, WorktreeRemovecommand, http, mcp_tool
SessionStart, Setupcommand, mcp_tool

On PermissionDenied, prompt and agent hooks run but their output is discarded.


PlaceholderResolves to
${CLAUDE_PROJECT_DIR}Project root directory
${CLAUDE_PLUGIN_ROOT}Plugin installation directory (changes on update)
${CLAUDE_PLUGIN_DATA}Plugin persistent data directory (survives updates)

Prefer exec form for hooks referencing these: each args element is passed verbatim, no quoting needed for spaces or special chars.


Available in SessionStart, Setup, CwdChanged, and FileChanged hooks. Write export VAR=value lines to this path to persist variables into subsequent Bash commands for the session.

Terminal window
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
fi

Use append (>>) to preserve variables set by other hooks.


Stop hook continuation cap: both decision: "block" and additionalContext go through the stop_hook_active input and the 8-consecutive-continuation cap. Read stop_hook_active from stdin and exit 0 when it is true to let Claude stop cleanly.

Exit 1 does not block: without valid JSON output, only exit 2 blocks a PreToolUse call or UserPromptSubmit. Exit 1 is non-blocking: the action proceeds. A hook can also block with exit 0 plus a JSON decision such as permissionDecision: "deny". A script path that does not exist fails with a code like 127, which is also non-blocking, so a mistyped path silently disables a policy gate.

asyncRewake vs async: asyncRewake: true runs the hook in the background AND wakes the session when the process exits with code 2, even if Claude is idle. Use when a long-running background check needs to report a failure mid-session.

SessionEnd budget: Total budget is 1.5s. Setting timeout: 30 on a hook raises the budget to 30s (max 60s), but a hook without its own timeout keeps the 1.5s default. Plugin hook timeouts do not raise the budget. CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=5000 overrides the budget and, since v2.1.268, also sets the timeout of hooks without their own.

MessageDisplay batching: Fires multiple times per message (once per batch of lines) in interactive mode, once after the full message in -p/Agent SDK mode. final: true marks the last batch; don’t rely on a non-empty delta as the end signal.

WorktreeCreate replaces git entirely: The hook must handle the full worktree setup. .worktreeinclude is not processed. Copy .env and other local files inside the hook.

if field on non-tool events: Adding if to SessionStart, Stop, or any non-tool event silently prevents the hook from running at all.

Multiple PreToolUse hooks with updatedInput: all matching hooks run in parallel. Conflicting decisions resolve as deny > defer > ask > allow, but the documentation does not define which updatedInput wins when several hooks rewrite the same call. Avoid having two hooks on the same matcher both returning updatedInput.

PermissionRequest does not prevent via exit 2: Use hookSpecificOutput.decision.behavior: "deny" in JSON output. Exit 2 is not honored on this event, and agent hooks are skipped on it since v2.1.280.

Duplicate handlers: the same handler defined in several settings files runs once. A plugin’s or a skill’s copy of the same handler stays separate.

Prompt hooks on PermissionDenied: Output is discarded. The only field this event reads is hookSpecificOutput.retry, which prompt and agent hooks cannot set. Use a command hook for retry signals.

Hooks without a controlling terminal: Since v2.1.139, hooks run without /dev/tty. Use terminalSequence in JSON output to emit desktop notifications or window titles instead of writing escape sequences directly.

UserPromptSubmit default timeout: 30s, not 600s. A stuck hook here blocks all user input. Set an explicit timeout if you need more time.

Use the hooks and events recap card