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) |
MessageDisplay | Claude Code is about to display an assistant message; hook can transform or suppress the text (v2.1.152+) |
SessionEnd | Session terminates; matcher = termination reason. Cannot block — side effects only |
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",
"terminalSequence": "\033]9;Notification text\007",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"additionalContext": "string"
}
}terminalSequence (v2.1.141+) — a terminal escape sequence for Claude Code to emit on your behalf: desktop notification, window title, or bell. Restricted to OSC 0/1/2/9/99/777 and BEL. Race-free and works inside tmux, GNU screen, and on Windows. Use this instead of writing to /dev/tty, which is unavailable to command hooks as of v2.1.139. Quick invocation: jq -nc --arg seq "$seq" '{terminalSequence: $seq}'
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 (2026-06-14 watchlist sweep)
CLAUDE_ENV_FILE— persisting environment variables across the session. ACLAUDE_ENV_FILEpath is now exposed in the hook environment forSessionStart,Setup,CwdChanged, andFileChangedhooks. Writeexport VAR=valuelines to this file to make environment variables available for the rest of the session — including to Claude’s Bash tool. This is the documented pattern for direnv-style setups where aSessionStarthook should setNODE_ENV, PATH entries, or secrets:#!/bin/bash if [ -n "$CLAUDE_ENV_FILE" ]; then echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE" echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE" fi$CLAUDE_CODE_REMOTE— detect web environment. Set to"true"when Claude Code is running in the web environment (cloud session on claude.ai). Hooks can branch on this to skip operations that require local filesystem access (e.g., writing to~/.local/).$ARGUMENTSplaceholder in prompt hooks. Thetype: "prompt"hook’spromptfield now supports a$ARGUMENTSplaceholder that injects the hook event context (tool name, arguments, etc.) into the prompt text sent to Claude for evaluation.updatedInputin PreToolUse and PermissionRequest hookSpecificOutput. Hooks on these events can rewrite the tool’s input before execution by returningupdatedInputalongside (or instead of) apermissionDecision. Distinct from"deny"(which blocks the call) —updatedInputpasses through with modified parameters. Use case: sanitize or redirect file paths, cap dangerous argument ranges, or inject audit metadata transparently.- Path placeholders formalized. The reference now lists
${CLAUDE_PROJECT_DIR},${CLAUDE_PLUGIN_ROOT}, and${CLAUDE_PLUGIN_DATA}as a distinct “Path placeholders” group that auto-substitute in hookcommandstrings and MCP toolinputfields. Previously scattered; now a named section.
Recent additions (v2.1.169, June 8 2026 — post-session hook)
- New
post-sessionlifecycle hook (self-hosted runner). Runs after the session ends and before the workspace is deleted — the place to snapshot uncommitted work or export logs on a self-hosted runner. The child-process SIGTERM→SIGKILL window is now configurable (default unchanged at 5s). Distinct fromSessionEnd(which fires client-side at session close):post-sessionis a runner-side teardown hook on the self-hosted runner. See the Week 25 digest.
Recent additions (v2.1.143, May 15 2026)
- Stop hook block cap (v2.1.143) — a Stop hook that blocks repeatedly (exit 2 every turn) will now cause Claude to end the turn with a warning after 8 consecutive blocks. Override the default with
CLAUDE_CODE_STOP_HOOK_BLOCK_CAP=<N>. Prevents runaway loops where a misconfigured Stop hook locks the session indefinitely.
Recent additions (2026-05-17 watchlist sweep)
terminalSequenceJSON output field (v2.1.141) — see JSON output section above. Hooks can now emit desktop notifications, window title changes, and bell by returningterminalSequencein their JSON output. Replaces direct/dev/ttywrites, which are unavailable to hooks as of v2.1.139.- Command hooks run without controlling terminal (v2.1.139) — as of v2.1.139 on macOS and Linux, command hooks run in their own session without a controlling terminal. Neither the hook process nor any child process can open
/dev/ttyor send escape sequences directly to Claude Code’s terminal. UsesystemMessagein JSON output to surface text; useterminalSequencefor notifications/title/bell. Windows has no/dev/ttyregardless of version. - Agent team hook input schemas — per-event extra fields (corrected in 2026-05-24 sweep — the 2026-05-17 snapshot had wrong field names):
TaskCreated: receivestask_name+task_description(notask_id, notask_title)TaskCompleted: receivestask_id+task_name(notask_title, notask_description)TeammateIdle: receivesagent_type+idle_reasononly (noagent_id)- Exit code 2 for
TaskCreated/TaskCompletedrolls back the action; exit code 2 forTeammateIdlekeeps the teammate working.{"continue": false, "stopReason": "..."}stops the teammate entirely (matching Stop hook semantics).
Recent additions (2026-05-24 watchlist sweep)
- Field name corrections for agent team hook events — the 2026-05-17 snapshot had incorrect field names. Corrected values:
TaskCreatedreceivestask_name+task_description(nottask_id/task_title);TaskCompletedreceivestask_id+task_name;TeammateIdlereceivesagent_type+idle_reasononly (noagent_id). See Lifecycle events table and agent-teams section above. statusMessageJSON output field — hooks can return{"statusMessage": "..."}to set the spinner/status-line text visible in the TUI during processing. UI-only; not injected into Claude’s context (unlikesystemMessage).oncefield (skill hooks only) —"once": truein a skill-bundled hook causes it to fire only on the first occurrence of the event per session, not every time. Useful for session-initialization hooks.- Exec form for command hooks —
"command": ["cmd", "arg1", "arg2"]array syntax bypasses shell interpolation. Recommended when hook arguments contain external data (e.g., file paths fromtool_input) that could contain shell metacharacters. watchPathsinSessionStartinput — the session start payload now includes the list of files registered forFileChangedwatching, so SessionStart hooks can see which watchers are active.initialUserMessageinSessionStartinput — the session start payload now includes the first user message (or empty string). Useful for session-title hooks that generate a title from the initial prompt.suppressOriginalPromptinUserPromptSubmithookSpecificOutput — set totrueto prevent the user’s raw prompt from being forwarded to Claude; only the hook’sadditionalContextis seen by the model. For privacy-filtering or prompt-rewriting workflows.
Recent additions (2026-06-09, events-table gap fix)
SessionEndevent documented — fires when a session terminates. The matcher value is the termination reason:clear(/clear),resume(session saved via--resume/--continue//resume),logout,prompt_input_exit(-pmode exit),bypass_permissions_disabled, orother. Input adds areasonfield alongside the commonsession_id/transcript_path/cwdfields. SessionEnd cannot block — the exit code is ignored (on exit 2, stderr is shown to the user only); use it purely for side effects: session logging, cleanup, analytics, saving state. Source:ai-research/claude-code-docs-hooks-sessionend-2026-06-09.md.
Recent additions (2026-06-12, v2.1.176)
ifcondition fix for Read/Edit/Write tool paths. Hookifconditions using permission rule syntax on file-path tools — e.g.Edit(src/**),Read(~/.ssh/**),Read(.env)— now match correctly. Prior to v2.1.176, these patterns silently failed on the three file-path tools (Read, Edit, Write), meaning hooks gated on file paths were not firing as documented. If you wrote path-gated hooks on Read/Edit/Write and found them unreliable, update to v2.1.176. See the Week 26 digest.
Recent additions (2026-06-04, v2.1.163)
- Stop and SubagentStop hooks:
hookSpecificOutput.additionalContextfor feedback loops (v2.1.163) — Stop and SubagentStop hooks can now returnhookSpecificOutput.additionalContextto inject feedback into Claude’s turn and keep it going, without being labeled a “hook error.” Previously, exit code 2 from a Stop hook blocked the turn but the error label was misleading for feedback use cases. Example pattern: a Stop hook that checks whether the output compiles and, if not, returnsadditionalContext: "your last diff doesn't compile — fix it before finishing"to drive a correction pass. This is distinct from the block-cap behavior (8 consecutive blocks triggers a forced exit, v2.1.143) —additionalContextis for cooperative feedback, not veto. Sources:raw/anthropic-watch-claude-code-tag-v2-1-163.md;ai-research/claude-code-docs-changelog-2026-06-05.md.
Recent additions (2026-05-31 watchlist sweep)
sessionTitleforSessionStarthookSpecificOutput — SessionStart hooks can now emitsessionTitleto set the session title (same effect as/rename). Applies only whensourceis"startup"or"resume"; ignored on"clear"and"compact". Read the existingsession_titleinput field first to avoid overwriting a title the user set with--name. Distinct fromUserPromptSubmit.sessionTitle(W15) — that fires on each prompt; this fires once at session startup.reloadSkillsforSessionStarthookSpecificOutput — Boolean. Whentrue, Claude Code re-scans skill and command directories after all SessionStart hooks finish, making skills the hook installed available from the first prompt. Without it, files the hook writes into~/.claude/skills/only appear in the next session (skill discovery runs before SessionStart hooks complete). Example:echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "reloadSkills": true}}'MessageDisplay.displayContent— TheMessageDisplayhook (v2.1.152+, already in the events table above) can return{"hookSpecificOutput": {"hookEventName": "MessageDisplay", "displayContent": "..."}}to replace the text shown on screen. Display-only: the transcript and what Claude sees internally keep the original text. No matcher support — fires on every assistant message. Use for terminal markup, output filtering, or rendered syntax highlighting.
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. - Week 21 release digest —
terminalSequencehook output (v2.1.141),CLAUDE_CODE_STOP_HOOK_BLOCK_CAPstop hook loop cap (v2.1.143). - 23 release digest —
MessageDisplayhook (v2.1.152), SessionStartsessionTitle/reloadSkillshookSpecificOutput fields. - Week 24 release digest — Stop/SubagentStop
hookSpecificOutput.additionalContextfeedback loop (v2.1.163). - Security-Guidance Plugin — a production feature built entirely from hooks (
SessionStartbootstrap,UserPromptSubmitbaseline,PostToolUseon edits + git commit/push,Stopend-of-turn review).
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?