Source: Anthropic official docs — Hooks guide + Anthropic official docs — Hooks reference Type: Product Feature Product: Claude Code
Hooks are user-defined shell commands, HTTP endpoints, MCP tool calls, or LLM prompts that execute at specific points in Claude Code’s lifecycle. They give deterministic control — actions always fire instead of depending on the LLM choosing to run them. Use hooks to enforce project rules (block edits to protected files), automate repetitive work (auto-format on save), inject context (re-load CLAUDE.md after compaction), or wire Claude Code into existing tools (audit logs, notifications, CI).
Lifecycle events (when hooks fire)
| Event | When it fires |
|---|---|
SessionStart | A session begins or resumes |
Setup | Runs with --init-only, or when --init/--maintenance are used in -p mode. For one-time CI/script preparation. Matcher values: init, maintenance |
UserPromptSubmit | User submits a prompt, before Claude processes it |
UserPromptExpansion | A slash command expands; can block the expansion |
PreToolUse | Before a tool call executes; can block, allow, deny, ask, or defer |
PermissionRequest | When a permission dialog appears |
PermissionDenied | When a tool call is auto-denied; can allow retry |
PostToolUse | After a tool call succeeds |
PostToolUseFailure | After a tool call fails |
PostToolBatch | After parallel tool calls resolve |
SubagentStart / SubagentStop | Subagent spawn / finish |
TaskCreated / TaskCompleted | Tasks created or marked complete (used by Agent Teams) |
Stop / StopFailure | Claude finishes or turn ends with API error |
TeammateIdle | An Agent Teams teammate is going idle |
InstructionsLoaded | CLAUDE.md or .claude/rules files load |
ConfigChange | Configuration files change |
CwdChanged | Working directory changes |
FileChanged | Watched files change on disk |
WorktreeCreate / WorktreeRemove | Worktrees created or removed |
PreCompact / PostCompact | Before / after context compaction |
Elicitation / ElicitationResult | MCP server input requests and responses |
Notification | Claude Code sends a notification (waiting for input/permission) |
Hook handler types
| Type | What it does |
|---|---|
command | Runs a shell script. Receives JSON on stdin, returns exit codes |
http | POSTs JSON to a URL |
mcp_tool | Calls a tool on a pre-connected MCP server |
prompt | Sends a prompt to Claude for evaluation (judgment-based gating) |
agent | Spawns a subagent for verification |
Exit codes (command hooks)
- Exit 0 — success; JSON output processed.
- Exit 2 — blocking error; stderr is fed back to Claude as feedback. The pending tool call (or task creation/completion) is blocked.
- Other codes — non-blocking error; execution continues.
JSON output
{
"continue": true,
"decision": "block",
"reason": "explanation",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"additionalContext": "string"
}
}Decision control varies by event:
- Top-level
decisionapplies toUserPromptSubmit,PostToolUse,Stop,PreCompact. hookSpecificOutput.permissionDecisioncontrolsPreToolUse:allow/deny/ask/defer.hookSpecificOutput.decision.behaviorcontrolsPermissionRequest:allow/deny.- Path return for
WorktreeCreate(command prints path to stdout). - No decision control for
SessionEnd,PostCompact,FileChanged,Notification.
Async hooks
Command hooks support background execution:
"async": true— runs without blocking; output discarded."asyncRewake": true— runs in background, but if it exits with code 2, Claude is woken with stderr/stdout shown as a system reminder. Useful for long-running checks that surface findings later without blocking the current turn.
MCP tool hooks
Call a pre-connected MCP server’s tool as a hook:
{
"type": "mcp_tool",
"server": "server_name",
"tool": "tool_name",
"input": { "file_path": "${tool_input.file_path}" }
}${path} substitution interpolates fields from the hook input.
HTTP hooks
POST JSON to a URL; response uses the same schema as command JSON output. Non-2xx responses are non-blocking errors (unlike command exit codes).
{
"type": "http",
"url": "http://localhost:8080/hooks",
"headers": { "Authorization": "Bearer $TOKEN" },
"allowedEnvVars": ["TOKEN"]
}Configure hook location
| File | Scope |
|---|---|
~/.claude/settings.json | User-level (global) |
.claude/settings.json | Project-level |
.claude/settings.local.json | Project, gitignored |
| Plugins, skills, agents | Bundled with each |
The /hooks slash command opens a read-only browser listing all configured hooks, the matching event, matcher, type, and source file. Use it to verify configuration; edit the JSON to make changes.
Matchers
Filter hooks by tool name, event type, or regex:
"Bash"— match a single tool"Edit|Write"— match either of two tools"mcp__memory__.*"— regex match against MCP tools"if": "Bash(rm *)"— further filter using permission rule syntax
Common patterns
- Desktop notification when Claude needs input —
Notificationevent +osascript(mac),notify-send(Linux), or PowerShell MessageBox (Windows). - Auto-format on edit —
PostToolUsewith matcher"Edit|Write", command pipes file path throughjqtoprettier --write. - Block edits to protected files —
PreToolUsewithpermissionDecision: "deny"for paths matching.env*,secrets/**, etc. - Re-inject context after compaction —
PostCompactreads CLAUDE.md and feeds it back asadditionalContext. - Audit configuration changes —
ConfigChangeevent logs all settings.json edits to a versioned audit log. - Reload env when files change —
FileChangedwatches.env/.envrcand re-sources them. - Auto-approve specific permissions —
PermissionRequestreturnsbehavior: "allow"for predictable cases. - Defer tool calls —
permissionDecision: "defer"pauses execution for an external UI to make the decision (requires-pflag).
Prompt-based and agent-based hooks
For decisions requiring judgment rather than deterministic rules:
type: "prompt"— sends a prompt to a Claude model, returns its decision.type: "agent"— spawns a subagent for verification or analysis before allowing the action through.
These are slower and more expensive than command/HTTP hooks but handle ambiguous cases (e.g., “is this commit message acceptable?”) that pattern-matching cannot.
Permission relay & defer
Hooks can update permission rules dynamically — add or remove rules, change permission modes, modify tool input. Combined with permissionDecision: "defer", this lets external systems (e.g., a Channel forwarding to a phone) make the call.
Recent additions (May 2026 watchlist sweep)
$CLAUDE_EFFORTenvironment variable (2026-05-10 sweep, W19, v2.1.128+) — hooks now receive the active effort level via two surfaces: (1)effort.levelJSON input field in the hook’s stdin payload, alongside existing fields liketool_nameandtool_input; (2)$CLAUDE_EFFORTenvironment variable set in the hook’s process environment and also available in Bash tool subprocess commands. Values:low,medium,high,xhigh,max. Practical use: gate expensive hook logic behind effort level, or include the level in audit logs.$CLAUDE_CODE_SESSION_IDalso now set in Bash subprocess environment (matching thesession_idpassed to hooks).PostToolUseupdatedToolOutputfor any tool (2026-05-10 sweep, W18) —PostToolUsehooks can replace tool output for any tool viahookSpecificOutput.updatedToolOutput, not only MCP tools. Enables post-processing of any tool’s output before Claude sees it.Setuphook event (2026-05-03 sweep) — fires when Claude Code starts with--init-only, or when--init/--maintenanceflags are used in-p(print) mode. Intended for one-time CI/script preparation that should not repeat every session. Matcher values:init(fires on--init) andmaintenance(fires on--maintenance). This event explains why--initand--maintenanceare documented as “print mode only” in the CLI reference — they route through theSetuphook event, which only makes sense in non-interactive flows.
Recent additions (Weeks 13–15, 2026)
Tracked from the Week 13 / Week 14 / Week 15 release digests:
iffield on individual hooks (v2.1.85, Week 13) — uses permission rule syntax to scope a hook to a specific call pattern. Pre-commit linter only spawns forBash(git commit *)instead of every Bash call. Already shown in Matchers above.CwdChangedandFileChangedevents (Week 13) — direnv-style setups (re-source.envrcon directory change, watch config files for hot-reload).PermissionDeniedevent (Week 14) — fires when auto mode’s classifier denies a tool call. Returnretry: truefrom the hook to let Claude try a different approach;/permissions→ Recent retries manually withr.deferexit-with-payload in-pmode (Week 14) —permissionDecision: "defer"in-p(print-mode / SDK) sessions pauses at the tool call and exits with adeferred_tool_usepayload so an SDK app or custom UI can surface the decision; resume with--resume. The non--pdefer path still routes through normal permission UI.- Hook output > 50K → disk (Week 14) — large stdout/stderr from a hook is saved to disk with a path + preview instead of injected into context. Prevents one chatty hook from blowing up your conversation budget.
disableSkillShellExecutionsetting (Week 14) — top-level lockdown switch that blocks inline shell from skills, slash commands, and plugin commands. Useful for tightly-scoped enterprise environments where only explicitly-configured hooks should ever run shells.UserPromptSubmit.sessionTitle(Week 15) — hooks onUserPromptSubmitcan set the session title viahookSpecificOutput.sessionTitle. Set programmatic titles based on the first user message instead of the auto-generated default.
Key Takeaways
- Hooks turn “Claude usually does X” into “X always happens” — the deterministic backbone for project rules, formatting, and security policy.
- Five handler types cover the spectrum from deterministic (
command,http,mcp_tool) to judgment-based (prompt,agent). - Exit code 2 is the blocking signal: stderr is fed to Claude as feedback. Any other non-zero exit is non-blocking.
- The
/hooksmenu is read-only — edit settings JSON to add or remove hooks. - Async hooks (
async,asyncRewake) decouple long-running checks from the current turn while still surfacing findings. - Decision control depends on event: top-level
decisionfor some,permissionDecisionforPreToolUse, no control at all for terminal events. - Hooks are layered — user, project, project-local (gitignored), plus plugins/skills/agents — and all fire when their event matches.
Try It
- Notification on idle — paste this into
~/.claude/settings.json(mac):{ "hooks": { "Notification": [ { "matcher": "", "hooks": [{"type": "command", "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"}] } ] } } - Run
/hooksin any session to confirm it registered. - Ask Claude to do something requiring permission, switch tabs — desktop notification fires.
- Add an auto-format hook to a project’s
.claude/settings.json:{ "hooks": { "PostToolUse": [ {"matcher": "Edit|Write", "hooks": [{"type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"}]} ] } } - For higher-stakes blocking, write a
PreToolUsehook that exits 2 whentool_input.file_pathstarts with.envor matchessecrets/.
Related
- Claude Code Agent Teams — uses
TeammateIdle,TaskCreated,TaskCompletedhooks for quality gates. - Claude Code Scheduled Tasks —
/loopand cron complement hooks (timed actions vs event-driven actions). - Claude Code Channels — push external events into a session; pairs with
permissionDecision: "defer"for remote approvals. - Claude Code Subagents —
agent-type hooks spawn subagents for verification. - Agent Skills Overview — skills can ship hooks bundled with them.
- Claude Code Plugins and Marketplaces — plugins can register hooks alongside commands and MCP servers.
- Skills vs MCP vs Plugins — When to Use Which — hooks fit into the broader extensibility picture.
- Essential MCP Servers for 2026 — MCP servers can be invoked as
mcp_toolhooks. - Week 13 release digest —
iffield,CwdChanged/FileChanged. - Week 14 release digest —
PermissionDenied,deferpayload,disableSkillShellExecution, output >50K to disk. - Week 15 release digest —
UserPromptSubmit.sessionTitle.
Open Questions
- What is the per-event hook execution timeout? Doc names “long-running” hooks but doesn’t specify the limit at which Claude Code kills them.
- How do hooks interact with
--dangerously-skip-permissions? Does the mode overridePreToolUsedeny verdicts? - Order of evaluation when multiple hooks register for the same event from user, project, and plugin scopes — which wins on conflicting
decisionvalues?