Files

243 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [Hooks, 生命周期钩子, 拦截器, PreToolUse, Claude-Code]
create time: 2026-06-09 22:30
---
# Hooks 生命周期钩子 - 执行引擎与拦截协议
## 概述
Claude Code 的 Hooks 系统定义了 27 种 Hook 事件,覆盖完整的 Agent 生命周期。通过 6 种 Hook 类型(command/prompt/agent/http/callback/function),Hooks 可以拦截工具调用、修改行为、注入上下文和控制执行流程,是企业级审计和安全加固的核心扩展点。
## 正文
### 27 种 Hook 事件
Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoints/sdk/coreTypes.ts`),覆盖完整的 Agent 生命周期:
| 阶段 | 事件 | 触发时机 | 匹配字段 |
|------|------|---------|---------|
| **会话** | `SessionStart` | 会话启动 | `source` |
| | `SessionEnd` | 会话结束 | `reason` |
| | `Setup` | 初始化完成 | `trigger` |
| **用户交互** | `UserPromptSubmit` | 用户提交消息 | — |
| | `Stop` | Agent 停止响应 | — |
| | `StopFailure` | Agent 停止失败 | `error` |
| **工具执行** | `PreToolUse` | 工具调用前 | `tool_name` |
| | `PostToolUse` | 工具调用后(成功) | `tool_name` |
| | `PostToolUseFailure` | 工具调用后(失败) | `tool_name` |
| **权限** | `PermissionRequest` | 权限请求 | `tool_name` |
| | `PermissionDenied` | 权限被拒 | `tool_name` |
| **子 Agent** | `SubagentStart` | 子 Agent 启动 | `agent_type` |
| | `SubagentStop` | 子 Agent 停止 | `agent_type` |
| **压缩** | `PreCompact` | 上下文压缩前 | `trigger` |
| | `PostCompact` | 上下文压缩后 | `trigger` |
| **协作** | `TeammateIdle` | Teammate 空闲 | — |
| | `TaskCreated` | 任务创建 | — |
| | `TaskCompleted` | 任务完成 | — |
| **MCP** | `Elicitation` | MCP 服务器请求用户输入 | `mcp_server_name` |
| | `ElicitationResult` | Elicitation 结果返回 | `mcp_server_name` |
| **通知** | `Notification` | 系统通知事件 | `notification_type` |
| **环境** | `ConfigChange` | 配置变更 | `source` |
| | `CwdChanged` | 工作目录变更 | — |
| | `FileChanged` | 文件变更 | `file_path` |
| | `InstructionsLoaded` | 指令加载 | `load_reason` |
| | `WorktreeCreate` / `WorktreeRemove` | Worktree 操作 | — |
### 6 种 Hook 类型
Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中:
| 类型 | 执行方式 | 适用场景 | 定义位置 |
|------|---------|---------|---------|
| `command` | Shell 命令(bash/PowerShell) | 通用脚本、CI 检查 | `src/schemas/hooks.ts` |
| `prompt` | 注入到 AI 上下文 | 代码规范提醒 | `src/schemas/hooks.ts` |
| `agent` | 启动子 Agent 执行 | 复杂分析任务 | `src/schemas/hooks.ts` |
| `http` | HTTP 请求 | 远程服务、Webhook | `src/schemas/hooks.ts` |
| `callback` | 内部 JS 函数 | 系统内置 Hook | `src/types/hooks.ts` |
| `function` | 运行时注册的函数 Hook | Agent/Skill 内部使用 | `src/utils/hooks/sessionHooks.ts` |
前四种(command/prompt/agent/http)为可持久化类型,通过 Zod schema 的 `z.discriminatedUnion('type', [...])` 声明。
### 执行引擎:execCommandHook
`execCommandHook()`(`src/utils/hooks.ts`)是命令型 Hook 的执行核心:
```mermaid
flowchart TD
A["execCommandHook()"] --> B["Shell 选择\nhook.shell ?? DEFAULT_HOOK_SHELL"]
B --> C["变量替换\nCLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, user_config.X"]
C --> D["环境变量注入\nCLAUDE_PROJECT_DIR, CLAUDE_ENV_FILE"]
D --> E["stdin 写入 jsonInput"]
E --> F["超时控制\nhook.timeout * 1000 ?? 600000ms"]
F --> G{"stdout 首行\n是否为 async: true?"}
G -->|"是"| H["转为后台任务\nAsyncHookRegistry"]
G -->|"否"| I["同步返回结果"]
```
#### 异步 Hook 的检测协议
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`isAsyncHookJSONOutput` 检测 + `executeInBackground` 调用)。
后台 Hook 通过 `registerPendingAsyncHook()` 注册到 `AsyncHookRegistry`,完成后通过 `enqueuePendingNotification()` 通知主线程。
#### asyncRewake:Hook 唤醒模型
`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型(通过 `useQueueProcessor`)或在忙碌时注入 `queued_command` 附件。
### Hook 输出的 JSON Schema
同步 Hook 的输出遵循严格的 Zod schema(`syncHookResponseSchema`,定义在 `src/types/hooks.ts`):
```json
{
"continue": false,
"suppressOutput": true,
"stopReason": "安全检查失败",
"decision": "approve | block",
"reason": "原因说明",
"systemMessage": "警告内容",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow | deny | ask",
"permissionDecisionReason": "匹配了安全规则",
"updatedInput": { },
"additionalContext": "额外上下文"
}
}
```
#### 各事件的 hookSpecificOutput
| 事件 | 专有字段 | 作用 |
|------|---------|------|
| `PreToolUse` | `permissionDecision`, `permissionDecisionReason`, `updatedInput`, `additionalContext` | 拦截/修改工具输入 |
| `PostToolUse` | `additionalContext`, `updatedMCPToolOutput` | 修改 MCP 工具输出 |
| `PostToolUseFailure` | `additionalContext` | 失败后注入上下文 |
| `UserPromptSubmit` | `additionalContext` | 注入额外上下文 |
| `SessionStart` | `additionalContext`, `initialUserMessage`, `watchPaths` | 设置初始消息和文件监控 |
| `PermissionRequest` | `decision`(含 `allow`/`deny` 子字段) | 权限请求的 Hook 决策 |
| `PermissionDenied` | `retry` | 指示是否重试 |
| `SubagentStart` | `additionalContext` | 子 Agent 启动时注入上下文 |
| `Elicitation` | `action`, `content` | 控制用户输入对话框 |
| `ElicitationResult` | `action`, `content` | Elicitation 结果处理 |
| `Notification` | `additionalContext` | 通知事件注入上下文 |
| `Setup` | `additionalContext` | 初始化时注入上下文 |
| `CwdChanged` | `watchPaths` | 目录变更后更新监控路径 |
| `FileChanged` | `watchPaths` | 文件变更后更新监控路径 |
| `WorktreeCreate` | `worktreePath` | Worktree 创建通知 |
### Hook 匹配机制:getMatchingHooks
`getMatchingHooks()`(`src/utils/hooks.ts`)负责从所有来源中查找匹配的 Hook。
#### 多来源合并
```text
getHooksConfig()
├── getHooksConfigFromSnapshot() ← settings.json 中的 Hook(user/project/local)
├── getRegisteredHooks() ← SDK 注册的 callback Hook
├── getSessionHooks() ← Agent/Skill 前置注册的 session Hook
└── getSessionFunctionHooks() ← 运行时 function Hook
```
#### 匹配规则
`matcher` 字段支持三种模式(`matchesPattern()` 函数):
```text
"Write" → 精确匹配
"Write|Edit" → 管道分隔的多值匹配
"^Bash(git.*)" → 正则匹配
"*" 或 "" → 通配(匹配所有)
```
#### if 条件过滤
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()` 预编译匹配器:
```json
{
"hooks": [{
"command": "check-git-branch.sh",
"if": "Bash(git push*)"
}]
}
```
`if` 条件使用 `permissionRuleValueFromString` 解析,支持与权限规则相同的语法(工具名 + 参数模式)。Bash 工具还会使用 tree-sitter 进行 AST 级别的命令解析。
#### Hook 去重
同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按四部分复合键做 Map 去重:`${pluginRoot}\0${shell}\0${command}\0${ifCondition}`(由 `hookDedupKey()` 函数构建),保留**最后合并的层级**。
### 工作区信任检查
> [!warning] 纵深防御
> **所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()` 函数)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。
### 四种 Hook 能力的源码映射
#### 1. 拦截操作(PreToolUse)
```json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny"
}
}
```
`processHookJSONOutput()` 将 `permissionDecision` 映射为 `result.permissionBehavior = 'deny'`,并设置 `blockingError`,阻止工具执行。
#### 2. 修改行为(updatedInput / updatedMCPToolOutput)
```json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": { "command": "npm test -- --bail" }
}
}
```
`updatedInput` 替换原始工具输入;`updatedMCPToolOutput`(PostToolUse 事件)替换 MCP 工具的返回值——可用于过滤敏感数据。
#### 3. 注入上下文(additionalContext / systemMessage)
- `additionalContext` -> 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息
- `systemMessage` -> 注入为系统警告,直接显示给用户
#### 4. 控制流程(continue / stopReason)
```json
{ "continue": false, "stopReason": "构建失败,停止执行" }
```
`continue: false` 设置 `preventContinuation = true`,阻止 Agent 继续执行后续操作。
### Session Hook 的生命周期
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`packages/builtin-tools/src/tools/AgentTool/runAgent.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(`src/utils/hooks/sessionHooks.ts`)清理。
```typescript
// runAgent.ts — 注册 agent 的前置 Hook
registerFrontmatterHooks(rootSetAppState, agentId, agentDefinition.hooks, ...)
// runAgent.ts — finally 块清理
clearSessionHooks(rootSetAppState, agentId)
```
> [!tip] 隔离保证
> 这确保 Agent A 的 Hook 不会泄漏到 Agent B 的执行中。
## 关联笔记
- [[custom-agents|自定义 Agent]]
- [[skills|Skills 技能系统]]
- [[why-safety-matters|AI 安全至关重要]]
- [[mcp-configuration|MCP 配置]]