# Hook 生命周期模式

## 问题

如果没有中心化的 hook 生命周期，扩展性会退化为混乱：hook 作者在 Agent 执行的任意点附加副作用，没有一致的信任执行，并发 hook 之间没有确定性排序，也没有结构化的方式将阻止决策传回调用者。外部 hook 可能在工作区信任建立之前执行，导致从不受信的配置文件进行远程代码执行。长时间运行的 hook 如果在没有正确注册表交接的情况下自行转为后台运行，会留下孤儿进程，在意想不到的时间传递过时或矛盾的信号。

这些问题出现在任何支持可扩展 hook 点的 Agent 运行时中——并非特定于某个具体实现。

## 黄金法则

### 所有 hook 流经单一分发点

永远不要让 hook 执行分散在多个调用处。单一分发函数是信任检查、来源合并、类型路由和结果聚合的咽喉。如果添加新的 hook 类型，分发点是唯一需要更改的地方。分散的执行处在信任执行或结果处理上不可避免地会偏离。

### 信任是全有或全无的

在任何 hook 触发之前，恰好检查一次工作区信任。如果信任尚未建立，跳过所有 hook——不仅仅是外部的。包括进程内 hook。这防止了一种半信任状态，其中某些 hook 执行而另一些不执行，这种状态比干净地全部跳过更难调试，产生不可预测的行为。

### 多个来源以显式优先级合并

Hook 来自多个来源：持久化配置、编程 SDK 注册和临时的会话作用域注册。将它们合并为具有定义优先级分层的单一有序列表。当企业策略限制 hook 时，整个来源层被干净地排除，而非逐个过滤 hook。

### 退出码承载语义权重

对于外部进程 hook，为特定退出码保留特定含义。一个码表示成功。一个特定码表示"阻止此操作并将我的错误消息注入 Agent 的上下文"。所有其他非零码表示"警告用户但不阻止"。这种三级约定让外部脚本无需结构化 JSON 输出即可控制 Agent 行为。

### 拒绝优于询问优于允许

当同一批次中的多个 hook 返回冲突的权限决策时，应用严格的优先级：拒绝覆盖询问，询问覆盖允许，穿透永远不设置聚合决策。这种确定性解析在 hook 不一致时消除了歧义。

### 会话 hook 在设计上是临时的

在运行时为特定 Agent 会话注册的 hook 必须作用域限定于该会话的 ID，并在会话结束时自动清理。它们永远不应写入持久化配置。如果子 Agent 注册了一个 hook，该 hook 不能在父 Agent 的会话或兄弟子 Agent 的会话中触发。这种隔离防止并行扇出期间的跨会话副作用。

## 适用场景

- 你的 Agent 运行时暴露了外部代码可以 hook 的生命周期事件。
- 你需要任何 hook 类型都无法绕过的信任执行。
- Hook 来自多个必须确定性合并的配置来源。
- 你支持同步阻塞 hook 和长时间运行的后台 hook。
- 多个 hook 可能对同一操作返回冲突的权限决策。
- 你需要会话结束时自动清理的会话作用域 hook。

## 权衡

| 决策 | 收益 | 代价 |
|---|---|---|
| 单一分发点 | 信任、路由和聚合集中一处 | 每种 hook 类型都必须符合相同的分发接口 |
| 全有或全无的信任门控 | 无半信任状态，简单的心智模型 | 不构成安全风险的进程内 hook 也被阻止 |
| 多来源合并与优先级 | 干净的策略执行，可预测的排序 | hook 作者必须理解自己属于哪个来源层 |
| 多种 hook 类型 | 每项工作有合适的工具（进程、LLM、HTTP、进程内） | 更多类型面需要文档和维护 |
| 异步生成器组合 | 流式部分结果，阻塞错误可早期退出 | 调用者必须处理增量累积 |
| 通过注册表交接的后台化 | 长时间运行的 hook 不阻塞主循环 | 如果跳过注册表交接有孤儿风险 |
| 会话作用域 hook | 临时 hook 在会话结束时自动清理 | 作用域限定于一个会话的 hook 不在子 Agent 会话中触发 |
| 跨来源去重 | 同一 hook 出现在多个配置中时不会重复触发 | 去重键设计必须考虑来源特定前缀 |

## 实现模式

