基于 Claude Code v2.1.88 源码分析 源码路径:
src/utils/hooks.ts,src/schemas/hooks.ts,src/types/hooks.ts,src/utils/hooks/
Hooks 系统是 Claude Code 的可扩展性核心,允许用户在 Claude Code 生命周期的关键节点注入自定义逻辑。 系统支持四种 Hook 类型(command / prompt / agent / http),覆盖 27 种事件, 并提供同步/异步两种执行模式、权限控制、模式匹配等能力。
主要源码文件:
| 文件 | 职责 |
|---|---|
src/schemas/hooks.ts |
Zod schema 定义(HookCommand, HookMatcher, HooksSettings) |
src/types/hooks.ts |
TypeScript 类型定义(HookResult, HookJSONOutput 等) |
src/utils/hooks.ts |
核心执行引擎(~4600 行,所有 executeXxxHooks 函数) |
src/utils/hooks/sessionHooks.ts |
Session 级别的运行时 Hook 管理 |
src/utils/hooks/hookEvents.ts |
Hook 事件广播系统 |
src/utils/hooks/hooksSettings.ts |
Hook 配置读取与 UI 展示 |
src/utils/hooks/hooksConfigSnapshot.ts |
配置快照管理(启动时捕获) |
src/utils/hooks/hookHelpers.ts |
Prompt/Agent hook 的辅助工具 |
src/utils/hooks/execPromptHook.ts |
Prompt hook 执行器 |
src/utils/hooks/execAgentHook.ts |
Agent hook 执行器 |
src/utils/hooks/execHttpHook.ts |
HTTP hook 执行器 |
src/utils/hooks/AsyncHookRegistry.ts |
异步 Hook 注册表 |
src/hooks/toolPermission/ |
权限审批交互(React hooks) |
所有 Hook 类型通过 z.discriminatedUnion('type', [...]) 定义在 src/schemas/hooks.ts:
// src/schemas/hooks.ts
// Command Hook -- 执行 shell 命令
const BashCommandHookSchema = z.object({
type: z.literal('command'),
command: z.string(),
if: IfConditionSchema(), // 可选: 权限规则语法过滤
shell: z.enum(['bash', 'powershell']).optional(),
timeout: z.number().positive().optional(),
statusMessage: z.string().optional(),
once: z.boolean().optional(), // 一次性执行后移除
async: z.boolean().optional(), // 后台异步执行
asyncRewake: z.boolean().optional(), // 异步执行,exit code 2 时唤醒模型
})
// Prompt Hook -- 调用 LLM 做单轮判断
const PromptHookSchema = z.object({
type: z.literal('prompt'),
prompt: z.string(), // 支持 $ARGUMENTS 占位符
if: IfConditionSchema(),
timeout: z.number().positive().optional(),
model: z.string().optional(), // 默认使用 small fast model
statusMessage: z.string().optional(),
once: z.boolean().optional(),
})
// Agent Hook -- 启动多轮 LLM Agent 做验证
const AgentHookSchema = z.object({
type: z.literal('agent'),
prompt: z.string(),
if: IfConditionSchema(),
timeout: z.number().positive().optional(),
model: z.string().optional(),
statusMessage: z.string().optional(),
once: z.boolean().optional(),
})
// HTTP Hook -- POST JSON 到远程 URL
const HttpHookSchema = z.object({
type: z.literal('http'),
url: z.string().url(),
if: IfConditionSchema(),
timeout: z.number().positive().optional(),
headers: z.record(z.string(), z.string()).optional(),
allowedEnvVars: z.array(z.string()).optional(),
statusMessage: z.string().optional(),
once: z.boolean().optional(),
})
// 联合类型
export const HookCommandSchema = z.discriminatedUnion('type', [
BashCommandHookSchema, PromptHookSchema, AgentHookSchema, HttpHookSchema,
])
// 推导出的 TypeScript 类型
export type HookCommand = z.infer<typeof HookCommandSchema>
export type BashCommandHook = Extract<HookCommand, { type: 'command' }>
export type PromptHook = Extract<HookCommand, { type: 'prompt' }>
export type AgentHook = Extract<HookCommand, { type: 'agent' }>
export type HttpHook = Extract<HookCommand, { type: 'http' }>除了用户可配置的四种类型,系统内部还有两种非持久化类型:
// src/types/hooks.ts -- Callback Hook (SDK 内部注册)
export type HookCallback = {
type: 'callback'
callback: (
input: HookInput,
toolUseID: string | null,
abort: AbortSignal | undefined,
hookIndex?: number,
context?: HookCallbackContext,
) => Promise<HookJSONOutput>
timeout?: number
internal?: boolean // 内部 hook 不计入遥测
}
// src/utils/hooks/sessionHooks.ts -- Function Hook (运行时回调)
export type FunctionHook = {
type: 'function'
id?: string
timeout?: number
callback: FunctionHookCallback // (messages, signal?) => boolean
errorMessage: string
statusMessage?: string
}Callback Hook: 由 SDK 或插件通过代码注册,如 attribution hooks、session file access hooks。 Function Hook: Session 级别的 TypeScript 回调,用于 Stop hook 中强制结构化输出等场景。
所有事件定义在 src/entrypoints/sdk/coreTypes.ts:
export const HOOK_EVENTS = [
'PreToolUse', // 工具执行前
'PostToolUse', // 工具执行后
'PostToolUseFailure', // 工具执行失败后
'Notification', // 通知发送时
'UserPromptSubmit', // 用户提交 prompt 时
'SessionStart', // 新 session 启动
'SessionEnd', // session 结束
'Stop', // Claude 即将结束回复
'StopFailure', // API 错误导致回合结束
'SubagentStart', // 子 agent 启动
'SubagentStop', // 子 agent 结束
'PreCompact', // 上下文压缩前
'PostCompact', // 上下文压缩后
'PermissionRequest', // 权限对话框显示时
'PermissionDenied', // 自动模式拒绝工具调用后
'Setup', // 仓库初始化/维护
'TeammateIdle', // Teammate 即将空闲
'TaskCreated', // 任务创建时
'TaskCompleted', // 任务完成时
'Elicitation', // MCP 请求用户输入
'ElicitationResult', // 用户响应 MCP elicitation
'ConfigChange', // 配置文件变更
'WorktreeCreate', // 创建 worktree
'WorktreeRemove', // 移除 worktree
'InstructionsLoaded', // 指令文件加载时
'CwdChanged', // 工作目录切换后
'FileChanged', // 被监视文件变更
] as const| 分类 | 事件 | Exit 0 | Exit 2 | 其他 Exit |
|---|---|---|---|---|
| 工具生命周期 | PreToolUse | stdout 不显示 | stderr 发给模型,阻止工具调用 | stderr 仅显示给用户 |
| PostToolUse | stdout 在 transcript 模式显示 | stderr 立即发给模型 | stderr 仅显示给用户 | |
| PostToolUseFailure | stdout 在 transcript 模式显示 | stderr 立即发给模型 | stderr 仅显示给用户 | |
| 用户交互 | UserPromptSubmit | stdout 发给 Claude | 阻止处理,stderr 显示给用户 | stderr 仅显示给用户 |
| PermissionRequest | 使用 hook 的决定 | - | stderr 仅显示给用户 | |
| PermissionDenied | stdout 在 transcript 显示 | - | stderr 仅显示给用户 | |
| Session 生命周期 | SessionStart | stdout 发给 Claude | 阻止错误被忽略 | stderr 仅显示给用户 |
| SessionEnd | 命令成功完成 | - | stderr 仅显示给用户 | |
| Setup | stdout 发给 Claude | 阻止错误被忽略 | stderr 仅显示给用户 | |
| 停止控制 | Stop | stdout 不显示 | stderr 发给模型,继续对话 | stderr 仅显示给用户 |
| StopFailure | fire-and-forget | - | 输出被忽略 | |
| 子 Agent | SubagentStart | stdout 发给子 agent | 阻止错误被忽略 | stderr 仅显示给用户 |
| SubagentStop | stdout 不显示 | stderr 发给子 agent,继续运行 | stderr 仅显示给用户 | |
| 压缩 | PreCompact | stdout 追加为自定义压缩指令 | 阻止压缩 | stderr 仅显示给用户 |
| PostCompact | stdout 显示给用户 | - | stderr 仅显示给用户 | |
| 配置/文件 | ConfigChange | 允许变更 | 阻止变更应用 | stderr 仅显示给用户 |
| CwdChanged | 命令成功完成 | - | stderr 仅显示给用户 | |
| FileChanged | 命令成功完成 | - | stderr 仅显示给用户 | |
| InstructionsLoaded | 纯观测性,不支持阻止 | - | stderr 仅显示给用户 |
// src/schemas/hooks.ts
export const HookMatcherSchema = z.object({
matcher: z.string().optional(), // 匹配模式
hooks: z.array(HookCommandSchema()),
})
export const HooksSchema = z.partialRecord(
z.enum(HOOK_EVENTS),
z.array(HookMatcherSchema()),
)matchesPattern() 函数 (src/utils/hooks.ts:1346) 实现三级匹配:
1. 空 matcher 或 "*" --> 匹配所有
2. 纯字母数字 + "|" --> 精确匹配(支持 pipe 分隔多值)
例: "Write|Edit" 匹配 "Write" 或 "Edit"
3. 包含特殊字符 --> 作为正则表达式匹配
例: "^Bash$" 匹配 "Bash"
不同事件使用不同字段作为 matchQuery:
// src/utils/hooks.ts:getMatchingHooks()
switch (hookInput.hook_event_name) {
case 'PreToolUse':
case 'PostToolUse':
case 'PostToolUseFailure':
case 'PermissionRequest':
case 'PermissionDenied':
matchQuery = hookInput.tool_name // 匹配工具名
break
case 'SessionStart':
matchQuery = hookInput.source // 'startup' | 'resume' | 'clear' | 'compact'
break
case 'Setup':
matchQuery = hookInput.trigger // 'init' | 'maintenance'
break
case 'Notification':
matchQuery = hookInput.notification_type
break
case 'FileChanged':
matchQuery = basename(hookInput.file_path)
break
// ... 其他事件类似
}Hook 支持 if 字段做更细粒度的过滤,使用权限规则语法:
{
"type": "command",
"command": "npm test",
"if": "Bash(npm *)"
}prepareIfConditionMatcher() 解析 if 条件,仅在 PreToolUse / PostToolUse /
PostToolUseFailure / PermissionRequest 事件中生效。匹配逻辑复用了权限系统的
permissionRuleValueFromString 解析器和工具的 preparePermissionMatcher 方法。
executeXxxHooks() 各事件的入口函数
|
v
executeHooks() 核心执行引擎(async generator)
|
+-- shouldDisableAllHooksIncludingManaged() 全局禁用检查
+-- shouldSkipHookDueToTrust() 工作区信任检查
+-- getMatchingHooks() 模式匹配 + 去重 + if 过滤
| +-- getHooksConfig() 聚合所有来源的 hook 配置
| | +-- getHooksConfigFromSnapshot() settings.json 快照
| | +-- getRegisteredHooks() SDK/插件注册的 hooks
| | +-- getSessionHooks() Session 级动态 hooks
| | +-- getSessionFunctionHooks() Function hooks
| +-- matchesPattern() matcher 模式匹配
| +-- prepareIfConditionMatcher() if 条件过滤
| +-- 去重(按 type + command/prompt/url 分组)
|
+-- 并行执行所有匹配的 hook
| +-- execCommandHook() --> shell 命令执行
| +-- execPromptHook() --> LLM 单轮查询
| +-- execAgentHook() --> LLM 多轮 Agent
| +-- execHttpHook() --> HTTP POST
| +-- callback() --> 内部回调
| +-- FunctionHook --> 运行时回调
|
+-- processHookJSONOutput() 解析 JSON 输出
+-- yield AggregatedHookResult 逐个产出结果
Command Hook 是最复杂的执行路径:
1. Shell 选择: hook.shell --> 'bash'(默认) 或 'powershell'
2. 路径转换: Windows 上 bash 使用 POSIX 路径 (/c/Users/...)
3. 插件变量替换: ${CLAUDE_PLUGIN_ROOT}, ${user_config.X}
4. 环境变量注入:
- CLAUDE_PROJECT_DIR: 项目根目录
- CLAUDE_PLUGIN_ROOT: 插件根目录
- CLAUDE_ENV_FILE: 环境脚本路径(SessionStart/Setup/CwdChanged/FileChanged)
5. 命令前缀: CLAUDE_CODE_SHELL_PREFIX(仅 bash)
6. 进程生成:
- bash: spawn(command, [], { shell: true })
- powershell: spawn(pwshPath, ['-NoProfile', '-NonInteractive', '-Command', cmd])
7. stdin 写入 JSON hook input
8. stdout 解析:
a. 首行检测异步协议 {"async": true}
b. Prompt Request 检测 {"prompt": "id", "message": "...", "options": [...]}
c. JSON 输出验证(hookJSONOutputSchema)
d. 非 JSON 作为纯文本处理
1. $ARGUMENTS 占位符替换
2. 构造 system prompt,要求返回 {"ok": true/false, "reason": "..."}
3. 调用 queryModelWithoutStreaming()
- 使用 json_schema output format 强制 JSON 输出
- 默认使用 small fast model (Haiku)
- 默认超时 30 秒
4. 解析响应,验证 hookResponseSchema
5. ok=true --> success; ok=false --> blocking error
1. $ARGUMENTS 占位符替换
2. 创建独立 agentId (hook-agent-UUID)
3. 注册 StructuredOutputTool 强制结构化输出
4. 配置 agent:
- 过滤掉 agent 不允许使用的工具(防止嵌套 agent/plan mode)
- 权限模式设为 dontAsk
- 禁用 thinking
- 允许读取 transcript 文件
5. 使用 query() 多轮执行(最多 50 轮)
6. 监听 structured_output attachment
7. ok=true --> success; ok=false --> blocking error
1. URL 允许列表检查 (allowedHttpHookUrls)
2. Header 环境变量插值 ($VAR_NAME, ${VAR_NAME})
- 仅 allowedEnvVars 中列出的变量会被替换
- 值经过 sanitizeHeaderValue 防止 CRLF 注入
3. 沙箱代理配置 (sandbox network proxy)
4. SSRF 防护 (ssrfGuardedLookup -- 阻止私有/链路本地 IP)
5. axios.post(url, jsonInput, { headers, proxy, lookup })
6. 响应解析:
- 空响应体 --> 视为空 JSON 对象
- 非 JSON --> 验证错误
- JSON --> hookJSONOutputSchema 验证
// src/types/hooks.ts
export const syncHookResponseSchema = z.object({
continue: z.boolean().optional(), // false 则阻止 Claude 继续
suppressOutput: z.boolean().optional(), // 隐藏 stdout
stopReason: z.string().optional(), // continue=false 时的消息
decision: z.enum(['approve', 'block']).optional(),
reason: z.string().optional(),
systemMessage: z.string().optional(), // 警告消息
hookSpecificOutput: z.union([
// PreToolUse: 可修改权限决策和工具输入
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(),
}),
// PermissionRequest: 允许/拒绝权限
z.object({
hookEventName: z.literal('PermissionRequest'),
decision: z.union([
z.object({
behavior: z.literal('allow'),
updatedInput: z.record(z.string(), z.unknown()).optional(),
updatedPermissions: z.array(permissionUpdateSchema()).optional(),
}),
z.object({
behavior: z.literal('deny'),
message: z.string().optional(),
interrupt: z.boolean().optional(),
}),
]),
}),
// SessionStart: 可设置 watchPaths 和 initialUserMessage
z.object({
hookEventName: z.literal('SessionStart'),
additionalContext: z.string().optional(),
initialUserMessage: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
}),
// PostToolUse: 可替换 MCP 工具输出
z.object({
hookEventName: z.literal('PostToolUse'),
additionalContext: z.string().optional(),
updatedMCPToolOutput: z.unknown().optional(),
}),
// PermissionDenied: 可请求重试
z.object({
hookEventName: z.literal('PermissionDenied'),
retry: z.boolean().optional(),
}),
// WorktreeCreate: 返回 worktree 路径
z.object({
hookEventName: z.literal('WorktreeCreate'),
worktreePath: z.string(),
}),
// ... 其他事件的特定输出
]).optional(),
})const asyncHookResponseSchema = z.object({
async: z.literal(true),
asyncTimeout: z.number().optional(), // 默认 15000ms
})
export const hookJSONOutputSchema = z.union([
asyncHookResponseSchema,
syncHookResponseSchema,
])export function isSyncHookJSONOutput(json: HookJSONOutput): json is SyncHookJSONOutput {
return !('async' in json && json.async === true)
}
export function isAsyncHookJSONOutput(json: HookJSONOutput): json is AsyncHookJSONOutput {
return 'async' in json && json.async === true
}PreToolUse hook 可以通过 hookSpecificOutput.permissionDecision 做出权限决策:
allow --> 自动批准工具调用
deny --> 阻止工具调用
ask --> 显示权限对话框让用户决定
优先级规则 (在 executeHooks 的结果聚合中):
deny > ask > allow > (无决策)
即: 任何一个 hook 返回 deny,最终结果就是 deny。
ask 优先于 allow,但不能覆盖 deny。
当权限对话框即将显示时触发,hook 可直接做出允许/拒绝决策:
// PermissionRequest hook 输出
{
hookSpecificOutput: {
hookEventName: 'PermissionRequest',
decision: {
behavior: 'allow',
updatedInput: { ... }, // 可选: 修改工具输入
updatedPermissions: [ // 可选: 更新权限规则
{ destination: 'session', value: 'Read(*.ts)' }
],
}
}
}
// 或
{
hookSpecificOutput: {
hookEventName: 'PermissionRequest',
decision: {
behavior: 'deny',
message: '不允许执行此操作',
interrupt: true, // 可选: 中断整个流程
}
}
}src/hooks/toolPermission/PermissionContext.ts 封装了权限决策流程:
function createPermissionContext(tool, input, toolUseContext, ...) {
return {
// 运行 PermissionRequest hooks
async runHooks(permissionMode, suggestions, updatedInput, startTimeMs) {
for await (const hookResult of executePermissionRequestHooks(...)) {
if (hookResult.permissionRequestResult) {
// allow --> handleHookAllow() 持久化权限更新
// deny --> buildDeny() 构建拒绝结果
}
}
return null // 无 hook 做出决策
},
// Hook 允许后的处理
async handleHookAllow(finalInput, permissionUpdates, startTimeMs) {
await this.persistPermissions(permissionUpdates)
return this.buildAllow(finalInput, {
decisionReason: { type: 'hook', hookName: 'PermissionRequest' },
})
},
// 持久化权限更新
async persistPermissions(updates: PermissionUpdate[]) { ... },
// 构建允许/拒绝结果
buildAllow(updatedInput, opts?): PermissionAllowDecision { ... },
buildDeny(message, reason): PermissionDenyDecision { ... },
}
}1. async: true(配置级别)
在 settings.json 中设置 "async": true:
{
"hooks": {
"PostToolUse": [{
"matcher": "Write",
"hooks": [{
"type": "command",
"command": "npx prettier --write $FILE",
"async": true
}]
}]
}
}2. 运行时协商(首行输出)
Hook 进程在首行输出 {"async": true} 来请求异步执行:
#!/bin/bash
echo '{"async": true, "asyncTimeout": 30000}'
# 后续长时间运行的任务...
long_running_checkasyncRewake 是特殊的异步模式,当 hook 进程以 exit code 2 退出时,
会将 stderr 作为 task-notification 注入消息队列,唤醒模型继续处理:
// src/utils/hooks.ts:executeInBackground()
if (asyncRewake) {
void shellCommand.result.then(async result => {
if (result.code === 2) {
enqueuePendingNotification({
value: wrapInSystemReminder(
`Stop hook blocking error from command "${hookName}": ${stderr || stdout}`,
),
mode: 'task-notification',
})
}
})
return true
}src/utils/hooks/AsyncHookRegistry.ts 管理所有后台运行的异步 hook:
type PendingAsyncHook = {
processId: string
hookId: string
hookName: string
hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion'
toolName?: string
pluginId?: string
startTime: number
timeout: number // 默认 15 秒
command: string
responseAttachmentSent: boolean // 是否已发送响应
shellCommand?: ShellCommand
stopProgressInterval: () => void
}
// 主要 API
registerPendingAsyncHook(...) // 注册异步 hook
getPendingAsyncHooks() // 获取未完成的 hook
checkForAsyncHookResponses() // 轮询已完成的 hook
removeDeliveredAsyncHooks(ids) // 清理已处理的 hook
finalizePendingAsyncHooks() // Session 结束时清理所有checkForAsyncHookResponses() 在每次 query loop 迭代中被调用,
检查是否有异步 hook 完成,如果完成则解析其输出并注入到消息流中。
当 once: true 时,hook 执行一次后自动从配置中移除。
这在 session hook 和 skill hook 中常见,用于一次性初始化逻辑。
src/utils/hooks/hookEvents.ts 提供了独立于消息流的事件广播:
// 事件类型
type HookStartedEvent = {
type: 'started'
hookId: string; hookName: string; hookEvent: string
}
type HookProgressEvent = {
type: 'progress'
hookId: string; hookName: string; hookEvent: string
stdout: string; stderr: string; output: string
}
type HookResponseEvent = {
type: 'response'
hookId: string; hookName: string; hookEvent: string
output: string; stdout: string; stderr: string
exitCode?: number
outcome: 'success' | 'error' | 'cancelled'
}
// 注册处理器(SDK 消费)
registerHookEventHandler(handler: HookEventHandler | null)
// 发射事件
emitHookStarted(hookId, hookName, hookEvent)
emitHookProgress(data)
emitHookResponse(data)
// 周期性进度报告(异步 hook)
startHookProgressInterval(params): () => void // 返回清理函数事件过滤: SessionStart 和 Setup 事件始终广播;
其他事件需要通过 setAllHookEventsEnabled(true) 启用
(SDK includeHookEvents 选项或 CLAUDE_CODE_REMOTE 模式)。
缓冲机制: 在 handler 注册前,事件缓存在 pendingEvents 数组中
(最多 100 条),handler 注册后立即 flush。
src/utils/hooks/sessionHooks.ts 管理运行时动态注册的 hook。
这些 hook 存储在内存中,不持久化到 settings.json。
type SessionStore = {
hooks: {
[event in HookEvent]?: SessionHookMatcher[]
}
}
// 使用 Map 而非 Record,避免 O(N^2) 复制开销
type SessionHooksState = Map<string, SessionStore>// 添加 command/prompt/http/agent hook
addSessionHook(setAppState, sessionId, event, matcher, hook, onHookSuccess?, skillRoot?)
// 添加 function hook(内存回调)
addFunctionHook(setAppState, sessionId, event, matcher, callback, errorMessage, options?)
--> returns hookId
// 移除 hook
removeSessionHook(setAppState, sessionId, event, hook)
removeFunctionHook(setAppState, sessionId, event, hookId)
// 查询
getSessionHooks(appState, sessionId, event?) // 排除 function hooks
getSessionFunctionHooks(appState, sessionId, event?)
getSessionHookCallback(appState, sessionId, event, matcher, hook)
// 清理
clearSessionHooks(setAppState, sessionId)- Skill Hooks: Skill 注册的 frontmatter hooks 通过
addSessionHook添加 - Structured Output 强制:
registerStructuredOutputEnforcement注册 Stop hook 确保 agent hook 调用 SyntheticOutputTool - Agent Frontmatter Hooks: Agent 定义中的 hooks 在运行时注册为 session hooks
src/utils/hooks/hooksConfigSnapshot.ts 在应用启动时捕获一份 hook 配置快照:
// 应用启动时调用
captureHooksConfigSnapshot()
// 设置修改后更新(会先 resetSettingsCache)
updateHooksConfigSnapshot()
// 获取当前快照
getHooksConfigFromSnapshot(): HooksSettings | null配置来源优先级:
policySettings.disableAllHooks === true --> 返回空(全部禁用)
policySettings.allowManagedHooksOnly === true --> 仅返回 policySettings.hooks
isRestrictedToPluginOnly('hooks') --> 仅返回 policySettings.hooks
mergedSettings.disableAllHooks === true --> 仅返回 policySettings.hooks(非管理策略不能禁用管理 hooks)
默认 --> 返回 mergedSettings.hooks(所有来源合并)
src/hooks/notifs/ 目录包含 React hooks,用于 UI 通知:
| Hook | 用途 |
|---|---|
useSettingsErrors |
设置文件错误通知 |
useRateLimitWarningNotification |
速率限制警告 |
useDeprecationWarningNotification |
弃用警告 |
useStartupNotification |
启动通知 |
useFastModeNotification |
快速模式通知 |
useLspInitializationNotification |
LSP 初始化通知 |
useMcpConnectivityStatus |
MCP 连接状态 |
usePluginAutoupdateNotification |
插件自动更新通知 |
usePluginInstallationStatus |
插件安装状态 |
useAutoModeUnavailableNotification |
自动模式不可用通知 |
useModelMigrationNotifications |
模型迁移通知 |
这些是 React hooks (useXxx),与 settings.json 中的 Hooks 系统是不同层面的概念。 它们是 UI 层面的通知组件,不参与 hook 执行引擎。
Hooks 系统中的 Notification 事件用于拦截通知发送,通过 executeNotificationHooks()
触发,matcher 匹配 notification_type 字段。
{
"hooks": {
// PreToolUse: 在 Write/Edit 前运行 lint
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"Remember to follow project style guide\"}}'",
"timeout": 10
}
]
}
],
// PostToolUse: 编辑后自动格式化
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$(echo $ARGUMENTS | jq -r '.tool_input.file_path // .tool_input.file_name // empty')\"",
"async": true
}
]
}
],
// Stop: 使用 LLM 验证任务完成
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Verify that all modified files have proper error handling. $ARGUMENTS",
"model": "claude-sonnet-4-6"
}
]
}
],
// Stop: 使用 Agent 深度验证
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that unit tests pass and coverage is above 80%.",
"timeout": 120
}
]
}
],
// PermissionRequest: 自动批准 Read 操作
"PermissionRequest": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"allow\"}}}'"
}
]
}
],
// HTTP hook: 通知外部系统
"SessionStart": [
{
"hooks": [
{
"type": "http",
"url": "https://hooks.example.com/claude-session",
"headers": {
"Authorization": "Bearer $API_TOKEN"
},
"allowedEnvVars": ["API_TOKEN"]
}
]
}
],
// CwdChanged: 目录切换时加载环境变量
"CwdChanged": [
{
"hooks": [
{
"type": "command",
"command": "if [ -f .envrc ]; then echo 'export FOO=bar' > \"$CLAUDE_ENV_FILE\"; fi"
}
]
}
],
// FileChanged: 监视配置文件变更
"FileChanged": [
{
"matcher": ".envrc|.env",
"hooks": [
{
"type": "command",
"command": "echo 'Config file changed, reloading environment'"
}
]
}
]
},
// HTTP hook URL 允许列表
"allowedHttpHookUrls": [
"https://hooks.example.com/*",
"https://api.mycompany.com/webhooks/*"
],
// HTTP hook 允许的环境变量
"httpHookAllowedEnvVars": ["API_TOKEN", "WEBHOOK_SECRET"],
// 管理策略选项
"allowManagedHooksOnly": false,
"disableAllHooks": false
}Hook 配置从多个来源合并:
| 来源 | 路径 | 优先级 |
|---|---|---|
| User Settings | ~/.claude/settings.json |
最高(用户级) |
| Project Settings | .claude/settings.json |
项目级 |
| Local Settings | .claude/settings.local.json |
本地覆盖 |
| Policy Settings | 管理策略 | 策略级(可限制其他来源) |
| Plugin Hooks | ~/.claude/plugins/*/hooks/hooks.json |
插件级 |
| Session Hooks | 内存 | 运行时动态 |
| Built-in Hooks | 代码内注册 | 内部 |
所有 hook 执行前必须通过信任检查:
// src/utils/hooks.ts
export function shouldSkipHookDueToTrust(): boolean {
const isInteractive = !getIsNonInteractiveSession()
if (!isInteractive) return false // SDK 模式隐式信任
return !checkHasTrustDialogAccepted()
}这是一个纵深防御措施,防止未经信任的工作区执行任意命令。 历史漏洞包括: SessionEnd hooks 在用户拒绝信任对话框时执行、 SubagentStop hooks 在信任建立前执行。
- URL 允许列表:
allowedHttpHookUrls限制可访问的 URL 模式 - SSRF 防护:
ssrfGuardedLookup阻止私有 IP 和链路本地地址 - Header 注入防护:
sanitizeHeaderValue移除 CR/LF/NUL 字节 - 环境变量控制: 仅
allowedEnvVars中的变量会被插值 - 沙箱代理: sandbox 模式下请求经过代理,由代理强制域名白名单
// policySettings.disableAllHooks: 完全禁用所有 hook(包括管理策略自身的)
shouldDisableAllHooksIncludingManaged(): boolean
// policySettings.allowManagedHooksOnly: 仅允许管理策略定义的 hook
shouldAllowManagedHooksOnly(): booleanexport interface HookResult {
hook: HookCommand | HookCallback | FunctionHook
outcome: 'success' | 'blocking' | 'non_blocking_error' | 'cancelled'
message?: HookResultMessage
systemMessage?: string
blockingError?: HookBlockingError
preventContinuation?: boolean
stopReason?: string
permissionBehavior?: 'ask' | 'deny' | 'allow' | 'passthrough'
hookPermissionDecisionReason?: string
additionalContext?: string
updatedInput?: Record<string, unknown>
updatedMCPToolOutput?: unknown
permissionRequestResult?: PermissionRequestResult
elicitationResponse?: ElicitationResponse
watchPaths?: string[]
retry?: boolean
}export type AggregatedHookResult = {
message?: HookResultMessage
blockingError?: HookBlockingError
preventContinuation?: boolean
stopReason?: string
permissionBehavior?: PermissionResult['behavior']
hookPermissionDecisionReason?: string
hookSource?: string
additionalContexts?: string[]
updatedInput?: Record<string, unknown>
updatedMCPToolOutput?: unknown
permissionRequestResult?: PermissionRequestResult
watchPaths?: string[]
elicitationResponse?: ElicitationResponse
elicitationResultResponse?: ElicitationResponse
retry?: boolean
}executeHooks() 是一个 async generator,逐个 yield AggregatedHookResult。
权限行为按优先级聚合: deny > ask > allow > passthrough。
blocking error 立即 yield,不阻止后续 hook 执行。
additionalContext 会追加为 hook_additional_context 系统消息。
Command Hook 支持交互式 prompt 请求,用于在 hook 执行过程中向用户提问:
// Hook 进程输出 JSON 请求:
{
"prompt": "request-id",
"message": "请选择操作方式:",
"options": [
{ "key": "1", "label": "方式 A", "description": "使用方式 A 处理" },
{ "key": "2", "label": "方式 B" }
]
}
// 系统通过 stdin 返回用户选择:
{
"prompt_response": "request-id",
"selected": "1"
}请求和响应通过 stdin/stdout 序列化传输,异步 prompt 处理链保证顺序。
Hook 执行产生以下遥测事件:
| 事件 | 触发点 |
|---|---|
tengu_run_hook |
Hook 开始执行 |
tengu_repl_hook_finished |
Hook 批次执行完成 |
tengu_agent_stop_hook_success |
Agent hook 成功 |
tengu_agent_stop_hook_error |
Agent hook 错误 |
tengu_agent_stop_hook_max_turns |
Agent hook 达到最大轮数 |
hook_execution_start (OTEL) |
Beta tracing -- hook 开始 |
hook_execution_complete (OTEL) |
Beta tracing -- hook 完成 |
此外,hook_duration_ms 指标通过 StatsStore 记录,
addToTurnHookDuration 累计到每轮的 hook 执行时间。