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)

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",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "additionalContext": "string"
  }
}

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