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)

EventWhen it fires
SessionStartA session begins or resumes
SetupRuns with --init-only, or when --init/--maintenance are used in -p mode. For one-time CI/script preparation. Matcher values: init, maintenance
UserPromptSubmitUser submits a prompt, before Claude processes it
UserPromptExpansionA slash command expands; can block the expansion
PreToolUseBefore a tool call executes; can block, allow, deny, ask, or defer
PermissionRequestWhen a permission dialog appears
PermissionDeniedWhen a tool call is auto-denied; can allow retry
PostToolUseAfter a tool call succeeds
PostToolUseFailureAfter a tool call fails
PostToolBatchAfter parallel tool calls resolve
SubagentStart / SubagentStopSubagent spawn / finish
TaskCreated / TaskCompletedTasks created or marked complete (used by Agent Teams)
Stop / StopFailureClaude finishes or turn ends with API error
TeammateIdleAn Agent Teams teammate is going idle
InstructionsLoadedCLAUDE.md or .claude/rules files load
ConfigChangeConfiguration files change
CwdChangedWorking directory changes
FileChangedWatched files change on disk
WorktreeCreate / WorktreeRemoveWorktrees created or removed
PreCompact / PostCompactBefore / after context compaction
Elicitation / ElicitationResultMCP server input requests and responses
NotificationClaude Code sends a notification (waiting for input/permission)
MessageDisplayClaude Code is about to display an assistant message; hook can transform or suppress the text (v2.1.152+)
SessionEndSession terminates; matcher = termination reason. Cannot block — side effects only

Hook handler types

TypeWhat it does
commandRuns a shell script. Receives JSON on stdin, returns exit codes
httpPOSTs JSON to a URL
mcp_toolCalls a tool on a pre-connected MCP server
promptSends a prompt to Claude for evaluation (judgment-based gating)
agentSpawns a subagent for verification

Exit codes (command hooks)

  • Exit 0 — success; JSON output processed.
  • Exit 2blocking 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 decision applies to UserPromptSubmit, PostToolUse, Stop, PreCompact.
  • hookSpecificOutput.permissionDecision controls PreToolUse: allow / deny / ask / defer.
  • hookSpecificOutput.decision.behavior controls PermissionRequest: 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

FileScope
~/.claude/settings.jsonUser-level (global)
.claude/settings.jsonProject-level
.claude/settings.local.jsonProject, gitignored
Plugins, skills, agentsBundled 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 inputNotification event + osascript (mac), notify-send (Linux), or PowerShell MessageBox (Windows).
  • Auto-format on editPostToolUse with matcher "Edit|Write", command pipes file path through jq to prettier --write.
  • Block edits to protected filesPreToolUse with permissionDecision: "deny" for paths matching .env*, secrets/**, etc.
  • Re-inject context after compactionPostCompact reads CLAUDE.md and feeds it back as additionalContext.
  • Audit configuration changesConfigChange event logs all settings.json edits to a versioned audit log.
  • Reload env when files changeFileChanged watches .env / .envrc and re-sources them.
  • Auto-approve specific permissionsPermissionRequest returns behavior: "allow" for predictable cases.
  • Defer tool callspermissionDecision: "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.

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:
    #!/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/).
  • $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-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).
  • PostToolUse updatedToolOutput 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.

Recent additions (Weeks 13–15, 2026)

Tracked from the Week 13 / Week 14 / Week 15 release digests:

  • 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.

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 /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

  1. 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\"'"}]
          }
        ]
      }
    }
  2. Run /hooks in any session to confirm it registered.
  3. Ask Claude to do something requiring permission, switch tabs — desktop notification fires.
  4. 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"}]}
        ]
      }
    }
  5. For higher-stakes blocking, write a PreToolUse hook that exits 2 when tool_input.file_path starts with .env or matches secrets/.

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 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?