Book 第三十九章:Hooks 为什么更像生命周期总线
第六部分:迁移与附录

第三十九章: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.ts
  • src/utils/hooks/hooksConfigManager.ts
  • src/utils/hooks/AsyncHookRegistry.ts
  • src/query/stopHooks.ts
  • src/utils/plugins/loadPluginHooks.ts

2. 先说结论

我对这一块的判断是:

Claude Code 的 hook runtime 更像生命周期总线,不只是外部扩展点。

最重要的结论有四个:

  1. hook 事件面覆盖了 tool、session、permission、compact、协作等多条链。
  2. hook 来源不只一种,至少有配置型 hook、内存函数 hook、插件 hook。
  3. 内部后台能力也复用 hook runtime,而不是单独再造一套事件系统。
  4. 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

这里定义的事件已经很能说明问题了。

包括但不限于:

  • PreToolUse
  • PostToolUse
  • PostToolUseFailure
  • PermissionRequest
  • PermissionDenied
  • UserPromptSubmit
  • SessionStart
  • SessionEnd
  • Stop
  • StopFailure
  • PreCompact
  • PostCompact
  • SubagentStart
  • SubagentStop
  • Elicitation
  • ElicitationResult
  • ConfigChange
  • InstructionsLoaded
  • WorktreeCreate
  • WorktreeRemove
  • CwdChanged
  • FileChanged

这已经不是“工具前后埋个回调”,而是正式的 runtime lifecycle taxonomy。

5. Hook 来源:至少三层

源码锚点:

  • src/utils/hooks/sessionHooks.ts
  • src/utils/plugins/loadPluginHooks.ts

5.1 配置型 / 插件型 hook

这类 hook 来自:

  • settings
  • plugin manifests / configs

特点是:

  • 声明式
  • 可热更新
  • 通常会落到 matcher + command 执行

5.2 Session function hook

sessionHooks.ts 里还有一层更轻的能力:

  • addSessionHook
  • addFunctionHook
  • removeFunctionHook
  • removeSessionHook

它们是内存态的 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 运行时,本质上是一条“可被插件、会话和内部系统共同订阅的生命周期总线”。