- 将每次 hook 调用路由到单一分发函数。此函数是信任检查、来源合并、类型分发和结果聚合的唯一位置。
- 将信任检查实现为分发中的第一个操作。如果信任缺失，立即返回空结果——不评估哪些 hook 本应匹配。
- 按定义的优先级顺序合并 hook 来源（如持久化配置、SDK 注册、会话作用域）。当策略标志限制为仅管理的 hook 时，排除整个来源层而非过滤单个条目。
- 使用包含来源特定上下文的复合键跨来源去重。来自不同插件的两个相同命令的 hook 是不同的；来自用户配置和项目配置的相同命令折叠为后者优先的条目。
- 至少支持以下 hook 类型类别：外部进程（shell 命令）、LLM 子调用、子 Agent 委派、HTTP 端点、具有完整输出控制的进程内回调，以及用于简单允许/拒绝决策的轻量布尔门控。
- 使用异步生成器组合并行运行所有匹配的 hook。每个 hook 在可用时产出类型化的结果片段。分发函数合并所有生成器并向调用者产出聚合的片段。
- 对于外部进程 hook，将退出码映射到语义结果：一个码表示成功，一个保留码表示阻塞错误（错误消息在 stderr 上），所有其他非零码表示非阻塞警告。
- 支持两种后台化模式：静态（在 hook 配置中于执行前声明）和动态（hook 将后台化信号作为第一行输出发出）。后台化的 hook 被交接到待处理 hook 注册表，以便跨轮次跟踪。
- 将会话 hook 作用域限定于特定的 Agent 或会话 ID。主会话下注册的 hook 不在子 Agent 会话中触发。提供显式移除函数，但也在会话结束时自动清除所有会话 hook。
- 聚合多个 hook 的权限决策时，应用严格优先级：拒绝优于询问，询问优于允许，穿透被忽略。
- 根据所需能力选择 hook 类型。使用外部进程 hook 进行 shell 级副作用。使用 LLM 子调用 hook 当 hook 本身需要推理时。使用 HTTP hook 进行外部服务集成。使用进程内回调当 hook 需要对结构化输出字段（权限决策、注入上下文、输入重写）的完全控制时。使用轻量布尔门控当你只需要对对话历史进行是/否决策而无需 JSON 序列化开销时。
- 注册进程级清理处理器在关闭时终止所有运行中的 hook。没有这个，后台化的 hook 会在父进程之后存活成为孤儿。
- 防范事件类型限制：某些事件可能与某些 hook 类型不兼容。例如，在早期会话设置期间的 HTTP hook 可能在响应消费者准备好之前触发事件时死锁。在分发层而非依赖 hook 作者自行发现来记录这些限制。

## 踩坑指南

**信任门控阻止所有 hook 类型，不仅仅是外部的。** 每个 hook——包括进程内回调和布尔门控——在信任缺失时都被跳过。永远不要因为 hook 在进程内运行就假设它已触发。如果 hook 未触发，首先检查信任状态。

**缺失的外部脚本可能产生与故意阻止相同的退出码。** 如果 hook 脚本在注册和执行之间被删除，shell 本身可能以保留的阻塞码退出。在注册时而非仅在执行时验证 hook 脚本存在。

**策略限制模式静默丢弃会话 hook。** 当企业策略将 hook 限制为仅管理来源时，技能或子 Agent 注册的会话作用域 hook 收到零匹配且无错误。管理和非管理部署之间的测试行为可能不同。

**带"唤醒"语义的后台 hook 绕过待处理注册表。** 正常后台化的 hook 出现在注册表中，在下一轮之前被解决。唤醒式 hook 设计上跨轮次存活——它们在以阻塞退出码完成时注入任务通知来唤醒模型，而非出现在待处理 hook 注册表中。不要对必须在下一次工具调用前完成的 hook 使用唤醒。

**布尔门控 hook 不可序列化。** 仅返回 true/false 的轻量进程内门控本质上是临时的。它们被排除在配置快照、分析和遥测之外。如果需要持久性或可观测性，使用完整回调类型。

**去重使用后者优先，可能让多作用域作者意外。** 当相同的 hook 命令同时出现在用户级和项目级配置中时，项目级条目胜出。去重键中的来源特定前缀防止跨来源冲突，但同来源冲突被静默解决。

