Skip to content

🌐 Language: English | 中文版 → 📖 Read Online → — Sidebar nav, dark mode & full-text search. Better than raw GitHub.

Hook System: 20 Event Types, Full Lifecycle Interception

Source files: utils/hooks.ts (5,023 lines), utils/hooks/ directory, hooks/ top-level (17,931 lines)

TL;DR

Claude Code has a 20-event hook system that lets users and plugins intercept nearly every action — from tool execution to session lifecycle to permission decisions. Hooks can approve, deny, modify inputs, inject context, or stop the entire session. The main implementation file (hooks.ts) is 5,023 lines — larger than most entire applications.


1. The 20 Hook Events

05 hook system 1

Most Important Hook: PreToolUse

PreToolUse fires before every tool execution. It can:

ActionJSON OutputEffect
Allow{ "permissionDecision": "allow" }Skip permission prompt, auto-approve
Deny{ "permissionDecision": "deny" }Block the tool, return error to model
Ask{ "permissionDecision": "ask" }Force permission prompt
Modify input{ "updatedInput": {...} }Change tool arguments before execution
Stop session{ "continue": false }Halt the entire agent loop

2. Hook Execution Architecture

Hook Types

Hooks can be implemented in three ways:

typescript
// 源码位置: src/utils/hooks.ts:45-55
// 1. Command hooks — shell scripts
{ "type": "command", "command": "python validate.py" }

// 2. HTTP hooks — webhook endpoints
{ "type": "http", "url": "https://api.example.com/hook" }

// 3. Agent hooks — Claude sub-agents as hooks
{ "type": "agent", "agent": "security-reviewer" }

// 4. Function hooks — in-process JavaScript (SDK only)
{ "type": "function", "callback": async (input) => {...} }

Execution Flow

05 hook system 2

Exit Code Convention

Exit CodeMeaning
0Success — proceed normally
1Non-blocking error — log and continue
2Blocking error — stop the tool
OtherTreated as non-blocking error

3. Hook Matching

Hooks declare which events and tools they intercept:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git *)",
        "command": "python validate_git.py"
      },
      {
        "matcher": "FileWrite",
        "command": "node check_readonly.js"
      }
    ]
  }
}

The matcher supports:

  • Tool name only: "Bash" — matches all Bash commands
  • Tool + pattern: "Bash(git *)" — matches Bash commands starting with git
  • Wildcard: "*" — matches all tools

Permission Matcher Pipeline

typescript
// 源码位置: src/Tool.ts:180-185
// Each tool implements preparePermissionMatcher()
// This creates a closure that tests patterns against tool input
tool.preparePermissionMatcher(input)
// Returns: (pattern: string) => boolean
// e.g., for Bash: checks if command matches "git *"

4. Hook JSON Protocol

Hooks communicate via a structured JSON protocol:

Input (stdin / POST body)

json
{
  "session_id": "uuid",
  "transcript_path": "/path/to/transcript.json",
  "cwd": "/project",
  "tool_name": "Bash",
  "tool_input": { "command": "git push" },
  "permission_mode": "default",
  "agent_id": "optional-agent-id"
}

Output (stdout / response body)

json
{
  "continue": true,
  "decision": "approve",
  "reason": "Git operations are safe",
  "systemMessage": "Optional message to inject",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": { "command": "git push --no-verify" },
    "additionalContext": "Note: --no-verify was added"
  }
}

The output schema is validated with Zod (hookJSONOutputSchema). Invalid JSON is treated as plain text.


5. Workspace Trust Security

A critical security mechanism prevents hooks from running in untrusted workspaces:

typescript
// 源码位置: src/utils/hooks.ts:4850-4860
export function shouldSkipHookDueToTrust(): boolean {
  const isInteractive = !getIsNonInteractiveSession()
  if (!isInteractive) return false  // SDK mode: trust is implicit

  // ALL hooks require workspace trust in interactive mode
  return !checkHasTrustDialogAccepted()
}

This prevents a malicious .claude/settings.json in a cloned repo from executing arbitrary commands when a user opens the project.


6. Async Hook Execution

Hooks can run asynchronously in the background:

typescript
// 源码位置: src/utils/hooks.ts:4920-4935
// AsyncHookJSONOutput declares the hook wants background execution
{ "async": true, "asyncRewake": true }

// The hook is registered and continues running
registerPendingAsyncHook({
  processId, hookId, shellCommand, ...
})

// When it completes, it can re-wake the model
// via task-notification message

asyncRewake hooks can inject results back into the conversation after completion — even if the model has moved on to other tasks.


7. Hook Aggregation

When multiple hooks match the same event, their results are aggregated:

typescript
// 源码位置: src/utils/hooks.ts:4980-4995
// Aggregation rules:
// - Any "deny" → deny (most restrictive wins)
// - Any "blocking" → stop
// - Multiple "additionalContext" → concatenated
// - Multiple "updatedInput" → last one wins
// - "preventContinuation" → any true stops the session

This means a plugin's security hook can block an operation even if another plugin's hook approved it.


Transferable Design Patterns

The following patterns can be directly applied to other agentic systems or CLI tools.

Pattern 1: Exit Code as Control Flow

Using exit codes (0/1/2) for success/warn/block is simple, universal, and works across any language — Python, Bash, Node, Go. No SDK needed.

Pattern 2: JSON-or-Text Output

Hooks can return either structured JSON or plain text. JSON gets validated and parsed into decisions; plain text becomes an informational message. This makes simple hooks trivial (echo a message) while allowing sophisticated hooks to control permissions.

Pattern 3: Matcher Closures

The preparePermissionMatcher() pattern pre-compiles the matching logic once, then calls the closure per pattern. This avoids re-parsing tool inputs for every hook rule.


Summary

AspectDetail
Total code5,023 lines in hooks.ts + 17,931 lines in hooks/ directory
Event types20 lifecycle events
Hook typesCommand (shell), HTTP (webhook), Agent (sub-agent), Function (SDK)
Key hookPreToolUse — can approve/deny/modify any tool call
CommunicationJSON via stdin/stdout, validated with Zod
SecurityWorkspace trust required, managed hooks only option
AsyncBackground execution with re-wake capability
AggregationMost restrictive wins (deny > allow)

Previous: ← 04 — Plugin SystemNext: → 06 — Bash Execution Engine

Released under the MIT License.