Claude Code hooks — deep reference

Hooks are Claude Code’s deterministic-automation layer: user-defined commands, HTTP endpoints, MCP-tool calls, prompt invocations, or agent dispatches that fire at specific points in the session lifecycle. They receive structured JSON context on stdin and return decisions through exit codes and JSON stdout. Hooks are how you enforce auto-formatting on every edit, block destructive bash commands, log every tool call, inject session-specific context at startup, write export statements that mutate the agent’s environment, send a desktop notification when Claude pauses, and dozens of similar deterministic policies.

See also

1. Hook cadences and events

Per code.claude.com/docs/en/hooks, events fire at these cadences:

CadenceEvents
Once per sessionSessionStart, SessionEnd, Setup
Once per turnUserPromptSubmit, UserPromptExpansion, Stop, StopFailure
Per tool callPreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied, PostToolBatch
Async / event-drivenFileChanged, CwdChanged, ConfigChange, WorktreeCreate, WorktreeRemove, Notification, InstructionsLoaded
Subagent-specificSubagentStart, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted
Context managementPreCompact, PostCompact
MCP elicitationElicitation, ElicitationResult

Event semantics

  • SessionStart — fires on every new session (incl. resumed). Matcher: session source (startup, resume, clear, compact).
  • Setup — runs from --init / --maintenance flags. Matcher: init, maintenance.
  • SessionEnd — fires on exit. Matcher: end reason (clear, resume, logout, other).
  • UserPromptSubmit — fires when user submits a prompt (before Claude sees it). Can inject additionalContext or block submission.
  • UserPromptExpansion — fires when @file / !command expansion happens in the prompt.
  • PreToolUse — fires before each tool call. Can allow / deny / ask / defer, modify input, inject context.
  • PostToolUse — fires after each tool call. Can inject additional context for Claude’s next turn.
  • PostToolUseFailure — fires when a tool errors. Same shape as PostToolUse.
  • PermissionRequest / PermissionDenied — fired on permission prompts. PermissionDenied can request retry: true.
  • PostToolBatch — fires after a batch of parallel tool calls completes.
  • Stop — fires when Claude tries to end its turn. Can block (decision: "block") to force continuation with a reason.
  • StopFailure — fires when the model errors; output is always ignored.
  • PreCompact / PostCompact — surround context auto-compaction.
  • FileChanged — file mutations detected by Claude Code’s watcher. Matcher: literal filenames pipe-separated (.envrc|.env).
  • CwdChangedcd in bash, --add-dir, etc.
  • ConfigChangesettings.json reload.
  • WorktreeCreateclaude -w creating a worktree; exit code non-zero aborts worktree creation.
  • Notification — Claude wants to surface something. Matcher: permission_prompt, idle_prompt, auth_success.
  • InstructionsLoaded — system prompt + tools assembled; last hook before first model call.
  • SubagentStart / SubagentStop — subagent lifecycle. Matcher: agent type (general-purpose, Explore, Plan, or custom name).
  • TeammateIdle — agent-teams teammate goes idle.
  • TaskCreated / TaskCompleted — for the task-list subsystem.
  • Elicitation — MCP elicitation request; can auto-respond.
  • ElicitationResult — fires after elicitation completes.

2. Matchers

The matcher field filters when a hook fires within an event type:

PatternBehavior
"Bash"Exact tool name
"Edit|Write|MultiEdit"Pipe-separated exact matches
"^Notebook"JS regex
"mcp__memory__.*"MCP tool pattern
"*"All (default)

Per-event matcher types

EventMatcher fieldExamples
Tool events (Pre/PostToolUse, etc.)Tool nameBash, Edit|Write, mcp__.*
SessionStartSourcestartup, resume, clear, compact
SetupTriggerinit, maintenance
SessionEndReasonclear, resume, logout, other
NotificationTypepermission_prompt, idle_prompt, auth_success
SubagentStart/StopAgent typegeneral-purpose, Explore, Plan, custom
FileChangedFilenames.env|.envrc|secrets.*
CwdChanged, UserPromptSubmit, PostToolBatch, Stop(no matcher; always fires)

