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 [[claude-ai/agent-teams
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
DirectoryAdded
A new working directory is registered mid-session via /add-dir or the SDK register_repo_root control request (v2.1.219+). Distinct from CwdChanged — fires when the session’s directory set expands, not when the active cwd changes
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
Common hook input fields
All hooks receive JSON via stdin. Fields present across nearly every event: session_id, transcript_path, cwd, permission_mode, hook_event_name. Tool-related hooks (PreToolUse, PostToolUse, PermissionRequest, etc.) additionally receive tool_name and tool_input. Individual events layer on further extras documented throughout the “Recent additions” sections below — agent_id/agent_type inside subagent turns, prompt_id, effort.level, and others.
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.
Per-event exit code 2 behavior
Event
Blocks?
Effect
PreToolUse
Yes
Blocks tool call
PermissionRequest
Yes
Denies permission
UserPromptSubmit
Yes
Blocks prompt, erases it
UserPromptExpansion
Yes
Blocks the expansion
Stop
Yes
Prevents Claude from stopping
SubagentStop
Yes
Prevents the subagent from stopping
TeammateIdle
Yes
Prevents the teammate from going idle
TaskCreated
Yes
Rolls back the task creation
TaskCompleted
Yes
Prevents the task from being marked as completed
ConfigChange
Yes
Blocks the configuration change (except policy_settings)
PostToolBatch
Yes
Stops the agentic loop before the next model call
PreCompact
Yes
Blocks compaction
Elicitation
Yes
Denies the elicitation
ElicitationResult
Yes
Blocks the response (action becomes decline)
WorktreeCreate
Yes
Any non-zero exit fails creation
PostToolUse
No
Shows stderr to Claude
PostToolUseFailure
No
Shows stderr to Claude
PermissionDenied
No
Ignored; use hookSpecificOutput.retry: true to let model retry
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 decision applies to UserPromptSubmit, PostToolUse, Stop, PreCompact.
Path return for WorktreeCreate (command prints path to stdout).
No decision control for SessionEnd, PostCompact, FileChanged, Notification.
Execution semantics
Default timeout: 60 seconds per hook, configurable per hook. The settings field for overriding it isn’t named in the source that surfaced this — flagged in Open Questions.
Parallel execution. When multiple hooks match the same event (e.g. two PostToolUse hooks with overlapping matchers, or the same event registered at user, project, and plugin scope), they run concurrently rather than sequentially.
Automatic deduplication. Identical hook commands registered more than once for the same event are deduplicated so they don’t fire twice.
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.
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.
Safeguard: direct edits to hook configuration files require review in the /hooks menu before they take effect — this prevents a plugin, skill, or other process from silently adding a hook to your configuration without your seeing it first. Hooks otherwise execute arbitrary shell commands at your own user permission level, so treat hook scripts like any other code you’d run locally: validate and sanitize stdin input, quote shell variables, use absolute paths, and avoid letting a hook touch .env or credential files unless that’s specifically its job.
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
Case sensitive — "bash" will not match the Bash tool.
Common patterns
Desktop notification when Claude needs input — Notification event + osascript (mac), notify-send (Linux), or PowerShell MessageBox (Windows).
Auto-format on edit — PostToolUse with matcher "Edit|Write", command pipes file path through jq to prettier --write.
Block edits to protected files — PreToolUse with permissionDecision: "deny" for paths matching .env*, secrets/**, etc.
Re-inject context after compaction — PostCompact reads CLAUDE.md and feeds it back as additionalContext.
Audit configuration changes — ConfigChange event logs all settings.json edits to a versioned audit log.
Reload env when files change — FileChanged watches .env / .envrc and re-sources them.
Auto-approve specific permissions — PermissionRequest returns behavior: "allow" for predictable cases.
Defer tool calls — permissionDecision: "defer" pauses execution for an external UI to make the decision (requires -p flag).
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.
Debugging
Every hook receives transcript_path, pointing to the session’s full JSONL history — no extra setup needed. Watch it live: tail -f /path/to/transcript.jsonl | jq.
Transcripts show what Claude did, not why your hook approved or blocked it. For hook-specific debugging, wrap the actual tool command in a small logging script that captures stdin, the firing event, and the exit code:
Reference it in the hook’s command field ahead of the real command: log-wrapper.sh your-tool-command.py.
Recent additions (v2.1.210–v2.1.212, July 2026)
New hook behavior from v2.1.210–v2.1.212, sourced from ai-research/claude-code-docs-changelog-2026-07-17.md:
Hook callback timeout was misreported as user rejection (v2.1.210 fix). A hook that timed out (exceeded its timeout window) was previously reporting back to the model as a “user rejection” of the tool call — this caused unattended sessions to stop and wait for human input when a hook simply ran too long. Now correctly reported as a timeout, not a user rejection. Relevant to hooks with async: false (synchronous/blocking) that wrap slow external calls.
continue: false hook output no longer dropped mid-stream (v2.1.211 fix). A PreToolUse hook returning {"continue": false} now reliably halts the agentic loop. Previously, if the hook’s output arrived between two streaming chunks of Claude’s response, the halt signal was silently ignored and the loop continued. This was a timing-dependent correctness bug — hooks whose continue: false seemed intermittently ineffective should be re-tested on v2.1.211+.
Auto mode no longer overrides PreToolUse hook ask decision (v2.1.211 fix). A PreToolUse hook returning permissionDecision: "ask" for unsandboxed Bash was previously being overridden by the auto-mode classifier’s own verdict — the hook asked but auto mode’s permission grant silently bypassed it. Now the hook’s ask decision floors the outcome at a human permission prompt regardless of auto mode’s classifier verdict. This restores the expected hook authority: if a hook says “ask a human,” auto mode cannot override that.
Recent additions (2026-07-12 watchlist sweep)
New hooks reference content as of this sweep, sourced from ai-research/watchlist-snapshots/code-claude-com-docs-en-hooks-2026-07-12.md:
agent_id and agent_type as common hook input fields (subagent context). Hook events now include agent_id (unique identifier for the subagent) and agent_type (agent name, e.g. "Explore", "security-reviewer", or plugin-scoped like "my-plugin:reviewer") in the JSON input when the hook fires inside a subagent turn (i.e., when Claude Code is running with --agent or as a spawned subagent). These fields are distinct from the per-event fields on agent-teams events (TeammateIdle, TaskCreated, TaskCompleted) which have their own schemas.
Recent additions (2026-07-05 watchlist sweep)
New hook-reference content in Claude Code as of v2.1.196–v2.1.199, sourced from ai-research/watchlist-snapshots/code-claude-com-docs-en-hooks-2026-07-05.md:
prompt_id in common hook input fields (v2.1.196+). All hook events now include a prompt_id UUID in the JSON input. The value identifies the user prompt currently being processed and matches the prompt.id attribute on OpenTelemetry events, enabling correlation between hook output and telemetry for a single prompt. Absent until first user input.
CLAUDE_CODE_BRIDGE_SESSION_ID in hook environment (v2.1.199+). Set to the Remote Control session ID while the local session has an active Remote Control connection; unset otherwise. Complements the existing $CLAUDE_CODE_REMOTE (set in web environments).
PostToolBatch in exit-code-2 table.PostToolBatch exit code 2 stops the agentic loop before the next model call — the first time this event has been documented with blocking semantics. The per-event table above now includes it alongside the other blocking events.
Additional events in exit-code-2 table. The table now explicitly covers: UserPromptExpansion (blocks expansion), SubagentStop (prevents subagent stopping), ConfigChange (blocks config change except policy_settings), Elicitation (denies), ElicitationResult (blocks response), PreCompact (blocks compaction), plus all non-blocking events that were previously implied but not enumerated.
Hyphen support in exact-match matchers documented (v2.1.195+). Hyphens are now in the exact-match character set. On earlier versions (<v2.1.195), a hyphenated matcher like code-reviewer is evaluated as a regex — anchor it as ^code-reviewer$ for pre-v2.1.195 compatibility.
Recent additions (2026-07-03, v2.1.199)
SessionStart, Setup, and SubagentStart hooks now surface stderr on code-2 exit. Previously, when these three hooks exited with code 2, their stderr output was silently discarded — the blocking signal fired but the error message was invisible. Fixed in v2.1.199: the error is now shown in the transcript, making hook failures diagnosable without adding extra logging. This is a silent regression fix affecting the three startup-phase events; PreToolUse, PostToolUse, and other events were already surfacing stderr correctly.
Recent additions (2026-06-28 watchlist sweep)
Per-event exit code 2 behavior table. The official hooks reference now documents exactly which events block vs. which ones don’t when a hook returns exit code 2. See the “Per-event exit code 2 behavior” table in the Exit codes section above. Key distinctions: UserPromptSubmit exit 2 erases the prompt (not just blocks it); WorktreeCreate fails on any non-zero exit (not just 2); PostToolUse and StopFailure are non-blocking (exit 2 shows stderr to Claude without stopping the turn). This was previously implied by individual event descriptions but not consolidated into a single table.
Recent additions (2026-06-14 watchlist sweep)
CLAUDE_ENV_FILE — persisting environment variables across the session. A CLAUDE_ENV_FILE path is now exposed in the hook environment for SessionStart, Setup, CwdChanged, and FileChanged hooks. Write export VAR=value lines 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 a SessionStart hook should set NODE_ENV, PATH entries, or secrets:
$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/).
$ARGUMENTS placeholder in prompt hooks. The type: "prompt" hook’s prompt field now supports a $ARGUMENTS placeholder that injects the hook event context (tool name, arguments, etc.) into the prompt text sent to Claude for evaluation.
updatedInput in PreToolUse and PermissionRequest hookSpecificOutput. Hooks on these events can rewrite the tool’s input before execution by returning updatedInput alongside (or instead of) a permissionDecision. Distinct from "deny" (which blocks the call) — updatedInput passes 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 hook command strings and MCP tool input fields. Previously scattered; now a named section.
Recent additions (v2.1.169, June 8 2026 — post-session hook)
New post-session lifecycle 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 from SessionEnd (which fires client-side at session close): post-session is 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)
terminalSequence JSON output field (v2.1.141) — see JSON output section above. Hooks can now emit desktop notifications, window title changes, and bell by returning terminalSequence in their JSON output. Replaces direct /dev/tty writes, 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/tty or send escape sequences directly to Claude Code’s terminal. Use systemMessage in JSON output to surface text; use terminalSequence for notifications/title/bell. Windows has no /dev/tty regardless 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: receives task_name + task_description (no task_id, no task_title)
TaskCompleted: receives task_id + task_name (no task_title, no task_description)
TeammateIdle: receives agent_type + idle_reason only (no agent_id)
Exit code 2 for TaskCreated/TaskCompleted rolls back the action; exit code 2 for TeammateIdle keeps 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: TaskCreated receives task_name + task_description (not task_id/task_title); TaskCompleted receives task_id + task_name; TeammateIdle receives agent_type + idle_reason only (no agent_id). See Lifecycle events table and agent-teams section above.
statusMessage JSON 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 (unlike systemMessage).
once field (skill hooks only) — "once": true in 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 from tool_input) that could contain shell metacharacters.
watchPaths in SessionStart input — the session start payload now includes the list of files registered for FileChanged watching, so SessionStart hooks can see which watchers are active.
initialUserMessage in SessionStart input — 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.
suppressOriginalPrompt in UserPromptSubmit hookSpecificOutput — set to true to prevent the user’s raw prompt from being forwarded to Claude; only the hook’s additionalContext is seen by the model. For privacy-filtering or prompt-rewriting workflows.
Recent additions (2026-06-09, events-table gap fix)
SessionEnd event 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 (-p mode exit), bypass_permissions_disabled, or other. Input adds a reason field alongside the common session_id/transcript_path/cwd fields. 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)
if condition fix for Read/Edit/Write tool paths. Hook if conditions 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-24, v2.1.191)
Comma-separated hook matchers now fire (v2.1.191 fix). Hooks configured with a comma-separated matcher list — e.g. "Bash,PowerShell" — were silently never firing before v2.1.191. The single-tool ("Bash"), alternation ("Edit|Write"), and regex ("mcp__memory__.*") matcher forms were unaffected; only the comma-joined list form was broken. If you relied on a multi-tool comma matcher and found the hook dead, update to v2.1.191 and re-test. See the Week 26 digest.
Recent additions (2026-06-04, v2.1.163)
Stop and SubagentStop hooks: hookSpecificOutput.additionalContext for feedback loops (v2.1.163) — Stop and SubagentStop hooks can now return hookSpecificOutput.additionalContext to 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, returns additionalContext: "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) — additionalContext is 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)
sessionTitle for SessionStart hookSpecificOutput — SessionStart hooks can now emit sessionTitle to set the session title (same effect as /rename). Applies only when source is "startup" or "resume"; ignored on "clear" and "compact". Read the existing session_title input field first to avoid overwriting a title the user set with --name. Distinct from UserPromptSubmit.sessionTitle (W15) — that fires on each prompt; this fires once at session startup.
reloadSkills for SessionStart hookSpecificOutput — Boolean. When true, 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 — The MessageDisplay hook (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_EFFORT environment variable (2026-05-10 sweep, W19, v2.1.128+) — hooks now receive the active effort level via two surfaces: (1) effort.level JSON input field in the hook’s stdin payload, alongside existing fields like tool_name and tool_input; (2) $CLAUDE_EFFORT environment 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_ID also now set in Bash subprocess environment (matching the session_id passed to hooks).
PostToolUseupdatedToolOutput for any tool (2026-05-10 sweep, W18) — PostToolUse hooks can replace tool output for any tool via hookSpecificOutput.updatedToolOutput, not only MCP tools. Enables post-processing of any tool’s output before Claude sees it.
Setup hook event (2026-05-03 sweep) — fires when Claude Code starts with --init-only, or when --init/--maintenance flags 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) and maintenance (fires on --maintenance). This event explains why --init and --maintenance are documented as “print mode only” in the CLI reference — they route through the Setup hook event, which only makes sense in non-interactive flows.
if field 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 for Bash(git commit *) instead of every Bash call. Already shown in Matchers above.
CwdChanged and FileChanged events (Week 13) — direnv-style setups (re-source .envrc on directory change, watch config files for hot-reload).
PermissionDenied event (Week 14) — fires when auto mode’s classifier denies a tool call. Return retry: true from the hook to let Claude try a different approach; /permissions → Recent retries manually with r.
defer exit-with-payload in -p mode (Week 14) — permissionDecision: "defer" in -p (print-mode / SDK) sessions pauses at the tool call and exits with a deferred_tool_use payload so an SDK app or custom UI can surface the decision; resume with --resume. The non--p defer 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.
disableSkillShellExecution setting (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 on UserPromptSubmit can set the session title via hookSpecificOutput.sessionTitle. Set programmatic titles based on the first user message instead of the auto-generated default.
Recent additions (2026-07-16 watchlist sweep)
v2.1.207 — Shell-form plugin hooks no longer substitute ${user_config.*}. Plugin hooks can reference ${user_config.*} values, but as of v2.1.207 this only substitutes in exec form (command + args), not shell form. A shell-form hook whose command references ${user_config.*} now fails with an error instead of running — read $CLAUDE_PLUGIN_OPTION_<KEY> (e.g. $CLAUDE_PLUGIN_OPTION_WEBHOOK_URL) from a shell-form hook instead, or switch to exec form. Before v2.1.207, shell-form hooks substituted these values directly — a shell-injection-shaped gap given shell form re-parses the whole command string. The same exec-form-only restriction shipped the same release for plugin monitor commands and MCP headersHelper (see Plugins and Marketplaces).
Key Takeaways
Hooks turn “Claude usually does X” into “X always happens” — the deterministic backbone for project rules, formatting, and security policy.
Hooks run with a 60-second default timeout (configurable per hook), execute in parallel when multiple match the same event, and identical commands are automatically deduplicated.
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 /hooks menu 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 decision for some, permissionDecision for PreToolUse, 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 /hooks in 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:
Security-Guidance Plugin — a production feature built entirely from hooks (SessionStart bootstrap, UserPromptSubmit baseline, PostToolUse on edits + git commit/push, Stop end-of-turn review).
Resolved 2026-07-16: default hook execution timeout is 60 seconds, configurable per hook (source: claude.com/blog/how-to-configure-hooks). The settings field name for overriding the default isn’t given by that source — still open if a reader needs the exact key.
How do hooks interact with --dangerously-skip-permissions? Does the mode override PreToolUse deny verdicts?
Order of evaluation when multiple hooks register for the same event from user, project, and plugin scopes — which wins on conflicting decision values? The 2026-07-16 blog post confirms same-event hooks run in parallel rather than sequentially, which reframes the question (it’s not “order” so much as “conflict resolution between concurrent results”) but doesn’t answer it.