3.6 Hooks System: User Scripts Intervening in AI Decision Flow

Source location: src/utils/hooks/ (17 files) Configuration: hooks field in settings.json or .claude/settings.json


One-sentence understanding

Hooks let you insert your own scripts into Claude's workflow — before/after tool execution, before Claude starts replying, when a session starts, and more.

User-script hooks ≈ OS hooks / browser extensions / framework middleware

What Hooks can do

ScenarioHook event used
Block Claude from running dangerous bash commandsPreToolUse → exit code 2
Log all file changes to audit logPostToolUse
Filter sensitive words whenever user submits promptUserPromptSubmit
Send system notification when Claude finishes responseStop
Inject project context at session startSessionStart
Append custom summary instructions before /compactPreCompact
Auto-monitor .env file changesFileChanged

Full Hook event list (27 events)

Tool-related

EventTrigger timingInputEffect of exit code 2
PreToolUsebefore tool executionJSON: tool argsblock tool execution, pass error to Claude
PostToolUseafter tool executionJSON: {inputs, response}pass error to Claude immediately
PostToolUseFailurewhen tool failsJSON: {tool_name, error, error_type, is_interrupt, is_timeout}pass error to Claude immediately
PermissionDeniedtool call denied by Auto-mode classifierJSON: {tool_name, tool_input, reason}returning {"hookSpecificOutput":{"retry":true}} lets Claude retry
PermissionRequestwhen permission popup is shownJSON: {tool_name, tool_input, tool_use_id}can allow/deny via hookSpecificOutput

Conversation-related

EventTrigger timingEffect of exit code 2
UserPromptSubmitwhen user submits promptblock processing, clear prompt, show error to user
Stopbefore Claude finishes responsepass error to Claude, continue conversation
StopFailureturn ends due to API errorignored (fire-and-forget)

Session lifecycle

EventTrigger timingsource in input
SessionStartnew session startsstartup / resume / clear / compact
SessionEndsession endsclear / logout / prompt_input_exit / other

Compaction-related

EventEffect of exit code 2
PreCompactblock compaction (stdout appended as custom compaction instruction)
PostCompactoutput shown to user

Subagent-related

EventDescription
SubagentStartAgentTool subagent starts (stdout passed to subagent)
SubagentStopbefore subagent completes response (exit code 2 keeps subagent running)

Workspace and files

EventDescription
CwdChangedworking directory changed (can update env vars via CLAUDE_ENV_FILE)
FileChangedwatched file changed (requires watchPaths returned via CwdChanged)
WorktreeCreaterequest to create Worktree (stdout returns worktree path)
WorktreeRemoverequest to remove Worktree

Multi-user collaboration (KAIROS only)

EventDescription
TeammateIdleteammate about to become idle (exit code 2 blocks idle state)
TaskCreatedtask created (exit code 2 blocks creation)
TaskCompletedtask marked completed (exit code 2 blocks completion)

MCP-related

EventDescription
ElicitationMCP server requests user input (can auto-reply via hookSpecificOutput)
ElicitationResultafter user answers MCP request (can override user response)

Config and settings

EventDescription
Setuptriggered by init or maintenance, for repo bootstrap/maintenance
ConfigChangeruntime config file changed (exit code 2 blocks config activation)
InstructionsLoadedCLAUDE.md loaded (observation only, cannot block)
Notificationwhen notifications are sent (permission_prompt / idle_prompt, etc.)

Three Hook execution modes

1. Shell command (most common)

// ~/.claude/settings.json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "jq -e '.command | test(\"^rm\")' && echo '禁止删除文件' >&2 && exit 2 || exit 0"
      }]
    }]
  }
}

2. HTTP Hook

{
  "hooks": {
    "PostToolUse": [{
      "hooks": [{
        "type": "http",
        "url": "https://my-audit-server.com/hook",
        "method": "POST"
      }]
    }]
  }
}

3. Agent Hook (delegate handling to Claude)

{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "agent",
        "prompt": "请检查刚刚完成的工作是否符合项目规范"
      }]
    }]
  }
}

Exit code rules (general)

Exit codeEffect
0success; stdout routed by event rules (to Claude for some events, to user for others)
2block/influence Claude: stderr sent to model (or triggers special behavior)
othersstderr shown to user only, does not affect Claude

Exit code 2 is "for Claude", while other non-zero codes are "for the user". This allows hooks to both inform users and control Claude behavior.


Hook source (HookSource)

type HookSource =
  | 'user_settings'      // ~/.claude/settings.json
  | 'project_settings'   // .claude/settings.json
  | 'local_settings'     // .claude/settings.local.json
  | 'policySettings'     // enterprise policy (read-only)
  | 'pluginHook'         // registered by MCP plugin
  | 'sessionHook'        // temporarily registered at runtime
  | 'builtinHook'        // built-in Claude Code hooks (compaction-related)

Priority: policy > project > local > user > session > builtin


Matcher: fine-grained matching

Use matcher to trigger hooks only under specific conditions:

// Intercept Bash tool only
{"matcher": "Bash", "hooks": [...]}

// Regex: intercept file-writing tools only
{"matcher": "^(str_replace|create_file|write_file)", "hooks": [...]}

// Match specific notification type
{"matcher": "permission_prompt", "hooks": [...]}

AsyncHookRegistry: concurrency management

AsyncHookRegistry.ts manages concurrently running hook processes:

// All hook event types can run multiple processes concurrently
// Used to track in-progress hooks (for UI progress display)
// Force-abort timed-out hooks (prevent hook from hanging the whole flow)

ssrfGuard: security protection for HTTP Hooks

When HTTP hooks call external URLs, ssrfGuard.ts prevents SSRF (server-side request forgery):

// Check whether URL points to private/internal addresses
// Reject 127.0.0.1, 169.254.x.x (cloud metadata), 10.x.x.x, etc.
// Prevent attacker from abusing hooks to access internal services through Claude

Next