243 lines
9.9 KiB
Markdown
243 lines
9.9 KiB
Markdown
---
|
||
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 配置]]
|