钩子挂在事件上。事件按生命周期分五组:会话级、工具调用、子智能体、完成、较新。 十七个事件里最关键的维度不是「在什么时候触发」, 是「能不能阻止」——能在事前插手的只有那几个。
上一节说钩子的钥匙在配置手上,这一节讲配置挂在哪儿:事件。 这一节的表可以当查询用:想在哪个时刻插手,就去那一组里找能阻止的事件。
一、五组事件
会话级:会话开始、用户提交一句话、会话结束、通知。这一组管的是「一次对话的边界」。 工具调用:调用前、调用后。这一组是钩子最密集的地方——因为每一个工具调用都会经过它们。 子智能体:启动、结束。这一组管的是「隔离出去的那段活」,第 4 章的东西在这里被约束。
完成:模型认为「我干完了」的那一刻。这一组是质量门控的落点。 较新:压缩上下文等,属于后来补上的一类,行为和老事件一样,只是时机更细。 分组的意义是定位:你想在「事前」「事中」「事后」还是「收尾」插手,先选组再选事件。
二、能不能阻止:最关键的维度
能阻止的事件,钩子返回一个否定决定,这次动作就不发生。 只能观察的事件,钩子怎么返回都没用——动作已经发生了,你只能补做、记录、或者打回让它重来。 这条区别决定了钩子能干什么:想在事情发生之前插手,只能用能阻止的那一类。
演示里十个事件有五个能阻止:用户提交、工具调用前、子智能体结束、完成、压缩前。 另外五个只能观察:会话开始、工具调用后、子智能体启动、会话结束、通知。 分不清这个维度,会写出一堆「我以为拦住了其实没拦住」的钩子——这是最常见的白干。
三、一次会话会触发多少次
演示里走了一遍完整会话:读一个文件、改三处、派两个子智能体、收尾,一共触发三十一次。 次数最多的是工具调用前和工具调用后,各九次——因为每一次工具调用都要经过两道。 所以挂在这两个事件上的钩子必须快。它慢五十毫秒,一次会话就多出将近一秒。
而会话级的事件一共才几次,挂在上面慢一点无所谓。 这个分布还解释了一件事: 为什么 5.4 节会强调「能用命令判就别用模型判」——因为命令判的那类钩子恰恰挂在最频繁的事件上。
四、三个最常用的事件
工具调用前(PreToolUse):拦截的主战场。危险命令、敏感文件、越界写,全在这里。 工具调用后(PostToolUse):补做的主战场。格式化、lint、审计、统计,全在这里。 完成(Stop):门控的主战场。测试没过、检查没跑、报告没写,全在这里打回。
三个事件刚好对应三个位置:事前拦、事后补、收尾卡。绝大多数工程钩子都是这三类。 其余的事件不是没用,是更专门:会话级管上下文注入,子智能体级管隔离区的进出。
五、选事件的顺序
第一步:这件事要拦,还是要补?要拦 → 只能选能阻止的事件,且必须在动作发生之前。 第二步:动作是「工具调用」还是「整段会话结束」?前者选工具调用组,后者选完成组。 第三步:要不要区分「谁触发的」?子智能体触发的和主窗口触发的可以分开管,那就选子智能体组。
三步走完,事件基本就定了。 定不下来通常说明这件事本身没想清——「我要在合适的时候检查一下」这种需求,做不出钩子。 一句话:钩子不是「有空就跑一下」,它是「在某个确定的时刻必须跑」。时刻说不清,就不是钩子。
六、动手 Demo:一次会话走完,哪些事件被触发了、哪些能拦
先按组列出常用事件,标出能不能阻止;再走一遍完整会话,数每个事件被触发几次。 教材列的是十七个,这里跑常用的十个——另外七个是同类细分,行为一样,只是时机更细。
EVENTS = [
(u"SessionStart", u"会话级", False),
(u"PreToolUse", u"工具调用", True), # 能阻止
(u"PostToolUse", u"工具调用", False), # 动作已发生
(u"Stop", u"完成", True),
]
for nm, n in SCRIPT: # 一次会话里各事件的触发次数
seq.extend([nm] * n)
实跑输出:
--------------------------------------------------------------------
一次会话走完,哪些事件被触发了、哪些能拦
--------------------------------------------------------------------
教材按生命周期把它们分成五组、共十七个事件。
这里跑常用的十个——另外七个是同类事件的细分,行为一模一样,只是时机不同。
① 事件分四组
--------------------------------------------------------------------
| 分组 | 事件 | 个数 |
|---|---|---:|
| 会话级 | SessionStart、UserPromptSubmit、SessionEnd、Notification | 4 |
| 工具调用 | PreToolUse、PostToolUse | 2 |
| 子智能体 | SubagentStart、SubagentStop | 2 |
| 完成 | Stop | 1 |
| 较新 | PreCompact | 1 |
② 哪些事件能「阻止」
--------------------------------------------------------------------
| 事件 | 分组 | 能否阻止 |
|---|---|---|
| SessionStart | 会话级 | 只能观察 |
| UserPromptSubmit | 会话级 | 能拦 |
| PreToolUse | 工具调用 | 能拦 |
| PostToolUse | 工具调用 | 只能观察 |
| SubagentStart | 子智能体 | 只能观察 |
| SubagentStop | 子智能体 | 能拦 |
| Stop | 完成 | 能拦 |
| SessionEnd | 会话级 | 只能观察 |
| Notification | 会话级 | 只能观察 |
| PreCompact | 较新 | 能拦 |
③ 一次真实会话的事件序列
--------------------------------------------------------------------
读一个文件、改三处、派两个子智能体、最后收尾,一共触发 31 次钩子。
| 事件 | 触发次数 | 占比 |
|---|---:|---:|
| SessionStart | 1 | 3% |
| UserPromptSubmit | 3 | 9% |
| PreToolUse | 9 | 29% |
| PostToolUse | 9 | 29% |
| SubagentStart | 2 | 6% |
| SubagentStop | 2 | 6% |
| Stop | 3 | 9% |
| SessionEnd | 1 | 3% |
| PreCompact | 1 | 3% |
次数最多的是 PreToolUse 和 PostToolUse,各九次——因为每一个工具调用都会经过它们。
也是因此,挂在它们上面的钩子必须快。
④ 「能否阻止」为什么是最关键的维度
--------------------------------------------------------------------
能阻止的事件,钩子可以返回一个否定决定,让这次动作不发生。它适合做护栏:危险命令、敏感文件。
只能观察的事件,钩子怎么返回都没用——动作已经发生了。它适合做记录和补充:审计、格式化。
分不清这个维度,会写出一堆「我以为拦住了其实没拦住」的钩子。
! 记一句就够:想在事情发生之前插手,只能用能阻止的那一类事件。
事后补做的,一律挂在只能观察的事件上。
①分组只是定位用:想在事前、事中、事后还是收尾插手,先选组再选事件。 ②能不能阻止那一列才是重点。 十个事件里五个能阻止:用户提交、工具调用前、子智能体结束、完成、压缩前。 另外五个只能观察:会话开始、工具调用后、子智能体启动、会话结束、通知。 这一列分不清,就会写出一堆「以为拦住了其实没拦住」的钩子。 ③一次会话触发三十一次,其中工具调用前和工具调用后各九次,占了将近六成。 所以挂在这两个事件上的钩子必须快——它慢五十毫秒,一次会话就多出将近一秒。 会话级的事件一共才几次,慢一点无所谓。 ④最后一段把三个最常用的事件归了位:事前拦(工具调用前)、事后补(工具调用后)、收尾卡(完成)。 绝大多数工程钩子都是这三类。 记一句就够:想在事情发生之前插手,只能用能阻止的那一类事件。事后补做的,一律挂在只能观察的事件上。
30 秒实验
- 把「改三处」改成「改十处」,重跑第 ③ 步。触发总数变成多少?工具调用那两道占的比例有变化吗?
- 给会话加一次「压缩上下文」,重跑第 ③ 步。哪个事件的次数会跳一下?它为什么能阻止?
- 假设某个钩子挂在工具调用后、耗时一百毫秒,按第 ③ 步的次数算,一次会话多花多少时间?
常见误区
- 在只能观察的事件上写「拦截」逻辑。动作已经发生了,拦不住,只能补做或打回。
- 给工具调用后挂一个慢钩子。它一次会话要跑九次,慢一百毫秒就多出将近一秒。
- 以为事件越多越好,每个都挂一遍。事件密集的那两个位置上,钩子会互相拖慢。
- 把「有空检查一下」做成钩子。钩子要求在确定的时刻必跑,时刻说不清就不是钩子。
- 忽略子智能体那一组。隔离出去的那段活同样要受约束,否则它就是钩子的盲区。
自测三问(点开对答案)
1. 事件分哪五组? —— 会话级、工具调用、子智能体、完成、较新。分组只是定位,先选组再选事件。 2. 最关键的维度为什么是「能不能阻止」? —— 因为想在事情发生之前插手,只能用能阻止的事件。在只能观察的事件上写拦截逻辑,拦不住。 3. 为什么挂在工具调用前后的钩子必须快? —— 因为每个工具调用都要经过这两道,一次会话里它们各触发九次左右。慢一点就被放大成整段卡顿。事件定了,下一节看配置写在哪儿:六个位置,用途各不同,还有合并的坑。
- ☐ (本篇完,下一篇 5.3 配置体系:6 个位置,6 种用途)
相关笔记
- 钩子的定位:为什么只有钩子不漏
- 配置体系:六个位置怎么选
- Claude Code 橙皮书 · 索引
- 🐎 Claude Code 实战 · 索引