**HTTP hook 在早期生命周期事件中可能死锁。** 如果 HTTP hook 为会话启动或设置事件注册，出站请求可能在响应消费者准备好之前触发，造成死锁。分发层应显式过滤不兼容的 hook 类型/事件类型组合，而非留给 hook 作者处理。

**会话 hook 在会话清理后不可见。** 一旦会话结束且其 hook 被清除，不留下它们曾存在的记录。如果调试需要理解过去会话中哪些 hook 是活跃的，你需要单独的日志——会话存储本身是临时的。

## Claude Code 实证

Claude Code 的 hook 系统是这些原则的生产实现，支持六种 hook 类型，覆盖数十个生命周期事件。

**带信任门控的单一分发。** 所有 hook 执行流经一个异步生成器函数。它做的第一件事是检查工作区信任。在交互模式下，这要求用户已接受信任对话框。在 SDK（非交互）模式下，信任是隐含的。如果信任缺失，生成器立即返回——不触发任何类型的 hook。

**三层来源合并与策略执行。** Hook 按优先级顺序从三个来源提取：来自设置文件的快照 hook、SDK 注册的回调 hook，以及存储在基于 Map 的会话状态中的会话作用域 hook。当企业策略设置仅管理标志时，会话和插件 hook 在来源合并层被排除，而非逐个过滤。这意味着策略执行是一个单一条件，丢弃整个来源列表，而非需要为每种新 hook 类型维护的逐 hook 过滤。

**流式结果的异步生成器组合。** 每个 hook 作为独立的异步生成器运行，产出类型化的结果片段——阻塞错误、权限决策、额外上下文、修改后的输入。并行合并工具组合所有生成器，使调用者在片段到达时接收它们。这对阻塞错误至关重要：批次中的慢 hook 不会延迟快 hook 的阻塞信号。

**基于 Map 的会话状态用于并发。** 会话 hook 对会话存储使用 Map 而非普通对象。Map 修改返回相同的容器引用，因此应用存储的相等性检查看不到变化，存储的监听器都不触发。这避免了并行子 Agent 扇出期间的二次方监听器通知成本，此时许多 hook 可能快速连续注册。设计教训是通用的：当存储支持的数据结构在并发操作期间接收频繁写入时，选择一个其修改不触发变更检测级联的容器。

**两种模式的后台化。** Hook 可以静态后台化（在配置中声明）或动态后台化（hook 在做实际工作之前发出后台化信号作为第一行输出）。更进一步的变体，"唤醒"后台化，用于必须跨多个 Agent 轮次存活的 hook——在以阻塞退出码完成时，它们注入任务通知来唤醒模型，而非出现在待处理 hook 注册表中。这种三级后台化设计反映了现实世界中 hook 生命周期的范围：同轮次、下一轮次和跨多轮次。

**权限优先级。** 当多个 hook 对同一工具使用事件返回冲突的权限决策时，聚合遵循严格优先级：拒绝覆盖询问，询问覆盖允许，穿透被忽略。这确保单个注重安全的 hook 始终可以否决，无论有多少宽松的 hook 也回应。

**六种 hook 类型各有独特角色。** 系统支持命令 hook（shell 进程）、prompt hook（LLM 子调用）、Agent hook（完整子 Agent 委派）、HTTP hook（要求 JSON 响应的外部服务调用）、回调 hook（具有完整结构化输出控制的进程内函数），以及函数 hook（仅对对话历史的会话级布尔门控）。回调/函数的分离是刻意的：回调遵循与外部 hook 相同的协议，可以影响权限、注入上下文和重写输入。函数 hook 仅接收消息历史并返回布尔值——它们存在是为了无 JSON 序列化开销的轻量验证，且永远不被持久化或包含在遥测中。

**退出码纪律。** 外部命令 hook 遵循严格的三级约定：退出 0 是成功，退出 2 是阻塞错误（其 stderr 消息被注入模型的上下文），任何其他非零退出是非阻塞警告——向用户显示但不停止执行。这一约定让 shell 脚本作者仅使用进程退出码就能细粒度地控制 Agent 行为。

**来源感知键的去重。** 当相同的 hook 命令出现在多个设置作用域（用户、项目、工作区本地）时，合并层使用复合键去重。对于来自同一来源的 hook，后者优先。对于来自不同插件根的 hook，去重键中的来源特定前缀防止跨插件冲突被静默折叠。