3. Handler types

Five handler types, all returning the same JSON output schema (or using exit codes for command handlers):

3.1 Command (shell command or executable)

{
  "type": "command",
  "command": "./.claude/hooks/validate.sh",
  "args": ["--strict"],
  "timeout": 30,
  "async": false,
  "asyncRewake": false,
  "shell": "bash"
}

Exec form (args present): command is the executable, spawned directly with args. No shell interpretation. Safe for paths with spaces.

Shell form (args absent): command string passed to shell. Supports pipes, &&, globs, env expansion.

shell accepts "bash" (default) or "powershell".

3.2 HTTP

{
  "type": "http",
  "url": "http://localhost:8080/hook",
  "headers": {"Authorization": "Bearer $MY_TOKEN"},
  "allowedEnvVars": ["MY_TOKEN"],
  "timeout": 600
}

HTTP response handling:

  • 2xx + empty body → success
  • 2xx + plain text → success with context
  • 2xx + JSON → parsed as JSON output (same schema as command JSON output)
  • Non-2xx, timeout, connection failure → non-blocking error
  • HTTP cannot block via exit code — must return 2xx with JSON {"decision": "block", ...} or appropriate hookSpecificOutput to block

3.3 MCP tool

{
  "type": "mcp_tool",
  "server": "security",
  "tool": "scan_file",
  "input": {
    "file_path": "${tool_input.file_path}"
  }
}

Invokes a tool on an already-configured MCP server. ${tool_input.X} interpolates from the originating tool’s input. Useful for “before every Edit, run security MCP tool” patterns without a wrapper script.

3.4 Prompt

{
  "type": "prompt",
  "prompt": "Is this operation safe to proceed? $ARGUMENTS",
  "model": "claude-haiku-4-5"
}

Runs a sub-LLM call with the prompt. $ARGUMENTS (and event-specific interpolations) substituted. Default model: fast (Haiku). 30-second default timeout. Returns a yes/no decision (block / allow) or JSON output.

3.5 Agent

{
  "type": "agent",
  "agent": "security-reviewer"
}

Dispatches a subagent. 60-second default timeout. Used for richer policy decisions where a single prompt isn’t enough.

4. Common fields (all handler types)

FieldTypeDefaultPurpose
typestringrequiredcommand, http, mcp_tool, prompt, agent
ifstringTool-event-only. Permission rule syntax (Bash(git *)). Hook only fires if matched.
timeoutintegervaries (see below)Seconds before cancel
statusMessagestringCustom spinner message while hook runs

Timeout defaults: 600s (command, http, mcp_tool), 30s (prompt), 60s (agent), 30s for UserPromptSubmit tools.

5. Path placeholders

PlaceholderResolves to
${CLAUDE_PROJECT_DIR}Project root
${CLAUDE_PLUGIN_ROOT}Plugin install dir (plugin hooks only)
${CLAUDE_PLUGIN_DATA}Plugin persistent data dir
${user_config.X}User config values (plugin hooks only)
${tool_input.X}Input field from the originating tool call
$ARGUMENTSHook arguments (varies by event)

6. Stdin (JSON payload received by hook)

All events deliver these fields:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/directory",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "effort": {"level": "high"}
}

Subagent events add:

{ "agent_id": "uuid", "agent_type": "Explore" }

Tool events add:

{ "tool_name": "Bash", "tool_input": { /* parameters Claude is about to pass */ } }

PostToolUse events add:

{ "tool_response": { /* result returned to Claude */ } }

UserPromptSubmit events add:

{ "prompt": "the submitted prompt text" }

7. Exit codes (command handlers)

ExitMeaningEffect
0SuccessStdout (if JSON) parsed for decisions; otherwise no decision
2Blocking errorstderr fed back as error to Claude / action blocked (varies by event); stdout/JSON ignored
OtherNon-blocking errorstderr shown to user; execution continues

Special cases:

  • WorktreeCreate — any non-zero exit code aborts worktree creation
  • UserPromptSubmit — 30s default timeout
  • StopFailure — exit code and output always ignored

