claude-code-hooks
Use when creating, debugging, or modifying Claude Code hooks — event types, handler formats, exit codes, hookSpecificOutput schema, scope precedence
| Model | Source |
|---|---|
| inherit | pack: claude-code-internals |
Tools: Read, Grep, Glob, WebFetch, WebSearch
Full Reference
Claude Code Hooks Reference
Section titled “Claude Code Hooks Reference”Announcement
Section titled “Announcement”┏━ 🔧 claude-code-hooks ━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ your friendly armadillo is here to serve you ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Hooks are user-defined shell commands, HTTP endpoints, LLM prompts, or subagents that execute automatically at specific lifecycle points in Claude Code sessions. This is the authoritative reference — start here before writing, modifying, or debugging any hook.
Sources: code.claude.com/docs/en/hooks · platform.claude.com/docs/en/agent-sdk/hooks (verified 2026-03-01)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Quick Reference: All Hook Events
Section titled “Quick Reference: All Hook Events”| Event | When it fires | Supports Matcher | Can Block? |
|---|---|---|---|
SessionStart | Session begins or resumes | yes (startup/resume/clear/compact) | no |
UserPromptSubmit | User submits a prompt, before Claude processes it | no | yes |
PreToolUse | Before a tool call executes | yes (tool name) | yes |
PermissionRequest | When a permission dialog appears | yes (tool name) | yes |
PostToolUse | After a tool call succeeds | yes (tool name) | no (feedback only) |
PostToolUseFailure | After a tool call fails | yes (tool name) | no (feedback only) |
Notification | When Claude Code sends a notification | yes (notification type) | no |
SubagentStart | When a subagent is spawned | yes (agent type) | no |
SubagentStop | When a subagent finishes | yes (agent type) | yes |
Stop | When Claude finishes responding | no | yes |
TeammateIdle | When an agent team teammate is about to go idle | no | yes |
TaskCompleted | When a task is being marked as completed | no | yes |
ConfigChange | When a configuration file changes mid-session | yes (config source) | yes (except policy_settings) |
WorktreeCreate | When a worktree is being created | no | yes (any non-zero exit fails) |
WorktreeRemove | When a worktree is being removed | no | no |
PreCompact | Before context compaction | yes (manual/auto) | no |
SessionEnd | When a session terminates | yes (exit reason) | no |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Hook Locations and Scope Precedence
Section titled “Hook Locations and Scope Precedence”Where you define a hook determines its scope. Hooks at higher levels take precedence:
| Location | Scope | Shareable | Label in /hooks menu |
|---|---|---|---|
| Managed policy settings | Organization-wide | Admin-controlled | [Managed] |
~/.claude/settings.json | All your projects | No (machine-local) | [User] |
.claude/settings.json | Single project | Yes (committable) | [Project] |
.claude/settings.local.json | Single project | No (gitignored) | [Local] |
Plugin hooks/hooks.json | When plugin is enabled | Yes (bundled) | [Plugin] |
| Skill/agent frontmatter | While component is active | Yes (in component file) | n/a |
Precedence order (highest to lowest): managed → user → project → local → plugin → skill/agent
Security note: Admins can set allowManagedHooksOnly: true to block user, project, and plugin hooks. disableAllHooks: true in user/project/local settings cannot disable managed hooks.
Important: Hook changes during a session don’t take effect immediately. Claude Code snapshots hooks at startup. If hooks are modified externally, Claude Code warns you and requires review in /hooks before changes apply.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Configuration Schema
Section titled “Configuration Schema”Three levels of nesting:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/my-check.sh", "timeout": 30 } ] } ] }}PreToolUse— the hook event{ "matcher": "Bash", "hooks": [...] }— the matcher group{ "type": "command", "command": "..." }— the hook handler
For skill/agent frontmatter, use YAML:
hooks: PreToolUse: - matcher: "Bash" hooks: - type: command command: "./scripts/security-check.sh"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Handler Types
Section titled “Handler Types”command (shell)
Section titled “command (shell)”Executes a shell command. Input arrives on stdin as JSON. Output communicated via exit codes and stdout.
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check.sh", "async": false, "timeout": 60}| Field | Required | Description |
|---|---|---|
type | yes | "command" |
command | yes | Shell command to execute |
async | no | If true, runs in background without blocking. Decision fields have no effect |
timeout | no | Seconds before cancel. Default: 600 |
statusMessage | no | Custom spinner message while hook runs |
once | no | If true, runs only once per session then removed (skills only) |
http (webhook)
Section titled “http (webhook)”POSTs event JSON to a URL. Response body uses same JSON output format as command hooks.
{ "type": "http", "url": "http://localhost:8080/hooks/pre-tool-use", "headers": { "Authorization": "Bearer $MY_TOKEN" }, "allowedEnvVars": ["MY_TOKEN"], "timeout": 30}| Field | Required | Description |
|---|---|---|
type | yes | "http" |
url | yes | URL to POST to |
headers | no | Additional headers. Supports $VAR_NAME interpolation for vars in allowedEnvVars |
allowedEnvVars | no | Vars that may be interpolated into headers. Required for any env var interpolation |
timeout | no | Seconds before cancel. Default: 600 |
statusMessage | no | Custom spinner message |
HTTP error handling: non-2xx status, connection failures, and timeouts are all non-blocking. To block via HTTP, return a 2xx response with the appropriate JSON decision body.
HTTP hooks must be configured by editing settings JSON directly — the /hooks interactive menu only supports command hooks.
prompt (LLM evaluation)
Section titled “prompt (LLM evaluation)”Sends a prompt + hook input to a Claude model (Haiku by default) for single-turn yes/no evaluation.
{ "type": "prompt", "prompt": "Evaluate if this tool use is appropriate: $ARGUMENTS. Return JSON with ok and reason fields.", "model": "claude-haiku-4-5", "timeout": 30}| Field | Required | Description |
|---|---|---|
type | yes | "prompt" |
prompt | yes | Prompt text. Use $ARGUMENTS placeholder for hook input JSON |
model | no | Model to use. Defaults to a fast model |
timeout | no | Default: 30 |
statusMessage | no | Custom spinner message |
once | no | Skills only |
Model response must be JSON:
{ "ok": true }{ "ok": false, "reason": "Explanation shown to Claude" }Supported events: PermissionRequest, PostToolUse, PostToolUseFailure, PreToolUse, Stop, SubagentStop, TaskCompleted, UserPromptSubmit
agent (spawn subagent verifier)
Section titled “agent (spawn subagent verifier)”Spawns a subagent with tool access (Read, Grep, Glob) for multi-turn verification. Can inspect actual files and test output. Up to 50 turns before returning a decision.
{ "type": "agent", "prompt": "Verify all unit tests pass before Claude stops. Run the test suite and check results. $ARGUMENTS", "model": "claude-haiku-4-5", "timeout": 120}| Field | Required | Description |
|---|---|---|
type | yes | "agent" |
prompt | yes | Prompt. Use $ARGUMENTS placeholder for hook input JSON |
model | no | Defaults to a fast model |
timeout | no | Default: 60 |
statusMessage | no | Custom spinner message |
once | no | Skills only |
Response schema same as prompt hooks: { "ok": true } or { "ok": false, "reason": "..." }
Supported events: same as prompt hooks — PermissionRequest, PostToolUse, PostToolUseFailure, PreToolUse, Stop, SubagentStop, TaskCompleted, UserPromptSubmit
Command-only events (only type: "command" supported): ConfigChange, Notification, PreCompact, SessionEnd, SessionStart, SubagentStart, TeammateIdle, WorktreeCreate, WorktreeRemove
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Exit Codes
Section titled “Exit Codes”| Exit Code | Meaning | Behavior |
|---|---|---|
0 | Success | Claude Code parses stdout for JSON. JSON only processed on exit 0 |
2 | Blocking error | stdout ignored. stderr fed back to Claude as error message. Effect is event-specific (see table below) |
| any other | Non-blocking error | stderr shown in verbose mode (Ctrl+O). Execution continues |
Exit Code 2 Behavior Per Event
Section titled “Exit Code 2 Behavior Per Event”| Hook event | Can block? | What happens on exit 2 |
|---|---|---|
PreToolUse | yes | Blocks the tool call |
PermissionRequest | yes | Denies the permission |
UserPromptSubmit | yes | Blocks prompt processing and erases the prompt |
Stop | yes | Prevents Claude from stopping, continues conversation |
SubagentStop | yes | Prevents the subagent from stopping |
TeammateIdle | yes | Prevents the teammate from going idle (teammate continues working) |
TaskCompleted | yes | Prevents the task from being marked as completed |
ConfigChange | yes | Blocks config change from taking effect (except policy_settings) |
PostToolUse | no | Shows stderr to Claude (tool already ran) |
PostToolUseFailure | no | Shows stderr to Claude (tool already failed) |
Notification | no | Shows stderr to user only |
SubagentStart | no | Shows stderr to user only |
SessionStart | no | Shows stderr to user only |
SessionEnd | no | Shows stderr to user only |
PreCompact | no | Shows stderr to user only |
WorktreeCreate | yes | Any non-zero exit causes worktree creation to fail |
WorktreeRemove | no | Failures logged in debug mode only |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Common Input Fields (All Events)
Section titled “Common Input Fields (All Events)”Every hook receives these fields as JSON on stdin (command) or POST body (http):
| Field | Description |
|---|---|
session_id | Current session identifier |
transcript_path | Path to conversation JSONL file |
cwd | Current working directory when hook is invoked |
permission_mode | "default", "plan", "acceptEdits", "dontAsk", or "bypassPermissions" |
hook_event_name | Name of the event that fired |
JSON Output Fields (Universal)
Section titled “JSON Output Fields (Universal)”On exit 0, output a JSON object to stdout. These fields work across all events:
| Field | Default | Description |
|---|---|---|
continue | true | If false, Claude stops processing entirely. Takes precedence over event-specific decisions |
stopReason | none | Message shown to user when continue is false. Not shown to Claude |
suppressOutput | false | If true, hides stdout from verbose mode output |
systemMessage | none | Warning message shown to the user |
Stop Claude entirely regardless of event:
{ "continue": false, "stopReason": "Build failed — fix errors before continuing" }━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
hookSpecificOutput Format
Section titled “hookSpecificOutput Format”The canonical format for fine-grained control. Must include hookEventName matching the event that fired.
PreToolUse — allow/deny/ask with optional input modification
Section titled “PreToolUse — allow/deny/ask with optional input modification”{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Reason shown to Claude on deny; shown to user on allow/ask", "updatedInput": { "field_to_modify": "new_value" }, "additionalContext": "Context injected into Claude before the tool executes" }}| Field | Description |
|---|---|
permissionDecision | "allow" bypasses permission system, "deny" blocks tool call, "ask" prompts user |
permissionDecisionReason | For allow/ask: shown to user, not Claude. For deny: shown to Claude |
updatedInput | Modifies tool input before execution. Combine with "allow" to auto-approve with modified input |
additionalContext | String injected into Claude’s context before tool executes |
Deprecated: PreToolUse previously used top-level decision: "approve"/"block". These still work ("approve" → "allow", "block" → "deny") but use hookSpecificOutput.permissionDecision instead.
PermissionRequest — allow/deny on behalf of user
Section titled “PermissionRequest — allow/deny on behalf of user”{ "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "allow", "updatedInput": { "command": "npm run lint" }, "updatedPermissions": [{ "type": "toolAlwaysAllow", "tool": "Bash" }] } }}For deny:
{ "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "deny", "message": "Database writes are not allowed in this context", "interrupt": true } }}SessionStart — inject context
Section titled “SessionStart — inject context”{ "hookSpecificOutput": { "hookEventName": "SessionStart", "additionalContext": "Current sprint: Sprint 42. Focus area: auth refactor." }}UserPromptSubmit — block or inject context
Section titled “UserPromptSubmit — block or inject context”{ "decision": "block", "reason": "Explanation shown to user", "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "Additional context added to Claude's context" }}PostToolUse — feedback to Claude
Section titled “PostToolUse — feedback to Claude”{ "decision": "block", "reason": "Lint errors found — fix before proceeding", "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "Lint output: ...", "updatedMCPToolOutput": "replacement output for MCP tools only" }}PostToolUseFailure — context on failure
Section titled “PostToolUseFailure — context on failure”{ "hookSpecificOutput": { "hookEventName": "PostToolUseFailure", "additionalContext": "This command commonly fails due to missing env vars. Check .env.example." }}Stop / SubagentStop — block stop
Section titled “Stop / SubagentStop — block stop”{ "decision": "block", "reason": "Tests must pass before finishing. Run: npm test"}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Decision Control Quick Reference
Section titled “Decision Control Quick Reference”| Events | Decision pattern | Key fields |
|---|---|---|
UserPromptSubmit, PostToolUse, PostToolUseFailure, Stop, SubagentStop, ConfigChange | Top-level decision | decision: "block", reason |
TeammateIdle, TaskCompleted | Exit code only | Exit 2 blocks; stderr is feedback |
PreToolUse | hookSpecificOutput | permissionDecision (allow/deny/ask), permissionDecisionReason |
PermissionRequest | hookSpecificOutput | decision.behavior (allow/deny) |
WorktreeCreate | stdout path | Hook prints absolute path to created worktree. Non-zero exit fails creation |
WorktreeRemove, Notification, SessionEnd, PreCompact | none | Side effects only — no decision control |
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Matcher Syntax
Section titled “Matcher Syntax”Matchers are regex strings. Empty string, "*", or omitted matcher fires on every occurrence.
| Event | What matcher filters | Example values |
|---|---|---|
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest | tool name | Bash, Edit|Write, mcp__.*, Notebook.* |
SessionStart | how session started | startup, resume, clear, compact |
SessionEnd | why session ended | clear, logout, prompt_input_exit, bypass_permissions_disabled, other |
Notification | notification type | permission_prompt, idle_prompt, auth_success, elicitation_dialog |
SubagentStart, SubagentStop | agent type | Bash, Explore, Plan, or custom agent names |
PreCompact | what triggered compaction | manual, auto |
ConfigChange | configuration source | user_settings, project_settings, local_settings, policy_settings, skills |
UserPromptSubmit, Stop, TeammateIdle, TaskCompleted, WorktreeCreate, WorktreeRemove | no matcher support | matcher field silently ignored |
MCP Tool Matching
Section titled “MCP Tool Matching”MCP tools follow the pattern mcp__<server>__<tool>:
mcp__memory__create_entities → Memory server's create_entities toolmcp__filesystem__read_file → Filesystem server's read_file toolmcp__github__search_repositories → GitHub server's searchRegex patterns for MCP:
mcp__memory__.* → all tools from memory servermcp__.*__write.* → any tool containing "write" from any servermcp__.* → all MCP tools━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Environment Variables Available to Hooks
Section titled “Environment Variables Available to Hooks”| Variable | Available In | Description |
|---|---|---|
CLAUDE_PROJECT_DIR | all command hooks | Project root directory. Wrap in quotes for paths with spaces |
| Plugin root env var | plugin hooks only | Plugin’s root directory. Use for portable script paths |
CLAUDE_ENV_FILE | SessionStart only | File path where you can persist env vars for subsequent Bash commands |
CLAUDE_CODE_REMOTE | all command hooks | Set to "true" in remote web environments. Unset in local CLI |
Using CLAUDE_PROJECT_DIR
Section titled “Using CLAUDE_PROJECT_DIR”{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"}Persisting env vars from SessionStart
Section titled “Persisting env vars from SessionStart”#!/bin/bashif [ -n "$CLAUDE_ENV_FILE" ]; then echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE" echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE" # Use append (>>) to preserve vars set by other hooksfiexit 0Variables written to CLAUDE_ENV_FILE are available in all subsequent Bash commands during the session. This variable is ONLY available in SessionStart hooks.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Debugging Hooks
Section titled “Debugging Hooks”| Debug mechanism | How it works |
|---|---|
| stderr → Claude context | On exit 2, stderr is fed back to Claude. Use this to explain why something was blocked |
| stderr → verbose mode | On non-zero exits (not 2), stderr appears in verbose mode (Ctrl+O) |
| stdout → transcript | On exit 0, stdout appears in the transcript (unless suppressOutput: true) |
| stdout for SessionStart/UserPromptSubmit | stdout added as context Claude can see and act on |
/hooks menu | View, add, delete hooks. Shows source labels: [User], [Project], [Local], [Plugin] |
disableAllHooks: true | Temporarily disable all hooks in settings or via toggle in /hooks menu |
JSON validation failure: If your shell profile prints text on startup, it can interfere with JSON parsing. Keep startup scripts clean or redirect noise to stderr.
Mutual exclusion: Choose one approach per hook — either exit codes alone, or exit 0 + JSON output. Claude Code only processes JSON on exit 0. JSON in exit 2 output is ignored.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Event Reference with Input Schemas
Section titled “Event Reference with Input Schemas”SessionStart
Section titled “SessionStart”Fires when a session begins or resumes. Keep these hooks fast — they run on every session.
Input fields (in addition to common fields):
| Field | Description |
|---|---|
source | "startup", "resume", "clear", or "compact" |
model | Model identifier |
agent_type | Agent name if started with claude --agent <name> (optional) |
Decision: cannot block. Can inject additionalContext into Claude’s context.
UserPromptSubmit
Section titled “UserPromptSubmit”Fires when user submits a prompt, before Claude processes it.
Input fields:
| Field | Description |
|---|---|
prompt | The text the user submitted |
Decision: decision: "block" with reason. Or add additionalContext on allow.
PreToolUse
Section titled “PreToolUse”Fires after Claude creates tool parameters, before the tool call executes. Matches on tool name.
Supported tools: Bash, Edit, Write, Read, Glob, Grep, Agent, WebFetch, WebSearch, and any MCP tool.
Input fields:
| Field | Description |
|---|---|
tool_name | Name of the tool |
tool_use_id | Unique identifier for this tool call |
tool_input | Tool-specific parameters (see below) |
Key tool_input schemas:
Bash: command (string), description (string, optional), timeout (ms, optional), run_in_background (bool, optional)
Write: file_path (absolute path), content (string)
Edit: file_path, old_string, new_string, replace_all (bool, optional)
Read: file_path, offset (line number, optional), limit (lines, optional)
Agent: prompt, description, subagent_type, model (optional)
Decision: hookSpecificOutput.permissionDecision — allow/deny/ask. Can modify input with updatedInput.
PermissionRequest
Section titled “PermissionRequest”Fires when Claude Code is about to show the user a permission dialog. Matches on tool name.
Difference from PreToolUse: fires specifically when a permission dialog would appear, while PreToolUse fires before every tool execution regardless of permission status.
Input fields:
| Field | Description |
|---|---|
tool_name | Name of the tool |
tool_input | Tool parameters |
permission_suggestions | Array of “always allow” options the user would normally see (optional) |
Decision: hookSpecificOutput.decision.behavior — allow or deny.
PostToolUse
Section titled “PostToolUse”Fires immediately after a tool completes successfully. Cannot prevent what already happened.
Input fields:
| Field | Description |
|---|---|
tool_name | Name of the tool |
tool_use_id | Unique identifier |
tool_input | Parameters sent to the tool |
tool_response | Result returned by the tool |
Decision: decision: "block" with reason provides feedback to Claude. Also supports additionalContext and updatedMCPToolOutput (MCP tools only).
PostToolUseFailure
Section titled “PostToolUseFailure”Fires when a tool execution fails. Use for logging, alerts, corrective feedback.
Input fields:
| Field | Description |
|---|---|
tool_name | Name of the tool |
tool_use_id | Unique identifier |
tool_input | Parameters sent to the tool |
error | String describing what went wrong |
is_interrupt | Whether failure was caused by user interruption (optional bool) |
Decision: cannot block. Can inject additionalContext.
Notification
Section titled “Notification”Fires when Claude Code sends a notification. Cannot block notifications.
Input fields:
| Field | Description |
|---|---|
message | Notification text |
title | Optional title |
notification_type | permission_prompt, idle_prompt, auth_success, or elicitation_dialog |
Decision: none. Can inject additionalContext.
SubagentStart
Section titled “SubagentStart”Fires when a subagent is spawned via the Agent tool. Cannot block subagent creation.
Input fields:
| Field | Description |
|---|---|
agent_id | Unique identifier for the subagent |
agent_type | Built-in (Bash, Explore, Plan) or custom agent name |
Decision: none. Can inject additionalContext into the subagent’s context.
SubagentStop
Section titled “SubagentStop”Fires when a subagent finishes. Uses same decision control as Stop.
Input fields:
| Field | Description |
|---|---|
stop_hook_active | true if already continuing due to a stop hook — check to prevent infinite loops |
agent_id | Unique identifier for the subagent |
agent_type | Agent type (used for matcher filtering) |
agent_transcript_path | Subagent’s own transcript in subagents/ folder |
last_assistant_message | Text content of subagent’s final response |
Decision: decision: "block" with reason prevents the subagent from stopping.
Fires when the main Claude Code agent finishes responding. Does not fire on user interrupt.
Input fields:
| Field | Description |
|---|---|
stop_hook_active | true if Claude is already continuing due to a stop hook — ALWAYS check this |
last_assistant_message | Text content of Claude’s final response |
Decision: decision: "block" with reason (required on block) prevents stopping.
TeammateIdle
Section titled “TeammateIdle”Fires when an agent team teammate is about to go idle. Exit code only — no JSON decision control.
Input fields:
| Field | Description |
|---|---|
teammate_name | Name of the teammate going idle |
team_name | Name of the team |
Decision: exit code 2 only. stderr fed back to teammate as feedback.
TaskCompleted
Section titled “TaskCompleted”Fires when a task is being marked as completed. Exit code only — no JSON decision control.
Input fields:
| Field | Description |
|---|---|
task_id | Identifier of the task |
task_subject | Title of the task |
task_description | Detailed description (may be absent) |
teammate_name | Name of the completing teammate (may be absent) |
team_name | Name of the team (may be absent) |
Decision: exit code 2 only. stderr fed back to model as feedback.
ConfigChange
Section titled “ConfigChange”Fires when a configuration file changes mid-session. Matches on configuration source.
Matcher values: user_settings, project_settings, local_settings, policy_settings, skills
Input fields:
| Field | Description |
|---|---|
source | Which config type changed |
file_path | Path to the specific file modified (optional) |
Decision: decision: "block" with reason. Note: policy_settings changes cannot be blocked.
WorktreeCreate
Section titled “WorktreeCreate”Fires when claude --worktree or isolation: "worktree" triggers worktree creation. Replaces default git behavior — use to support non-git VCS.
Input fields:
| Field | Description |
|---|---|
name | Slug identifier for the new worktree (e.g., bold-oak-a3f2 or user-specified) |
Decision: hook must print absolute path of created worktree to stdout. Non-zero exit fails creation. Only type: "command" supported.
WorktreeRemove
Section titled “WorktreeRemove”Cleanup counterpart to WorktreeCreate. Fires when worktree is being removed.
Input fields:
| Field | Description |
|---|---|
worktree_path | Absolute path to the worktree being removed |
Decision: none. Cannot block removal. Failures logged in debug mode only. Only type: "command" supported.
PreCompact
Section titled “PreCompact”Fires before Claude Code runs a context compaction operation.
Matcher values: manual (user ran /compact), auto (auto-compact when context window full)
Input fields:
| Field | Description |
|---|---|
trigger | "manual" or "auto" |
custom_instructions | Instructions from /compact <text>. Empty string for auto |
Decision: none. Use for side effects like preserving critical context.
SessionEnd
Section titled “SessionEnd”Fires when a Claude Code session terminates.
Matcher values: clear, logout, prompt_input_exit, bypass_permissions_disabled, other
Input fields:
| Field | Description |
|---|---|
reason | Why the session ended (see matcher values) |
Decision: none. Cannot block termination. Use for cleanup, logging, state preservation.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Common Patterns
Section titled “Common Patterns”Quality gate on Stop (TDD enforcement)
Section titled “Quality gate on Stop (TDD enforcement)”{ "Stop": [ { "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/task-completed.sh", "timeout": 120 } ] } ]}#!/bin/bashINPUT=$(cat)STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')
# Always guard against infinite loopsif [ "$STOP_HOOK_ACTIVE" = "true" ]; then exit 0fi
if ! npm test 2>&1; then echo "Tests failing. Fix before stopping." >&2 exit 2fi
exit 0Context injection on SessionStart
Section titled “Context injection on SessionStart”#!/bin/bash# Inject dynamic project contextOPEN_ISSUES=$(gh issue list --limit 5 --json title,number 2>/dev/null || echo "[]")jq -n --arg ctx "Open issues: $OPEN_ISSUES" '{ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: $ctx }}'exit 0Permission gate — deny destructive ops
Section titled “Permission gate — deny destructive ops”#!/bin/bash# PreToolUse hook for BashCOMMAND=$(jq -r '.tool_input.command' < /dev/stdin)
if echo "$COMMAND" | grep -qE '(rm -rf|DROP TABLE|git push --force)'; then jq -n '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Destructive operation blocked by policy" } }' exit 0fi
exit 0Auto-approve safe read operations
Section titled “Auto-approve safe read operations”#!/bin/bashINPUT=$(cat)TOOL=$(echo "$INPUT" | jq -r '.tool_name')
# Auto-approve read-only toolsif [[ "$TOOL" =~ ^(Read|Glob|Grep)$ ]]; then jq -n '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", permissionDecisionReason: "Read-only operation auto-approved" } }' exit 0fi
exit 0Lint check after file writes
Section titled “Lint check after file writes”{ "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-check.sh", "async": true, "timeout": 60 } ] } ]}Subagent context injection (SubagentStart)
Section titled “Subagent context injection (SubagentStart)”{ "SubagentStart": [ { "hooks": [ { "type": "command", "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":\"Follow security policy: no hardcoded secrets, always use env vars.\"}}'" } ] } ]}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Anti-Patterns
Section titled “Anti-Patterns”Infinite loop on Stop (most common mistake)
Section titled “Infinite loop on Stop (most common mistake)”# WRONG — causes infinite loop#!/bin/bashnpm test || { echo "Fix tests" >&2; exit 2; }exit 0# CORRECT — always check stop_hook_active#!/bin/bashINPUT=$(cat)if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then exit 0 # already continuing from a stop hook — don't loopfinpm test || { echo "Fix tests" >&2; exit 2; }exit 0Same pattern applies to SubagentStop.
Overly broad matchers
Section titled “Overly broad matchers”// BAD — fires on every single tool call{ "matcher": "*" }
// BETTER — target specific tools{ "matcher": "Bash|Write|Edit" }
// BEST — target only what you need to control{ "matcher": "Bash" }JSON output on non-zero exit
Section titled “JSON output on non-zero exit”# WRONG — JSON is ignored on exit 2echo '{"hookSpecificOutput":{"permissionDecision":"deny"}}'exit 2
# CORRECT — exit 0 + JSON for structured controlecho '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Blocked"}}'exit 0
# CORRECT — exit 2 for simple blocking with stderr messageecho "This operation is blocked" >&2exit 2Shell startup output polluting JSON
Section titled “Shell startup output polluting JSON”# BAD — shell startup scripts that echo text will break JSON parsing# ~/.zshrc: echo "Welcome to my shell!" ← this breaks hooks
# CORRECT — redirect non-JSON startup output to stderr or suppress it# Or test your hook: echo '{}' | your-hook.sh | jq .Async hooks for decision-making
Section titled “Async hooks for decision-making”// WRONG — async hooks cannot make decisions{ "type": "command", "command": ".claude/hooks/block-unsafe.sh", "async": true // decisions like permissionDecision have no effect}
// CORRECT — use sync for blocking/control, async for side effects{ "type": "command", "command": ".claude/hooks/log-tool-use.sh", "async": true // fine for logging}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Async Hooks
Section titled “Async Hooks”Set "async": true on type: "command" hooks to run in background without blocking Claude. Use for long-running side effects (deployments, test suites, logging, notifications).
Critical: Async hooks cannot control Claude’s behavior. decision, permissionDecision, continue, and other response fields have no effect because the action they would have controlled has already completed.
When the async script finishes, its output is delivered on the next conversation turn.
{ "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "/path/to/run-tests.sh", "async": true, "timeout": 120 } ] } ]}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Hooks in Skills and Agents
Section titled “Hooks in Skills and Agents”Hooks defined in skill/agent YAML frontmatter are scoped to that component’s lifetime and cleaned up when it finishes.
---name: secure-operationsdescription: Perform operations with security checkshooks: PreToolUse: - matcher: "Bash" hooks: - type: command command: "./scripts/security-check.sh" timeout: 30 Stop: - hooks: - type: command command: "./scripts/verify-complete.sh"---Note: For subagents, Stop hooks in frontmatter are automatically converted to SubagentStop since that’s the event that fires when a subagent completes.
The once field (skills only) makes a hook run once per session then remove itself — useful for one-time setup:
hooks: SessionStart: - hooks: - type: command command: "./scripts/load-context.sh" once: true━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Parallel Execution and Deduplication
Section titled “Parallel Execution and Deduplication”All matching hooks in a matcher group run in parallel. Identical handlers are deduplicated automatically:
- Command hooks: deduplicated by command string
- HTTP hooks: deduplicated by URL
- Prompt/agent hooks: deduplicated by prompt string
Handlers run in the current directory with Claude Code’s environment variables.