vault backup: 2026-06-09 23:15:17
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
---
|
||||
title: "Hooks 生命周期钩子 - 执行引擎与拦截协议"
|
||||
description: "从源码角度解析 Claude Code Hooks 系统:27 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。"
|
||||
keywords: ["Hooks", "生命周期钩子", "拦截器", "PreToolUse", "Hook 协议"]
|
||||
tags: [Hooks, 生命周期钩子, 拦截器, PreToolUse, Hook协议]
|
||||
create time: 2026-06-09 22:30
|
||||
---
|
||||
|
||||
{/* 本章目标:从源码角度揭示 Hook 的执行引擎、匹配机制、返回值协议和生命周期管理 */}
|
||||
# Hooks 生命周期钩子 - 执行引擎与拦截协议
|
||||
|
||||
## 27 种 Hook 事件
|
||||
## 概述
|
||||
|
||||
从源码角度解析 Claude Code Hooks 系统:27 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。
|
||||
|
||||
## 正文
|
||||
|
||||
### 27 种 Hook 事件
|
||||
|
||||
Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoints/sdk/coreTypes.ts`),覆盖完整的 Agent 生命周期:
|
||||
|
||||
@@ -39,11 +44,11 @@ Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoin
|
||||
| | `InstructionsLoaded` | 指令加载 | `load_reason` |
|
||||
| | `WorktreeCreate` / `WorktreeRemove` | Worktree 操作 | — |
|
||||
|
||||
## 6 种 Hook 类型
|
||||
### 6 种 Hook 类型
|
||||
|
||||
Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中:
|
||||
|
||||
- **可持久化类型**(`command`、`prompt`、`agent`、`http`)— Zod schema 定义在 `src/schemas/hooks.ts`,通过 `z.discriminatedUnion('type', [...])` 声明
|
||||
- **可持久化类型**(`command`、`prompt`、`agent`、`http`)— Zod schema 定义在 `src/schemas/hooks.ts`
|
||||
- **callback 类型** — TypeScript 接口定义在 `src/types/hooks.ts`,用于 SDK 注册的内部 JS 函数
|
||||
- **function 类型** — 定义在 `src/utils/hooks/sessionHooks.ts`,用于运行时动态注册的函数 Hook
|
||||
|
||||
@@ -56,31 +61,31 @@ Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中:
|
||||
| `callback` | 内部 JS 函数 | 系统内置 Hook |
|
||||
| `function` | 运行时注册的函数 Hook | Agent/Skill 内部使用 |
|
||||
|
||||
## 执行引擎:execCommandHook
|
||||
### 执行引擎:execCommandHook
|
||||
|
||||
`execCommandHook()`(`src/utils/hooks.ts`,`execCommandHook` 函数)是命令型 Hook 的执行核心:
|
||||
`execCommandHook()`(`src/utils/hooks.ts`)是命令型 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}
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["execCommandHook(hook, hookEvent, hookName, jsonInput, signal)"] --> B{"Shell 选择"}
|
||||
B -->|"bash"| C["spawn(cmd, [], { shell: gitBashPath | true })"]
|
||||
B -->|"powershell"| D["spawn(pwsh, ['-NoProfile', '-NonInteractive', '-Command', cmd])"]
|
||||
A --> E["变量替换"]
|
||||
E --> F["${CLAUDE_PLUGIN_ROOT} -> pluginRoot 路径"]
|
||||
E --> G["${CLAUDE_PLUGIN_DATA} -> plugin 数据目录"]
|
||||
E --> H["${user_config.X} -> 用户配置值"]
|
||||
A --> I["环境变量注入"]
|
||||
I --> J["CLAUDE_PROJECT_DIR"]
|
||||
I --> K["CLAUDE_ENV_FILE SessionStart/Setup/CwdChanged/FileChanged"]
|
||||
I --> L["CLAUDE_PLUGIN_OPTION_* plugin options"]
|
||||
A --> M["stdin 写入: jsonInput + '\\n'"]
|
||||
A --> N["超时: hook.timeout * 1000 ?? 600000ms 10分钟"]
|
||||
A --> O["异步检测: 检查 stdout 首行是否为 {async:true}"]
|
||||
```
|
||||
|
||||
### 异步 Hook 的检测协议
|
||||
#### 异步 Hook 的检测协议
|
||||
|
||||
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`isAsyncHookJSONOutput` 检测 + `executeInBackground` 调用):
|
||||
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务:
|
||||
|
||||
```typescript
|
||||
const firstLine = firstLineOf(stdout).trim()
|
||||
@@ -95,13 +100,13 @@ if (isAsyncHookJSONOutput(parsed)) {
|
||||
|
||||
后台 Hook 通过 `registerPendingAsyncHook()` 注册到 `AsyncHookRegistry`,完成后通过 `enqueuePendingNotification()` 通知主线程。
|
||||
|
||||
### asyncRewake:Hook 唤醒模型
|
||||
#### asyncRewake:Hook 唤醒模型
|
||||
|
||||
`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型(通过 `useQueueProcessor`)或在忙碌时注入 `queued_command` 附件。
|
||||
`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型或在忙碌时注入 `queued_command` 附件。
|
||||
|
||||
## Hook 输出的 JSON Schema
|
||||
### Hook 输出的 JSON Schema
|
||||
|
||||
同步 Hook 的输出遵循严格的 Zod schema(`syncHookResponseSchema`,定义在 `src/types/hooks.ts`,`hookJSONOutputSchema` 定义在 `src/schemas/hooks.ts`):
|
||||
同步 Hook 的输出遵循严格的 Zod schema:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -121,7 +126,7 @@ if (isAsyncHookJSONOutput(parsed)) {
|
||||
}
|
||||
```
|
||||
|
||||
### 各事件的 hookSpecificOutput
|
||||
#### 各事件的 hookSpecificOutput
|
||||
|
||||
| 事件 | 专有字段 | 作用 |
|
||||
|------|---------|------|
|
||||
@@ -141,34 +146,34 @@ if (isAsyncHookJSONOutput(parsed)) {
|
||||
| `FileChanged` | `watchPaths` | 文件变更后更新监控路径 |
|
||||
| `WorktreeCreate` | `worktreePath` | Worktree 创建通知 |
|
||||
|
||||
## Hook 匹配机制:getMatchingHooks
|
||||
### Hook 匹配机制:getMatchingHooks
|
||||
|
||||
`getMatchingHooks()`(`src/utils/hooks.ts`,`getMatchingHooks` 函数)负责从所有来源中查找匹配的 Hook:
|
||||
`getMatchingHooks()`(`src/utils/hooks.ts`)负责从所有来源中查找匹配的 Hook:
|
||||
|
||||
### 多来源合并
|
||||
#### 多来源合并
|
||||
|
||||
```
|
||||
getHooksConfig()
|
||||
├── getHooksConfigFromSnapshot() ← settings.json 中的 Hook(user/project/local)
|
||||
├── getRegisteredHooks() ← SDK 注册的 callback Hook
|
||||
├── getSessionHooks() ← Agent/Skill 前置注册的 session Hook
|
||||
└── getSessionFunctionHooks() ← 运行时 function Hook
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["getHooksConfig()"] --> B["getHooksConfigFromSnapshot() settings.json 中的 Hook user/project/local"]
|
||||
A --> C["getRegisteredHooks() SDK 注册的 callback Hook"]
|
||||
A --> D["getSessionHooks() Agent/Skill 前置注册的 session Hook"]
|
||||
A --> E["getSessionFunctionHooks() 运行时 function Hook"]
|
||||
```
|
||||
|
||||
### 匹配规则
|
||||
#### 匹配规则
|
||||
|
||||
`matcher` 字段支持三种模式(`matchesPattern()` 函数,`src/utils/hooks.ts`):
|
||||
`matcher` 字段支持三种模式:
|
||||
|
||||
```
|
||||
"Write" → 精确匹配
|
||||
"Write|Edit" → 管道分隔的多值匹配
|
||||
"^Bash(git.*)" → 正则匹配
|
||||
"*" 或 "" → 通配(匹配所有)
|
||||
"Write" -> 精确匹配
|
||||
"Write|Edit" -> 管道分隔的多值匹配
|
||||
"^Bash(git.*)" -> 正则匹配
|
||||
"*" 或 "" -> 通配(匹配所有)
|
||||
```
|
||||
|
||||
### if 条件过滤
|
||||
#### if 条件过滤
|
||||
|
||||
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()`(`src/utils/hooks.ts`,`prepareIfConditionMatcher` 函数)预编译匹配器:
|
||||
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()` 预编译匹配器:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -181,13 +186,13 @@ Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditio
|
||||
|
||||
`if` 条件使用 `permissionRuleValueFromString` 解析,支持与权限规则相同的语法(工具名 + 参数模式)。Bash 工具还会使用 tree-sitter 进行 AST 级别的命令解析。
|
||||
|
||||
### Hook 去重
|
||||
#### Hook 去重
|
||||
|
||||
同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按四部分复合键做 Map 去重:`${pluginRoot}\0${shell}\0${command}\0${ifCondition}`(由 `hookDedupKey()` 函数构建),保留**最后合并的层级**。
|
||||
同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按四部分复合键做 Map 去重:`${pluginRoot}\0${shell}\0${command}\0${ifCondition}`,保留**最后合并的层级**。
|
||||
|
||||
## 工作区信任检查
|
||||
### 工作区信任检查
|
||||
|
||||
**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()` 函数,`src/utils/hooks.ts`)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
|
||||
**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()` 函数)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
|
||||
|
||||
```typescript
|
||||
// 交互模式下,所有 Hook 要求信任
|
||||
@@ -195,11 +200,12 @@ const hasTrust = checkHasTrustDialogAccepted()
|
||||
return !hasTrust
|
||||
```
|
||||
|
||||
SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。
|
||||
> [!tip]
|
||||
> SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。
|
||||
|
||||
## 四种 Hook 能力的源码映射
|
||||
### 四种 Hook 能力的源码映射
|
||||
|
||||
### 1. 拦截操作(PreToolUse)
|
||||
#### 1. 拦截操作(PreToolUse)
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -212,7 +218,7 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
|
||||
|
||||
`processHookJSONOutput()` 将 `permissionDecision` 映射为 `result.permissionBehavior = 'deny'`,并设置 `blockingError`,阻止工具执行。
|
||||
|
||||
### 2. 修改行为(updatedInput / updatedMCPToolOutput)
|
||||
#### 2. 修改行为(updatedInput / updatedMCPToolOutput)
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -225,12 +231,12 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
|
||||
|
||||
`updatedInput` 替换原始工具输入;`updatedMCPToolOutput`(PostToolUse 事件)替换 MCP 工具的返回值——可用于过滤敏感数据。
|
||||
|
||||
### 3. 注入上下文(additionalContext / systemMessage)
|
||||
#### 3. 注入上下文(additionalContext / systemMessage)
|
||||
|
||||
- `additionalContext` → 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息
|
||||
- `systemMessage` → 注入为系统警告,直接显示给用户
|
||||
- `additionalContext` -> 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息
|
||||
- `systemMessage` -> 注入为系统警告,直接显示给用户
|
||||
|
||||
### 4. 控制流程(continue / stopReason)
|
||||
#### 4. 控制流程(continue / stopReason)
|
||||
|
||||
```json
|
||||
{ "continue": false, "stopReason": "构建失败,停止执行" }
|
||||
@@ -238,9 +244,9 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
|
||||
|
||||
`continue: false` 设置 `preventContinuation = true`,阻止 Agent 继续执行后续操作。
|
||||
|
||||
## Session Hook 的生命周期
|
||||
### 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 的前置 Hook 通过 `registerFrontmatterHooks()` 注册,绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()` 清理。
|
||||
|
||||
```typescript
|
||||
// runAgent.ts — 注册 agent 的前置 Hook
|
||||
@@ -251,3 +257,8 @@ clearSessionHooks(rootSetAppState, agentId)
|
||||
```
|
||||
|
||||
这确保 Agent A 的 Hook 不会泄漏到 Agent B 的执行中。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[claude-code-best/docs/features/extensibility/custom-agents]]
|
||||
- [[claude-code-best/docs/features/extensibility/skills]]
|
||||
|
||||
Reference in New Issue
Block a user