8. JSON output schema

All handler types emit the same JSON schema on stdout (or as HTTP 2xx body):

{
  "continue": true,
  "stopReason": "Build failed",
  "suppressOutput": false,
  "systemMessage": "Warning: Production environment",
  "terminalSequence": "\\033]777;notify;Title;Body\\007",
  
  "decision": "block",
  "reason": "Permission denied",
  
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes prohibited",
    "additionalContext": "Context injected for Claude",
    "updatedInput": { },
    "retry": true,
    "action": "accept",
    "content": { },
    "worktreePath": "/path/to/worktree"
  }
}

Field-by-field

FieldApplies toEffect
continue: falseall eventsStops all processing; takes precedence over event-specific decisions
stopReasonall eventsMessage shown to user when continue: false
suppressOutputall eventsHide hook stdout from transcript
systemMessageall eventsUser-visible warning (not shown to model)
terminalSequenceall eventsOSC terminal escape sequence (notifications) — allowlist below
decision: "block"UserPromptSubmit, UserPromptExpansion, PostToolUse, Stop, SubagentStop, PreCompact, ConfigChangeBlocks the action
reasonblocking eventsExplanation for block
hookSpecificOutput.hookEventNamealwaysRepeats event name (sanity check)
hookSpecificOutput.permissionDecisionPreToolUse, PermissionRequest"allow", "deny", "ask", "defer"
hookSpecificOutput.permissionDecisionReasonpermission eventsWhy
hookSpecificOutput.additionalContextSessionStart, Setup, UserPromptSubmit, UserPromptExpansion, PreToolUse, PostToolUse, SubagentStartInjected into Claude’s window (10,000 char limit; excess saved to file)
hookSpecificOutput.updatedInputPreToolUse, PermissionRequestModified tool input before execution
hookSpecificOutput.retryPermissionDeniedAllow model to retry the denied tool call
hookSpecificOutput.actionElicitation, ElicitationResult"accept", "decline", "cancel"
hookSpecificOutput.contentElicitation, ElicitationResultForm field values
hookSpecificOutput.worktreePathWorktreeCreateCreated worktree path

Allowed terminal escape sequences

Only these are passed through (anything else stripped for security):

  • OSC 0, 1, 2 — window/icon titles
  • OSC 9 — iTerm2, ConEmu, Windows Terminal, WezTerm notifications
  • OSC 99 — Kitty notifications
  • OSC 777 — urxvt, Ghostty, Warp notifications
  • BEL

Example notification hook (works in modern terminals):

#!/bin/bash
input=$(cat)
title="Claude Code"
body=$(jq -r '.message // "Needs attention"' <<<"$input")
seq=$(printf '\\033]777;notify;%s;%s\\007' "$title" "$body")
jq -nc --arg seq "$seq" '{terminalSequence: $seq}'

9. Environment variables available to hook processes

VariableValue
CLAUDE_PROJECT_DIRProject root path
CLAUDE_PLUGIN_ROOTPlugin root (plugin hooks only)
CLAUDE_PLUGIN_DATAPlugin data dir
CLAUDE_ENV_FILEPath to write export VAR=... statements (SessionStart, Setup, CwdChanged, FileChanged) — persists across the rest of the session
CLAUDE_CODE_REMOTE"true" in web env
CLAUDE_EFFORTCurrent effort level (tool-use contexts)

Mutating session env via CLAUDE_ENV_FILE

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

The env is set for subsequent Bash tool calls in the session. Only valid in SessionStart, Setup, CwdChanged, and FileChanged events.

10. Configuration locations

LocationScopeShared?
~/.claude/settings.json.hooksAll projectsNo
.claude/settings.json.hooksOne projectYes (repo)
.claude/settings.local.json.hooksOne projectNo (gitignored)
Managed policy settingsOrg-wideYes (admin)
<plugin>/hooks/hooks.jsonWhen plugin enabledYes (bundled)
Skill / subagent frontmatter hooks:Component activeYes (component file)

Admin can enforce managed hooks only via allowManagedHooksOnly: true — user/project/plugin hooks then ignored; managed hooks from enabledPlugins still respected.

