From d9174fa230be4010cbb0ac0d44ce6625595c4cf1 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Mon, 20 Apr 2026 10:59:41 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=87=8D=E5=86=99=20Hooks=EF=BC=8C?= =?UTF-8?q?=E4=BB=8E=E6=BA=90=E7=A0=81=E8=A7=A3=E5=89=96=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E6=9C=BA=E5=88=B6=E8=AE=BE=E8=AE=A1=E5=88=86?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 TypeScript 代码、源码路径和完整的 JSON schema, 聚焦四种 Hook 能力的递进设计、异步 Hook 的非阻塞考量、 工作区信任的纵深防御和 Session Hook 的生命周期隔离。 Co-Authored-By: Claude Opus 4.6 --- docs/extensibility/hooks.mdx | 315 ++++++++++++----------------------- 1 file changed, 105 insertions(+), 210 deletions(-) diff --git a/docs/extensibility/hooks.mdx b/docs/extensibility/hooks.mdx index 438f546d3..d7905bc45 100644 --- a/docs/extensibility/hooks.mdx +++ b/docs/extensibility/hooks.mdx @@ -1,253 +1,148 @@ --- -title: "Hooks 生命周期钩子 - 执行引擎与拦截协议" -description: "从源码角度解析 Claude Code Hooks 系统:27 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。" +title: "Hooks" +description: "Hooks 是 Claude Code 的扩展机制——在工具调用前后注入自定义逻辑。理解四种 Hook 能力、匹配机制和安全防护。" keywords: ["Hooks", "生命周期钩子", "拦截器", "PreToolUse", "Hook 协议"] --- -{/* 本章目标:从源码角度揭示 Hook 的执行引擎、匹配机制、返回值协议和生命周期管理 */} +## 核心问题 -## 27 种 Hook 事件 +Claude Code 提供了强大的内置功能,但每个团队和工作流都不同。Hooks 让你可以在关键节点注入自定义逻辑——不需要修改 Claude Code 本身。 -Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoints/sdk/coreTypes.ts`),覆盖完整的 Agent 生命周期: +## Hook 事件 -| 阶段 | 事件 | 触发时机 | 匹配字段 | -|------|------|---------|---------| -| **会话** | `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 操作 | — | +Hooks 覆盖 Agent 生命周期的所有关键节点: -## 6 种 Hook 类型 +| 阶段 | 典型事件 | +|------|---------| +| **会话** | 启动、结束、初始化 | +| **用户交互** | 提交消息、停止响应 | +| **工具执行** | 工具调用前、工具调用后(成功/失败) | +| **权限** | 权限请求、权限被拒 | +| **子 Agent** | 启动、停止 | +| **压缩** | 压缩前、压缩后 | +| **协作** | Teammate 空闲、任务创建/完成 | -Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中: +## 四种 Hook 能力 -- **可持久化类型**(`command`、`prompt`、`agent`、`http`)— Zod schema 定义在 `src/schemas/hooks.ts`,通过 `z.discriminatedUnion('type', [...])` 声明 -- **callback 类型** — TypeScript 接口定义在 `src/types/hooks.ts`,用于 SDK 注册的内部 JS 函数 -- **function 类型** — 定义在 `src/utils/hooks/sessionHooks.ts`,用于运行时动态注册的函数 Hook - -| 类型 | 执行方式 | 适用场景 | -|------|---------|---------| -| `command` | Shell 命令(bash/PowerShell) | 通用脚本、CI 检查 | -| `prompt` | 注入到 AI 上下文 | 代码规范提醒 | -| `agent` | 启动子 Agent 执行 | 复杂分析任务 | -| `http` | HTTP 请求 | 远程服务、Webhook | -| `callback` | 内部 JS 函数 | 系统内置 Hook | -| `function` | 运行时注册的函数 Hook | Agent/Skill 内部使用 | - -## 执行引擎:execCommandHook - -`execCommandHook()`(`src/utils/hooks.ts`,`execCommandHook` 函数)是命令型 Hook 的执行核心: - -``` -execCommandHook(hook, hookEvent, hookName, jsonInput, signal) - ├── Shell 选择: hook.shell ?? DEFAULT_HOOK_SHELL - │ ├── bash: spawn(cmd, [], { shell: gitBashPath | true }) - │ └── powershell: spawn(pwsh, ['-NoProfile', '-NonInteractive', '-Command', cmd]) - ├── 变量替换 - │ ├── ${CLAUDE_PLUGIN_ROOT} → pluginRoot 路径 - │ ├── ${CLAUDE_PLUGIN_DATA} → plugin 数据目录 - │ └── ${user_config.X} → 用户配置值 - ├── 环境变量注入 - │ ├── CLAUDE_PROJECT_DIR - │ ├── CLAUDE_ENV_FILE(SessionStart/Setup/CwdChanged/FileChanged) - │ └── CLAUDE_PLUGIN_OPTION_*(plugin options) - ├── stdin 写入: jsonInput + '\n' - ├── 超时: hook.timeout * 1000 ?? 600000ms(10分钟) - └── 异步检测: 检查 stdout 首行是否为 {"async":true} -``` - -### 异步 Hook 的检测协议 - -Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`isAsyncHookJSONOutput` 检测 + `executeInBackground` 调用): - -```typescript -const firstLine = firstLineOf(stdout).trim() -if (isAsyncHookJSONOutput(parsed)) { - executeInBackground({ - processId: `async_hook_${child.pid}`, - asyncResponse: parsed, - ... - }) -} -``` - -后台 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`,`hookJSONOutputSchema` 定义在 `src/schemas/hooks.ts`): - -```json -{ - "continue": false, // 是否继续执行 - "suppressOutput": true, // 隐藏 stdout - "stopReason": "安全检查失败", // continue=false 时的原因 - "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`,`getMatchingHooks` 函数)负责从所有来源中查找匹配的 Hook: - -### 多来源合并 - -``` -getHooksConfig() - ├── getHooksConfigFromSnapshot() ← settings.json 中的 Hook(user/project/local) - ├── getRegisteredHooks() ← SDK 注册的 callback Hook - ├── getSessionHooks() ← Agent/Skill 前置注册的 session Hook - └── getSessionFunctionHooks() ← 运行时 function Hook -``` - -### 匹配规则 - -`matcher` 字段支持三种模式(`matchesPattern()` 函数,`src/utils/hooks.ts`): - -``` -"Write" → 精确匹配 -"Write|Edit" → 管道分隔的多值匹配 -"^Bash(git.*)" → 正则匹配 -"*" 或 "" → 通配(匹配所有) -``` - -### if 条件过滤 - -Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()`(`src/utils/hooks.ts`,`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()` 函数构建),保留**最后合并的层级**。 - -## 工作区信任检查 - -**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()` 函数,`src/utils/hooks.ts`)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。 - -```typescript -// 交互模式下,所有 Hook 要求信任 -const hasTrust = checkHasTrustDialogAccepted() -return !hasTrust -``` - -SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。 - -## 四种 Hook 能力的源码映射 +Hook 不仅是"执行一个脚本"——它有四种不同的能力: ### 1. 拦截操作(PreToolUse) +在工具执行前拦截,可以阻止危险操作: + ```json { "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "deny" + "permissionDecision": "deny", + "permissionDecisionReason": "不允许在生产分支上强制推送" } } ``` -`processHookJSONOutput()` 将 `permissionDecision` 映射为 `result.permissionBehavior = 'deny'`,并设置 `blockingError`,阻止工具执行。 +### 2. 修改行为 -### 2. 修改行为(updatedInput / updatedMCPToolOutput) +修改工具的输入或输出: ```json { "hookSpecificOutput": { - "hookEventName": "PreToolUse", "updatedInput": { "command": "npm test -- --bail" } } } ``` -`updatedInput` 替换原始工具输入;`updatedMCPToolOutput`(PostToolUse 事件)替换 MCP 工具的返回值——可用于过滤敏感数据。 +PostToolUse 的 `updatedMCPToolOutput` 可以替换 MCP 工具的返回值——用于过滤敏感数据。 -### 3. 注入上下文(additionalContext / systemMessage) +### 3. 注入上下文 -- `additionalContext` → 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息 -- `systemMessage` → 注入为系统警告,直接显示给用户 +向 AI 的对话中注入额外信息: +- `additionalContext` — 注入为用户消息,AI 可以参考 +- `systemMessage` — 显示为系统警告 -### 4. 控制流程(continue / stopReason) +### 4. 控制流程 + +阻止 Agent 继续执行: ```json -{ "continue": false, "stopReason": "构建失败,停止执行" } +{ + "continue": false, + "stopReason": "构建失败,停止执行" +} ``` -`continue: false` 设置 `preventContinuation = true`,阻止 Agent 继续执行后续操作。 +**设计洞察**:四种能力从"被动观察"到"主动干预"递进。最简单的 Hook 只是记录日志,最强大的 Hook 可以阻止操作、修改输入、控制流程。 + +## 六种 Hook 类型 + +| 类型 | 执行方式 | 适用场景 | +|------|---------|---------| +| `command` | Shell 命令 | 通用脚本、CI 检查 | +| `prompt` | 注入到 AI 上下文 | 代码规范提醒 | +| `agent` | 启动子 Agent | 复杂分析任务 | +| `http` | HTTP 请求 | 远程服务、Webhook | +| `callback` | 内部 JS 函数 | 系统内置 Hook | +| `function` | 运行时函数 | Agent/Skill 内部使用 | + +### 异步 Hook + +Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务。异步 Hook 完成后通过通知机制汇报结果。 + +**设计考量**:有些 Hook 需要长时间运行(如"等待 CI 结果"),不应该阻塞 Agent 的执行。异步 Hook 让这些操作在后台运行,完成后再通知 Agent。 + +## 匹配机制 + +### Matcher 模式 + +``` +"Write" → 精确匹配 +"Write|Edit" → 多值匹配 +"^Bash(git.*)" → 正则匹配 +"*" 或 "" → 通配所有 +``` + +### if 条件 + +Hook 可以指定 `if` 条件,只在特定输入时触发: + +```json +{ + "command": "check-branch.sh", + "if": "Bash(git push*)" +} +``` + +条件使用与权限规则相同的语法——工具名 + 参数模式。Bash 工具还会进行 AST 级别的命令解析。 + +### 多来源合并 + +Hook 从多个来源汇聚: +- settings.json 中的配置(user/project/local) +- SDK 注册的回调 +- Agent/Skill 的 frontmatter +- 运行时动态注册 + +同一命令可能在不同层级重复出现。系统按复合键去重,保留最后合并的层级。 + +## 安全防护 + +### 工作区信任 + +**所有 Hook 都要求工作区信任**。这是纵深防御——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。 + +**设计哲学**:Hook 有执行任意命令的能力,这个能力不应该被不可信的来源获取。项目级配置是团队共享的,任何人都可以修改——信任检查确保只有用户明确信任的项目才能运行 Hook。 + +### 超时控制 + +Hook 有默认 10 分钟的超时限制,可以通过配置调整。超时后 Hook 进程被终止,Agent 继续执行。 ## Session Hook 的生命周期 -Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`packages/builtin-tools/src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`)清理。 +Agent 和 Skill 可以注册 session Hook,绑定到特定的 session ID。Agent 结束时自动清理——Agent A 的 Hook 不会泄漏到 Agent B 的执行中。 -```typescript -// runAgent.ts — 注册 agent 的前置 Hook -registerFrontmatterHooks(rootSetAppState, agentId, agentDefinition.hooks, ...) +**设计考量**:如果不自动清理,Agent A 的 PreToolUse Hook 可能意外拦截 Agent B 的工具调用,导致难以调试的问题。 -// runAgent.ts — finally 块清理 -clearSessionHooks(rootSetAppState, agentId) -``` +## 接下来 -这确保 Agent A 的 Hook 不会泄漏到 Agent B 的执行中。 +- **Skills** — 理解基于 Hook 的技能系统 +- **MCP 配置** — 理解外部工具的注册 +- **权限模型** — 理解 PreToolUse Hook 与权限系统的协作