第三十九章:Hooks 为什么更像生命周期总线
把配置型 hook、插件 hook、session hook 和内部 post-turn 处理放回同一套 lifecycle system。
1. 为什么这章必须单独拆
前面的很多章节都碰到过 hooks:
- tool runtime
- permission runtime
- compact
- session memory
- plugin hooks
但如果一直把 hook 当成“某个点位支持回调”,就看不出 Claude Code 在这层真正做了什么。
从源码看,它已经是一套相当完整的 lifecycle system。
相关源码锚点:
src/utils/hooks/sessionHooks.tssrc/utils/hooks/hooksConfigManager.tssrc/utils/hooks/AsyncHookRegistry.tssrc/query/stopHooks.tssrc/utils/plugins/loadPluginHooks.ts
2. 先说结论
我对这一块的判断是:
Claude Code 的 hook runtime 更像生命周期总线,不只是外部扩展点。
最重要的结论有四个:
- hook 事件面覆盖了 tool、session、permission、compact、协作等多条链。
- hook 来源不只一种,至少有配置型 hook、内存函数 hook、插件 hook。
- 内部后台能力也复用 hook runtime,而不是单独再造一套事件系统。
Stop钩子是一个特殊收口点,很多后台行为都在这里聚合。
3. 总体结构图
flowchart TD
A["query / tool / permission / session 事件"] --> B["hooksConfigManager<br/>事件定义与 matcher 语义"]
C["sessionHooks / function hooks"] --> D["SessionHooksState"]
E["plugin hook configs"] --> F["loadPluginHooks"]
B --> G["AsyncHookRegistry"]
D --> G
F --> G
G --> H["外部 hook 命令 / 内部回调"]
H --> I["阻塞结果 / 异步进度 / response attachments"]
J["stopHooks.ts"] --> G
J --> K["promptSuggestion / SessionMemory / autoDream 等内部后处理"]
4. 事件面:它覆盖的不是一个点,而是一整条生命周期
源码锚点:
src/utils/hooks/hooksConfigManager.ts
这里定义的事件已经很能说明问题了。
包括但不限于:
PreToolUsePostToolUsePostToolUseFailurePermissionRequestPermissionDeniedUserPromptSubmitSessionStartSessionEndStopStopFailurePreCompactPostCompactSubagentStartSubagentStopElicitationElicitationResultConfigChangeInstructionsLoadedWorktreeCreateWorktreeRemoveCwdChangedFileChanged
这已经不是“工具前后埋个回调”,而是正式的 runtime lifecycle taxonomy。
5. Hook 来源:至少三层
源码锚点:
src/utils/hooks/sessionHooks.tssrc/utils/plugins/loadPluginHooks.ts
5.1 配置型 / 插件型 hook
这类 hook 来自:
- settings
- plugin manifests / configs
特点是:
- 声明式
- 可热更新
- 通常会落到 matcher + command 执行
5.2 Session function hook
sessionHooks.ts 里还有一层更轻的能力:
addSessionHookaddFunctionHookremoveFunctionHookremoveSessionHook
它们是内存态的 TS 回调,更像“当前 session 临时挂一个函数型监听器”。
5.3 内部 runtime hook
这一层特别重要。
像 session memory、prompt suggestion、autoDream 这些内部功能,也会借道 hook runtime。
这说明 Claude Code 不是把 hook 当插件专用扩展口,而是把它当成 runtime 级事件总线。
6. SessionHooksState:为什么要用 Map,而不是普通对象
源码锚点:
src/utils/hooks/sessionHooks.ts
这里作者专门用了:
Map<string, SessionStore>
目的也写得很清楚:
- O(1) 变更
- 避免 store listener churn
这说明这一层已经进入“高频运行时通路”了,不再是偶发配置读取。
7. AsyncHookRegistry:它解决的是异步执行的现实问题
源码锚点:
src/utils/hooks/AsyncHookRegistry.ts
这一层负责的不是事件定义,而是异步生命周期管理:
- 挂起中的 hook 跟踪
- 进度 interval
- 完成收口
- response attachments
还有一个很能说明问题的细节:
SessionStart完成后会触发 session env cache 失效
这意味着 hook 的完成结果本身,也会反向影响 runtime 环境。
8. stopHooks.ts:为什么它是特殊收口点
源码锚点:
src/query/stopHooks.ts
这一层特别值得单独说。
因为它不只是“执行 Stop hooks”。
它还会顺带触发一批内部后处理:
- 模板作业分类
- prompt suggestion
- memory extraction
- autoDream
- computer-use cleanup
然后才去跑真正的 Stop hooks,再汇总阻塞错误和 stop summary。
这说明 Stop 在 Claude Code 里是一个:
主回合结束后的统一收口相位。
很多不适合塞进主循环、但又必须借当前上下文做的事,都会集中挂到这里。
9. Hook 语义:不是所有 hook 都只是“通知一下”
从 hooksConfigManager.ts 可以看出,每类 hook 还有自己的语义差异:
- matcher 字段不同
- exit code 行为不同
- 有的能阻塞
- 有的只做通知
- 有的要传 tool / permission / file 之类的不同上下文
所以这层真正的抽象不是 eventName -> callback[],而是:
带事件类型、匹配规则、执行语义和失败策略的生命周期协议。
10. 对 C# 拆分最有用的建议
如果做 C# 版,这一层至少适合拆成四块:
10.1 HookEventCatalog
负责:
- 事件定义
- matcher schema
- 失败 / 阻塞语义
10.2 HookRegistry
负责:
- session hook
- plugin hook
- config hook
- 生命周期注册与卸载
10.3 HookExecutor
负责:
- 同步/异步执行
- timeout
- progress
- attachment 汇总
10.4 PostTurnPipeline
负责:
- Stop phase 内部后处理
- session summary
- prompt suggestion
- 后台 consolidation job 触发
11. 哪些值得抄,哪些不要抄
值得抄的:
- 用统一 lifecycle taxonomy 收束 hooks
- 内部后台能力和外部扩展共用事件总线
- 为异步 hook 单独建 registry,而不是塞进普通事件派发器
不要原样抄的:
- 第一版就把所有事件面一次做满
- 让 Stop phase 变成任何杂活都往里塞的黑洞
- 让每种 hook 自己定义一套失败语义
12. 一句话收口
Claude Code 的 hooks 运行时,本质上是一条“可被插件、会话和内部系统共同订阅的生命周期总线”。