11. Worked examples

Block destructive bash commands

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "if": "Bash(rm *)",
        "command": "./.claude/hooks/block-rm.sh"
      }]
    }]
  }
}

./.claude/hooks/block-rm.sh:

#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
 
if echo "$COMMAND" | grep -qE '\\brm\\s+-rf|\\brm\\s+--recursive'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive rm -rf blocked."
    }
  }'
else
  exit 0
fi

Auto-format on every Edit / Write

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write|MultiEdit",
      "hooks": [{
        "type": "command",
        "command": "./.claude/hooks/format.sh"
      }]
    }]
  }
}
#!/bin/bash
input=$(cat)
file=$(jq -r '.tool_input.file_path' <<<"$input")
case "$file" in
  *.py)  black "$file" 2>/dev/null ;;
  *.ts|*.tsx|*.js|*.jsx) prettier --write "$file" 2>/dev/null ;;
  *.go)  gofmt -w "$file" 2>/dev/null ;;
  *.rs)  rustfmt "$file" 2>/dev/null ;;
esac

Inject project context at session start

{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup|resume",
      "hooks": [{
        "type": "command",
        "command": "./.claude/hooks/inject-context.sh"
      }]
    }]
  }
}
#!/bin/bash
context=$(cat <<'EOF'
Current sprint: SP-204
Active feature flag: NEW_CHECKOUT_ENABLED=false
Production database is in maintenance mode until 17:00 UTC.
EOF
)
 
jq -n --arg ctx "$context" '{
  hookSpecificOutput: {
    hookEventName: "SessionStart",
    additionalContext: $ctx
  }
}'

HTTP hook to a local validator

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{
        "type": "http",
        "url": "http://localhost:9000/validate-edit",
        "timeout": 10
      }]
    }]
  }
}

Server returns:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "File contains syntax errors per local linter"
  }
}

MCP-tool hook

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "mcp_tool",
        "server": "security",
        "tool": "shell_audit",
        "input": {"command": "${tool_input.command}"}
      }]
    }]
  }
}

Notification on idle

{
  "hooks": {
    "Notification": [{
      "matcher": "idle_prompt",
      "hooks": [{
        "type": "command",
        "command": "./.claude/hooks/desktop-notify.sh"
      }]
    }]
  }
}

12. Security considerations

  1. Hooks run with project privileges. A malicious project’s .claude/settings.json can run arbitrary code on first open. Claude Code prompts for workspace trust before honoring project hooks.
  2. Managed hooks enforce policy. Admins use allowManagedHooksOnly + disableAllHooks immunity to lock down behavior.
  3. No /dev/tty access. Hooks run in own session — use terminalSequence for notifications, not direct terminal escapes.
  4. JSON injection defenses. If additionalContext is framed as system instructions, Claude’s prompt-injection defenses may flag it. Frame as factual statements.
  5. Path traversal. Always use ${CLAUDE_PROJECT_DIR} and validate paths.
  6. Env var interpolation in HTTP headers requires explicit allowedEnvVars allowlist — prevents accidental leakage of all env to remote endpoints.

13. Common pitfalls

  1. Forgetting jq -n for JSON output — outputting a plain string vs JSON misses the decision fields.
  2. Returning JSON from a non-zero exit: exit code 2 ignores stdout JSON — pick one path.
  3. additionalContext over 10,000 chars — gets saved to file instead of injected. Trim aggressively.
  4. Matcher regex confusion"Bash" is exact, not prefix. Use "^Bash" if you wanted regex anchors.
  5. Hook deduplication — identical handlers (same command+args or URL) run once per event. Add a no-op arg to force duplicates.
  6. async: true doesn’t block — fire-and-forget. Use asyncRewake: true if you want the hook to wake Claude on completion via exit 2.
  7. Plugin hooks ignore some fields. Per docs, plugin hooks don’t honor hooks, mcpServers, or permissionMode for security.
  8. Output cap at 10,000 chars for all output strings. Larger outputs saved to a file with a preview path provided.

14. Further reading