Hook 系统
Claude Code 的 hook 系统允许用户(以及 plugin)在 agentic 生命周期的关键节点注入自定义逻辑。Hook 是在 tool 调用前后、session 边界以及其他生命周期事件时执行的 shell 命令或回调。本章介绍 hook 架构、事件类型和执行模型。
Hook 架构
Section titled “Hook 架构”graph TD
A[Hook Configuration] --> B{Hook Source}
B --> C[settings.json hooks]
B --> D[Plugin hooks]
B --> E[SDK callback hooks]
C --> F[Shell command execution]
D --> G[Plugin callback execution]
E --> H[In-process callback]
F --> I[JSON output parsing]
G --> I
H --> I
I --> J{Decision}
J --> K[allow / deny / ask]
J --> L[continue / block]
J --> M[additional context]
J --> N[modified input]
Hook 事件类型
Section titled “Hook 事件类型”Claude Code 定义了丰富的 hook 事件集。核心生命周期 hook 如下:
Tool 生命周期 Hook
Section titled “Tool 生命周期 Hook”| 事件 | 触发时机 | 可修改内容 |
|---|---|---|
PreToolUse | tool 执行前 | permission 决策、输入、context |
PostToolUse | tool 成功后 | 附加 context、MCP 输出、是否继续 |
PostToolUseFailure | tool 失败后 | 附加 context、是否继续 |
Session 生命周期 Hook
Section titled “Session 生命周期 Hook”| 事件 | 触发时机 | 可修改内容 |
|---|---|---|
SessionStart | 新 session 开始时 | 附加 context、初始消息、监听路径 |
SessionEnd | session 结束时 | —(仅通知) |
Setup | 初始化设置时 | 附加 context |
其他 Hook 事件
Section titled “其他 Hook 事件”| 事件 | 触发时机 |
|---|---|
Stop | 模型产生最终响应(无 tool 调用)时 |
StopFailure | stop hook 失败时 |
Notification | 生成通知时 |
SubagentStart | sub-agent 启动时 |
SubagentStop | sub-agent 停止时 |
PreCompact | context compaction 前 |
PostCompact | context compaction 后 |
UserPromptSubmit | 用户提交消息时 |
PermissionDenied | permission 被拒绝时 |
PermissionRequest | 将显示 permission 提示时 |
TaskCreated | 后台任务创建时 |
TaskCompleted | 后台任务完成时 |
ConfigChange | 设置变更时 |
CwdChanged | 工作目录变更时 |
FileChanged | 被监听的文件变更时 |
InstructionsLoaded | CLAUDE.md 文件加载时 |
Elicitation | 请求 MCP elicitation 时 |
ElicitationResult | MCP elicitation 完成时 |
Hook 配置
Section titled “Hook 配置”Hook 在 settings.json 中配置(用户、项目或企业级范围):
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "python3 /path/to/check_command.py" } ] } ], "PostToolUse": [ { "matcher": "FileWrite", "hooks": [ { "type": "command", "command": "npx prettier --write $TOOL_INPUT_file_path" } ] } ], "SessionStart": [ { "hooks": [ { "type": "command", "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"Custom context here\"}}'" } ] } ] }}Matcher 模式
Section titled “Matcher 模式”matcher 字段决定哪些 tool 调用会触发该 hook:
- 无 matcher:hook 对该事件类型的所有 tool 调用均运行
- Tool 名称:
"Bash"——仅对 Bash tool 调用运行 - Tool + 模式:
"Bash(git *)"——对命令匹配git *的 Bash 调用运行
模式匹配由 tool 定义上的 preparePermissionMatcher() 处理。
Hook 执行
Section titled “Hook 执行”Shell 命令 Hook
Section titled “Shell 命令 Hook”主要的 hook 执行路径位于 src/utils/hooks.ts。Hook 以 shell 命令形式执行,通过环境变量和 stdin 接收 context:
// Environment variables passed to hook commandsCLAUDE_SESSION_ID // Current session IDCLAUDE_PROJECT_ROOT // Project root directoryCLAUDE_CWD // Current working directoryCLAUDE_TOOL_NAME // Name of the tool being invokedCLAUDE_TOOL_USE_ID // Tool use IDCLAUDE_HOOK_EVENT // Hook event type (PreToolUse, PostToolUse, etc.)TOOL_INPUT_* // Tool input fields (e.g., TOOL_INPUT_command for Bash)Hook 的 stdin 接收包含完整 context 的 JSON 对象:
{ "tool_name": "Bash", "tool_input": { "command": "git status" }, "tool_use_id": "toolu_01abc..."}Hook JSON 输出协议
Section titled “Hook JSON 输出协议”Hook 通过 stdout 上的 JSON 传达决策:
// src/types/hooks.ts — Sync hook responseconst syncHookResponseSchema = z.object({ continue: z.boolean().optional(), // Should Claude continue? (default: true) suppressOutput: z.boolean().optional(), // Hide stdout from transcript? stopReason: z.string().optional(), // Message when continue=false decision: z.enum(['approve', 'block']).optional(), reason: z.string().optional(), systemMessage: z.string().optional(), // Warning shown to user
hookSpecificOutput: z.union([ z.object({ hookEventName: z.literal('PreToolUse'), permissionDecision: z.enum(['allow', 'deny', 'ask']).optional(), permissionDecisionReason: z.string().optional(), updatedInput: z.record(z.string(), z.unknown()).optional(), additionalContext: z.string().optional(), }), z.object({ hookEventName: z.literal('PostToolUse'), additionalContext: z.string().optional(), updatedMCPToolOutput: z.unknown().optional(), }), // ... more event-specific shapes ]).optional(),});异步 Hook
Section titled “异步 Hook”Hook 可以通过返回 {"async": true} 声明自己是异步的:
const asyncHookResponseSchema = z.object({ async: z.literal(true), asyncTimeout: z.number().optional(),});异步 hook 在后台运行。它们通过 registerPendingAsyncHook() 注册,结果稍后到达。
Hook 执行流程(PreToolUse)
Section titled “Hook 执行流程(PreToolUse)”src/services/tools/toolHooks.ts 中的 runPreToolUseHooks 函数负责编排 PreToolUse hook:
sequenceDiagram
participant TE as toolExecution
participant TH as toolHooks
participant H as hooks.ts
participant CMD as Shell Command
TE->>TH: runPreToolUseHooks(tool, input, ...)
TH->>H: executePreToolHooks(toolName, input, ...)
H->>H: Find matching hooks (matcher check)
loop For each matching hook
H->>CMD: Execute command (env vars + stdin)
CMD-->>H: JSON stdout
H->>H: Parse with hookJSONOutputSchema
H-->>TH: Yield result (permission, context, etc.)
end
TH-->>TE: Yield aggregated results
Permission 解析
Section titled “Permission 解析”toolHooks.ts 中的 resolveHookPermissionDecision 函数以仔细的优先级顺序解析 hook permission:
export async function resolveHookPermissionDecision( hookPermissionResult, tool, input, toolUseContext, canUseTool, assistantMessage, toolUseID,): Promise<{ decision: PermissionDecision; input: Record<string, unknown> }> {
if (hookPermissionResult?.behavior === 'allow') { // Hook says allow, BUT settings.json deny/ask rules still apply const ruleCheck = await checkRuleBasedPermissions(tool, hookInput, toolUseContext); if (ruleCheck === null) { // No rule conflicts → allow return { decision: hookPermissionResult, input: hookInput }; } if (ruleCheck.behavior === 'deny') { // Deny rule overrides hook allow return { decision: ruleCheck, input: hookInput }; } // Ask rule → show dialog despite hook approval return { decision: await canUseTool(...), input: hookInput }; }
if (hookPermissionResult?.behavior === 'deny') { return { decision: hookPermissionResult, input }; }
// No hook or 'ask' → normal permission flow return { decision: await canUseTool(...), input };}PostToolUse Hook
Section titled “PostToolUse Hook”PostToolUse hook 在 tool 执行成功后运行:
export async function* runPostToolUseHooks( toolUseContext, tool, toolUseID, messageId, toolInput, toolResponse, requestId, mcpServerType,): AsyncGenerator<PostToolUseHooksResult> {
for await (const result of executePostToolHooks( tool.name, toolUseID, toolInput, toolOutput, toolUseContext, permissionMode, signal, )) { // Handle blocking errors if (result.blockingError) { yield { message: createAttachmentMessage({ type: 'hook_blocking_error', hookEvent: 'PostToolUse', blockingError: result.blockingError, })}; }
// Handle continuation prevention if (result.preventContinuation) { yield { message: createAttachmentMessage({ type: 'hook_stopped_continuation', message: result.stopReason || 'Execution stopped by PostToolUse hook', })}; return; // Stop processing further hooks }
// Handle additional context injection if (result.additionalContexts?.length > 0) { yield { message: createAttachmentMessage({ type: 'hook_additional_context', content: result.additionalContexts, })}; }
// Handle MCP tool output modification if (result.updatedMCPToolOutput && isMcpTool(tool)) { toolOutput = result.updatedMCPToolOutput; yield { updatedMCPToolOutput: toolOutput }; } }}Hook 超时
Section titled “Hook 超时”Hook 具有可配置的超时时间:
const TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes for tool hooks
// SessionEnd hooks have a much tighter timeoutconst SESSION_END_HOOK_TIMEOUT_MS_DEFAULT = 1500; // 1.5 seconds
export function getSessionEndHookTimeoutMs(): number { const raw = process.env.CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS; const parsed = raw ? parseInt(raw, 10) : NaN; return Number.isFinite(parsed) && parsed > 0 ? parsed : SESSION_END_HOOK_TIMEOUT_MS_DEFAULT;}回调 Hook(SDK/Plugin)
Section titled “回调 Hook(SDK/Plugin)”除 shell 命令外,hook 还可以注册为进程内回调:
export type HookCallback = { type: 'callback'; callback: ( input: HookInput, toolUseID: string | null, abort: AbortSignal | undefined, hookIndex?: number, context?: HookCallbackContext, ) => Promise<HookJSONOutput>; timeout?: number; internal?: boolean; // Excluded from metrics};回调 hook 的使用方包括:
- SDK 消费者:以编程方式注册 hook
- Plugin:通过其配置提供 hook
- 内部分析:追踪 session 文件访问模式
Hook 结果聚合
Section titled “Hook 结果聚合”当多个 hook 匹配同一事件时,其结果会被聚合:
export type AggregatedHookResult = { message?: Message; blockingErrors?: HookBlockingError[]; preventContinuation?: boolean; stopReason?: string; permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough'; additionalContexts?: string[]; updatedInput?: Record<string, unknown>; updatedMCPToolOutput?: unknown;};聚合规则:
- Permission:最严格优先(
deny>ask>allow) - 阻塞错误:全部收集并上报
- 附加 context:全部拼接
- 更新输入:最后一个 hook 的更新生效
- 继续执行:任意 hook 均可阻止继续
1. 写入后自动格式化
Section titled “1. 写入后自动格式化”{ "hooks": { "PostToolUse": [{ "matcher": "FileWrite", "hooks": [{ "type": "command", "command": "prettier --write \"$TOOL_INPUT_file_path\" 2>/dev/null; echo '{\"continue\":true,\"suppressOutput\":true}'" }] }] }}2. 拦截危险命令
Section titled “2. 拦截危险命令”{ "hooks": { "PreToolUse": [{ "matcher": "Bash(rm -rf *)", "hooks": [{ "type": "command", "command": "echo '{\"decision\":\"block\",\"reason\":\"Recursive force-delete is not allowed\"}'" }] }] }}3. 在 Session 启动时注入项目 Context
Section titled “3. 在 Session 启动时注入项目 Context”{ "hooks": { "SessionStart": [{ "hooks": [{ "type": "command", "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"This project uses pnpm, React 19, and TypeScript 5.7\"}}'" }] }] }}4. 自定义 Permission 逻辑
Section titled “4. 自定义 Permission 逻辑”{ "hooks": { "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "python3 /path/to/security_check.py" }] }] }}Python 脚本通过 stdin 接收命令并返回 permission 决策:
import json, sysdata = json.load(sys.stdin)command = data['tool_input']['command']if 'sudo' in command: print(json.dumps({ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "sudo is not allowed" } }))else: print(json.dumps({ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" } }))