Claude Code Hooks let you intercept the agent loop at 32+ lifecycle events and inject custom logic — block dangerous commands, notify Slack, run formatters, enforce policies, log every tool call. This page is the complete production reference: every event, every handler type, every exit-code rule, verified against Anthropic’s official Claude Code docs as of May 2026 (v2.1.141+).
If you’re wiring up Hooks in production, this is the page to bookmark.
The 32+ Hook Events (Full Catalog)
Hooks fire at five lifecycle cadences. Pick the right one for your use case.
Session-Level (Once per session)
| Event | When it fires |
|---|---|
SessionStart | When a session begins or resumes |
Setup | When you start Claude Code with --init-only, --init, or --maintenance in -p mode |
SessionEnd | When a session terminates |
Turn-Level (Once per user prompt)
| Event | When it fires | Blocks? |
|---|---|---|
UserPromptSubmit | Before Claude processes the submitted prompt | ✅ |
UserPromptExpansion | When a typed command expands into a prompt, before it reaches Claude | ✅ |
Stop | When Claude finishes responding | ✅ |
StopFailure | When the turn ends due to an API error | ❌ observability only |
Agentic Loop (Every tool call)
| Event | When it fires | Blocks? |
|---|---|---|
PreToolUse | Before a tool call executes | ✅ |
PostToolUse | After a tool call succeeds | ❌ (already ran) |
PostToolUseFailure | After a tool call fails | ❌ (already failed) |
PostToolBatch | After a full batch of parallel tool calls resolves, before the next model call | ✅ stops loop |
PermissionRequest | When a permission dialog appears | ✅ denies |
PermissionDenied | When a tool call is denied by the auto mode classifier | ❌ already denied |
Agent / Team Events
| Event | When it fires | Blocks? |
|---|---|---|
SubagentStart | When a subagent is spawned | ❌ |
SubagentStop | When a subagent finishes | ✅ prevents stop |
TeammateIdle | When an agent team teammate is about to go idle | ✅ |
TaskCreated | When a task is being created via TaskCreate | ✅ rolls back |
TaskCompleted | When a task is being marked as completed | ✅ prevents completion |
File & Environment Events
| Event | When it fires | Blocks? |
|---|---|---|
InstructionsLoaded | When a CLAUDE.md or .claude/rules/*.md file is loaded into context | ❌ |
ConfigChange | When a configuration file changes during a session | ✅ (except policy_settings) |
CwdChanged | When the working directory changes | ❌ |
FileChanged | When a watched file changes on disk | ❌ |
WorktreeCreate | When a worktree is being created via --worktree | ❌ |
WorktreeRemove | When a worktree is being removed | ❌ |
Context & Notification Events
| Event | When it fires | Blocks? |
|---|---|---|
PreCompact | Before context compaction | ✅ blocks compaction |
PostCompact | After context compaction completes | ❌ |
Notification | When Claude Code sends a notification | ❌ |
Elicitation | When an MCP server requests user input during a tool call | ❌ |
ElicitationResult | After a user responds to an MCP elicitation | ❌ |
Total: 27 distinct events (the “32+” in the title counts subtypes — for example SessionStart accepts matchers startup, resume, clear, compact, which behave as separate trigger points).
The 5 Handler Types
{
"type": "command" | "http" | "mcp_tool" | "prompt" | "agent",
"if": "Bash(git *)", // Optional: permission rule filter
"timeout": 600, // Seconds before canceling
"statusMessage": "Checking...", // Optional: spinner message
"once": true // Optional: run once per session
}
1. command — Shell Script
The workhorse. Spawn a shell command, pipe stdin JSON, read stdout JSON / stderr.
{
"type": "command",
"command": "/path/to/secret-guard.sh",
"args": ["${tool_input.file_path}"],
"shell": "bash",
"async": false
}
- Shell form (no
args): full tokenization, pipes, globs available. - Exec form (with
args): no shell, each arg verbatim, path placeholders substituted. async: true: runs in background, doesn’t block the agent loop.asyncRewake: true: wakes when the async command exits with code 2.
2. http — Webhook
POST JSON to an HTTP endpoint. Useful for centralized policy services or Slack/Discord notifications.
{
"type": "http",
"url": "http://localhost:8080/hook",
"headers": { "Authorization": "Bearer $MY_TOKEN" },
"allowedEnvVars": ["MY_TOKEN"]
}
Response handling:
- 2xx empty body: success (= exit 0).
- 2xx with JSON: parsed as hook output.
- Non-2xx: non-blocking error, execution continues.
3. mcp_tool — Delegate to MCP Server
Forward the hook decision to a tool exposed by a connected MCP server.
{
"type": "mcp_tool",
"server": "my_server",
"tool": "security_scan",
"input": { "file_path": "${tool_input.file_path}" }
}
If the server is disconnected or the tool returns isError: true, the hook produces a non-blocking error and execution continues. Don’t rely on mcp_tool for policy enforcement that must hold under network failure.
4. prompt — Single-Turn Claude Decision
{
"type": "prompt",
"prompt": "Is this $ARGUMENTS safe? Reply JSON with {decision, reason}.",
"model": "claude-haiku-4-5"
}
$ARGUMENTS is the hook input JSON. The model returns a JSON object that Claude Code parses as hook output. Use Haiku for speed when the decision is bounded.
5. agent — Spawn a Subagent
Spawn a full subagent with tool access. The heaviest option, but supports multi-step reasoning.
{
"type": "agent",
"prompt": "Investigate why this command failed: $ARGUMENTS",
"model": "claude-opus-4-7"
}
Matcher Patterns
The matcher field filters when a hook fires within an event.
| Matcher value | Evaluated as | Example |
|---|---|---|
"*", "", omitted | Match all | Fires on every occurrence |
Letters/digits/_/| only | Exact string or |-separated list | Bash or Edit|Write |
| Contains other chars | JavaScript regex | ^Notebook or mcp__memory__.* |
MCP Tool Matching
MCP tools follow the pattern mcp__<server>__<tool>:
{ "matcher": "mcp__memory__.*" } // All memory server tools
{ "matcher": "mcp__.*__write.*" } // Write tools from any server
Per-Event Matcher Targets
| Event group | Matcher filters | Example values |
|---|---|---|
PreToolUse/PostToolUse/etc | Tool name | Bash, Edit|Write, mcp__.* |
SessionStart | Session source | startup, resume, clear, compact |
Setup | CLI flag | init, maintenance |
SessionEnd | Exit reason | clear, resume, logout, other |
Notification | Notification type | permission_prompt, auth_success |
SubagentStart/SubagentStop | Agent type | general-purpose, Explore, Plan |
PreCompact/PostCompact | Trigger type | manual, auto |
ConfigChange | Config source | user_settings, project_settings, policy_settings |
FileChanged | Literal filenames | .envrc|.env |
StopFailure | Error type | rate_limit, authentication_failed, server_error |
InstructionsLoaded | Load reason | session_start, nested_traversal, path_glob_match |
Exit Code Semantics (Critical for Security)
The single most important rule from the official docs:
“For most hook events, only exit code 2 blocks the action. Claude Code treats exit code 1 as a non-blocking error and proceeds with the action, even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use
exit 2.”
| Exit code | Meaning | Behavior |
|---|---|---|
| 0 | Success | Parse stdout for JSON output. Proceed unless JSON blocks. |
| 2 | Blocking error | Ignore stdout. Use stderr as error message. Blocks the action for capable events. |
| Any other | Non-blocking error | Ignore stdout. Show first line of stderr in transcript. Execution continues. |
This is the gotcha that breaks half of all hook implementations: developers reach for exit 1 instinctively, the hook fails silently, and the dangerous action proceeds anyway. Always use exit 2 for policy enforcement.
Configuration Locations
| Location | Scope | Shareable |
|---|---|---|
~/.claude/settings.json | All projects | No, local |
.claude/settings.json | Single project | Yes, committed |
.claude/settings.local.json | Single project | No, gitignored |
| Managed policy settings | Organization-wide | Yes, admin-controlled |
Plugin hooks/hooks.json | When enabled | Yes, bundled |
| Skill/agent frontmatter | Component lifetime | Yes, in file |
Three-Level Nesting Structure
{
"hooks": {
"PreToolUse": [ // Level 1: event
{
"matcher": "Bash", // Level 2: matcher group
"hooks": [
{
"type": "command", // Level 3: handler
"command": "/path/to/script.sh"
}
]
}
]
}
}
JSON Input / Output
Common stdin Input (all events)
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "default",
"hook_event_name": "PreToolUse"
}
In a subagent context, you additionally get agent_id and agent_type. In tool-use context, you get effort.level (also available as $CLAUDE_EFFORT env var).
JSON stdout Output
{
"continue": true, // false stops Claude entirely
"stopReason": "Build failed", // Message when continue=false
"suppressOutput": false, // Omit from debug log
"systemMessage": "Warning", // User warning
"terminalSequence": "\033]...", // Terminal escape (v2.1.141+)
"decision": "block", // For most events
"reason": "Not allowed",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": "...",
"permissionDecision": "deny",
"permissionDecisionReason": "..."
}
}
Decision Control Pattern
Different events expect different output schemas:
Most events — top-level decision:
{ "decision": "block", "reason": "Test suite must pass" }
PreToolUse — nested hookSpecificOutput.permissionDecision:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database writes not allowed"
}
}
PermissionRequest — hookSpecificOutput.decision:
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedInput": { "command": "npm run lint" }
}
}
}
PreToolUse Tool Input Schemas (Most Used)
Bash
{
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite",
"timeout": 120000,
"run_in_background": false
}
}
Edit / Write
// Write
{ "tool_input": { "file_path": "...", "content": "..." } }
// Edit
{ "tool_input": {
"file_path": "...",
"old_string": "original",
"new_string": "replacement",
"replace_all": false
}}
Read / Glob / Grep
// Read
{ "tool_input": { "file_path": "...", "offset": 10, "limit": 50 } }
// Glob
{ "tool_input": { "pattern": "**/*.ts", "path": "/path/to/dir" } }
// Grep
{ "tool_input": {
"pattern": "TODO.*fix",
"path": "/path/to/dir",
"glob": "*.ts",
"output_mode": "content",
"-i": true,
"multiline": false
}}
WebFetch / WebSearch
{ "tool_input": { "url": "https://example.com/api", "prompt": "Extract endpoints" } }
{ "tool_input": { "query": "search terms" } }
Environment Variables
| Variable | Available in | Purpose |
|---|---|---|
| Parent shell env | All hooks | Inherited |
$CLAUDE_EFFORT | All hooks | Current effort level |
$CLAUDE_CODE_REMOTE | All hooks | "true" in web environments |
$CLAUDE_ENV_FILE | SessionStart, Setup, CwdChanged, FileChanged only | File path for persisting env vars |
Example — set env vars from a SessionStart hook:
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
Path Placeholders
Available in all hook types:
${CLAUDE_PROJECT_DIR}— Project root${CLAUDE_PLUGIN_ROOT}— Plugin installation directory${CLAUDE_PLUGIN_DATA}— Plugin persistent data directory
Production Patterns
Pattern 1: Dangerous Command Block (PreToolUse + exit 2)
#!/bin/bash
# block-dangerous.sh — PreToolUse hook, matcher: Bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$CMD" | grep -qE 'rm -rf /|git push --force.*main'; then
echo "Refusing dangerous command: $CMD" >&2
exit 2 # Critical: NOT exit 1
fi
exit 0
Pattern 2: Format After Edit (PostToolUse)
#!/bin/bash
# format-after-edit.sh — PostToolUse, matcher: Edit|Write
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path')
case "$FILE" in
*.ts|*.tsx) npx prettier --write "$FILE" 2>/dev/null ;;
*.py) black "$FILE" 2>/dev/null ;;
*.go) gofmt -w "$FILE" 2>/dev/null ;;
esac
exit 0
Pattern 3: Secret Guard (PreToolUse Edit)
#!/bin/bash
INPUT=$(cat)
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string')
if echo "$CONTENT" | grep -qE 'sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|AKIA[A-Z0-9]{16}'; then
echo "Refusing to write potential secret to file" >&2
exit 2
fi
exit 0
Pattern 4: Telegram Notify on Stop
#!/bin/bash
# telegram-notify.sh — Stop hook, no matcher
MSG="Claude Code session ended at $(date)"
curl -s "https://api.telegram.org/bot$TG_TOKEN/sendMessage" \
-d "chat_id=$TG_CHAT_ID" -d "text=$MSG" > /dev/null
exit 0
Pattern 5: Cost Logger (PostToolUse + PostToolBatch)
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
echo "$(date -u +%FT%TZ) $TOOL" >> ~/.claude/cost-log.tsv
exit 0
Pipe the log into a daily aggregator for per-project tool usage analytics.
Pattern 6: Block Direct Push to Main
#!/bin/bash
# matcher: Bash, "if": "Bash(git push*)"
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command')
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
if echo "$CMD" | grep -qE 'git push.*origin (main|master)' && [ "$BRANCH" = "main" ]; then
echo "Push to main blocked. Use a feature branch + PR." >&2
exit 2
fi
exit 0
Hooks vs Subagents vs Skills (When to Use Which)
| Need | Use |
|---|---|
| Lifecycle event (pre-commit, post-edit, on-stop) — fire automatically | Hook |
| Reusable workflow invoked on demand by name | Skill |
| Context isolation for a side task | Subagent |
| Always-loaded instructions at session start | CLAUDE.md |
For broader configuration-file comparison see AGENTS.md vs CLAUDE.md vs .cursor/rules. For subagent details see Claude Code Subagents: The Complete 2026 Reference.
Common Pitfalls
- Using
exit 1for policy enforcement. Claude Code treats it as non-blocking — the dangerous action proceeds. Alwaysexit 2for blocks. - Mixing decision schemas. Most events use top-level
decision, butPreToolUseuseshookSpecificOutput.permissionDecision. Reading the wrong field = your block doesn’t fire. - Forgetting
chmod +x. Hook command silently fails (exit code is non-existent). Inspect with a logging$HOOK_LOGwrite. - Matcher regex case mismatch.
Edit|Write|multiEdit(lowercase m) doesn’t matchMultiEdit. UseEdit|Write|MultiEditexactly. mcp_toolfor security enforcement. If the MCP server disconnects, the hook produces a non-blocking error — the action proceeds. Usecommandhooks for hard policy.disableAllHooksdoesn’t disable managed hooks. Organization-deployed managed policy hooks survivedisableAllHooks: trueset at user/project/local level.- HTTP hook timeouts. Default timeout is 600 seconds. If your webhook handler is slow, the agent loop stalls. Use
async: truefor non-critical webhooks.
FAQ
What’s the most important Claude Code hook exit code rule?
exit 2 blocks, every other non-zero is non-blocking. This is the single most common implementation bug. Developers reach for exit 1 (Unix convention for failure), but Claude Code only respects exit 2 for policy enforcement on capable events.
How many hook events does Claude Code support?
27 distinct events as of May 2026 (v2.1.141+): SessionStart, Setup, SessionEnd, UserPromptSubmit, UserPromptExpansion, Stop, StopFailure, PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied, SubagentStart, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, Notification, Elicitation, ElicitationResult.
What’s the difference between PreToolUse and PostToolUse?
PreToolUse fires before the tool runs and can block with exit 2. PostToolUse fires after the tool succeeds and is observability-only — you can’t undo what already happened, but you can react (format files, log usage, notify).
How do I write a hook that blocks dangerous Bash commands?
Use a PreToolUse hook with matcher: "Bash". Read stdin JSON, extract tool_input.command, check against a deny pattern, and exit 2 with stderr message if matched. Critical: not exit 1. See “Pattern 1: Dangerous Command Block” above for working code.
Can hooks call HTTP endpoints instead of shell scripts?
Yes — type: "http". POST JSON to a URL, parse 2xx response as hook output. Useful for centralized policy services or Slack/Discord notifications. Caveat: non-2xx is non-blocking, so don’t use HTTP hooks for hard policy enforcement under network failure.
What does hookSpecificOutput mean?
Some events expect their decision output in a nested hookSpecificOutput object rather than the top-level decision field. PreToolUse uses hookSpecificOutput.permissionDecision, PermissionRequest uses hookSpecificOutput.decision. Most other events use the flat top-level decision. Read the docs for your specific event.
Are hooks shared across team members?
Depends on location. .claude/settings.json (project, committed) is shared. ~/.claude/settings.json (user, machine-local) is personal. .claude/settings.local.json (gitignored) is personal-per-project. Plugin hooks travel with the plugin. Managed policy hooks are admin-deployed organization-wide and survive disableAllHooks.
Can a hook stop Claude entirely instead of just blocking one action?
Yes — return {"continue": false, "stopReason": "..."} in JSON output. The session stops with the message you provide. Use sparingly — typically reserved for catastrophic states (compromised credentials, broken build, etc.).
How do I debug a hook that isn’t running?
Three checks: (1) chmod +x on the script. (2) Confirm matcher regex matches the actual tool name (case-sensitive, watch for MultiEdit vs multiEdit). (3) Inject a debug log at the top: echo "[$(date)] hook called $0" >> ~/.claude/hook-debug.log. If that log file stays empty, the hook isn’t being invoked — check /hooks menu to confirm registration.
What’s the timeout for an async hook?
Default is 600 seconds (10 min). Override with "timeout": 60 in the handler config. For async hooks (async: true), the timeout applies to total runtime — the agent loop continues without waiting. Use asyncRewake: true if you want the async hook to be able to wake Claude later with exit 2.
Related Articles
- Claude Code Subagents: The Complete 2026 Reference
- AGENTS.md vs CLAUDE.md vs .cursor/rules: Three-Way Comparison
- Career-Ops on Claude Code: The Complete 2026 Reference
- Inside Career-Ops: 14-Mode Skills Architecture Lessons
- 15 Best Claude Code Skills You Should Install in 2026
- Browse 165+ Real AI Coding Rules