事件 Hooks 系统
昨天(Day 13)Skills 是"你主动触发一段流程";今天 Hooks 反过来——系统在生命周期关键节点自动回调你配好的逻辑(工具执行前拦一下、模型想收工时检查一下)。贴真实的事件枚举,讲清配置与拦截,并和 React UI hooks 划清界限。
先分清两种 hook(最容易懵)
rm -rf、改完代码不格式化、测试没过就说"搞定了"。你没法整天盯着屏幕手动拦。怎么办?项目里 "hook" 指两种完全不同的东西,务必分清:
React UI hooks(不是今天主题)
src/hooks/*.ts 里绝大多数 useXxx——如 useMergedTools、useCanUseTool。它们是 Ink/React 的界面状态钩子(Day 04/06/09 见过)。事件 Hooks(今天主题)
src/utils/hooks.ts(5190 行)+ src/utils/hooks/。settings.json 驱动的生命周期钩子——PreToolUse/PostToolUse/Stop 等。src/hooks/useXxx.ts 是 UI;src/utils/hooks.ts 和 src/utils/hooks/ 是事件系统。今天只讲后者。Hook 是什么:在节点插入你的逻辑
事件 Hook 让你在 Agent 干活的关键节点插入自定义命令/脚本。典型用途:
- 工具执行前(PreToolUse)拦一下——"不许 Bash 跑
rm"、"Edit 前先备份"。 - 工具执行后(PostToolUse)做点事——"改完 .ts 文件自动跑 prettier"。
- 模型想收工时(Stop)检查——"测试没过不许停"(Day 10 讲的 Stop hook 续命)。
- 会话开始/结束、提交 prompt 时、压缩前后……
27 种事件(真实枚举)
HOOK_EVENTS(src/entrypoints/sdk/coreTypes.ts:25)——真实的完整枚举,共 27 个:
export const HOOK_EVENTS = [
'PreToolUse', 'PostToolUse', 'PostToolUseFailure',
'Notification', 'UserPromptSubmit', 'SessionStart', 'SessionEnd',
'Stop', 'StopFailure', 'SubagentStart', 'SubagentStop',
'PreCompact', 'PostCompact', 'PermissionRequest', 'PermissionDenied',
'Setup', 'TeammateIdle', 'TaskCreated', 'TaskCompleted',
'Elicitation', 'ElicitationResult', 'ConfigChange',
'WorktreeCreate', 'WorktreeRemove', 'InstructionsLoaded',
'CwdChanged', 'FileChanged',
] as const
怎么配(settings.json)
Hook 配在 settings.json 里,结构是"事件 → 匹配器 → 要跑的 hook":
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", // 只对 Bash 工具触发
"hooks": [{ "type": "command", "command": "./guard.sh", "timeout": 5000 }] }
],
"PostToolUse": [
{ "matcher": "Edit|Write", // 正则:Edit 或 Write
"hooks": [{ "type": "command", "command": "prettier --write $FILE" }] }
]
}
}
matcher 决定"这个 hook 对哪些情况触发"——工具类事件用工具名(支持正则 Edit|Write),无 matcher 视为匹配全部。Hook 来源和 Day 09 权限一样分层(用户/项目/本地/策略),另加插件 hook、session hook(如技能注册的,Day 13)、内置 hook。
退出码约定:hook 怎么影响流程
command 型 hook 是跑一个 shell 命令,它用退出码 + stderr 来影响 Agent(hooksConfigManager.ts:26 的 getHookEventMetadata)。以 PreToolUse 为例:
exit 0放行,不显示 stdout(一切正常)
exit 2★ 阻止工具调用,并把 stderr 内容给模型(告诉它为什么被拦)
其它码只给用户看(不给模型),但工具继续执行
guard.sh 内容:grep -q 'rm -rf' && { echo "危险命令,已阻止">&2; exit 2; }; exit 0 → 模型想跑 rm -rf → 脚本 exit 2 → 工具不执行,模型收到 stderr "危险命令,已阻止",于是换个更安全的做法。guard.sh 检测到模型要 rm -rf,就 echo "危险命令,已阻止" >&2; exit 2——工具不执行,模型看到 stderr 里的原因、换个做法。UserPromptSubmit 的 exit 2 更狠:会擦除你的原始 prompt 并阻止处理。用退出码当"信号"是 Unix 老传统(0=成功,非0=各种失败),这里用它做"hook 能不能拦住流程"的约定,简单有效。stderr = 标准错误输出(>&2 把内容写到它)。匹配与执行
匹配 getMatchingHooks(src/utils/hooks.ts:1739):拿到当前事件的所有 matcher → 按事件类型算 matchQuery(工具事件用 tool_name、SessionStart 用 source、FileChanged 用文件名)→ matchesPattern 过滤 → 标注来源、去重。
执行:各事件有独立入口——executePreToolHooks(:3538)、executePostToolHooks(:3594)、executeStopHooks(:3791)等,都是 async generator 逐条 yield 进度。command 型用 spawn 起子进程执行;启用沙箱时还加 network-only 沙箱。
工具侧的接线在 src/services/tools/toolHooks.ts——Day 07 执行管线里的第 5 关(PreToolUse)和第 9 关(PostToolUse)就是调这里。PreToolUse hook 甚至能返回权限决策(allow/ask/deny)、改输入、或直接中止(Day 07 讲过)。
五种 hook 形态
hook 不只有"跑命令"一种。type 字段可选五种(hooksSettings.ts):
| type | 做什么 |
|---|---|
command | 跑一个 shell 命令(最常用,退出码约定见 L05) |
prompt | 喂给一个 LLM 判断(让模型来决定拦不拦) |
agent | 跑一个子代理(Day 15) |
http | 打一个 HTTP 回调(通知外部系统) |
function/callback | 内部程序化 hook(代码里注册的) |
hookEvents.ts):把 hook 执行状态推给 UI,和执行逻辑解耦。今日小结 + 动手
🧠 今天你应该能回答
- 两种 hook 怎么区分?(src/hooks 是 React UI;src/utils/hooks 是事件系统)
- 事件 hook 和命令/skill 的区别?(自动触发 vs 主动调用)
- 27 种事件覆盖了 Agent 哪些阶段?
- PreToolUse 退出码 0/2/其它分别什么效果?
- 五种 hook 形态?prompt 型为什么灵活?
✋ 动手:对着真实代码读一遍
# 1. 27 种事件枚举(L03)
sed -n '25,53p' src/entrypoints/sdk/coreTypes.ts
# 2. 退出码约定(L05)
sed -n '26,90p' src/utils/hooks/hooksConfigManager.ts
# 3. 匹配与执行入口(L06)
grep -n "getMatchingHooks\|executePreToolHooks\|executePostToolHooks\|executeStopHooks" src/utils/hooks.ts | head
# 4. 工具侧接线(对应 Day 07 管线第 5/9 关)
sed -n '19,50p' src/services/tools/toolHooks.ts
# 5. 配一个 PreToolUse hook 拦 Bash(settings.json)
# hooks.PreToolUse[].matcher="Bash", command="echo blocked>&2;exit 2"