每个钩子都是一道疤:84 次智能体故障凝成的代码
在我的智能体编排系统里,84 个钩子拦截着 Claude Code 截至 v2.1.116(2026 年 4 月)所暴露的 26 种生命周期事件中的 15 种。每个钩子都是一段 shell 脚本或 Python 代码,在某个特定的智能体动作前后触发:读文件、写文件、bash 命令、网络请求、派生子智能体、git 操作、MCP 工具调用。每个钩子之所以存在,都是因为曾经出过事。
智能体编排系统里的每一个钩子,都能追溯到一次具体的生产事故;这套钩子集合因此成了以 shell 脚本形式固化下来的组织记忆。 智能体清空过 CDN 缓存,读过凭据文件,把根本没跑过的测试报告成”全部通过”,也曾偏离任务足足 40 分钟。每一次事故都留下一道小而确定的防护,在此后的每一次会话里默默触发。
不是理论上的错,是生产环境里的错。有智能体清空了一个承载着数百万请求的 CDN 缓存。有智能体试图写入 SSH 密钥。有智能体从未调用 pytest,却报告”所有测试通过”。还有智能体偏离任务之远,花了四十分钟去优化某个文件里的函数——那个文件与它接到的活儿毫无关系。
这些钩子没有一个是我事先设计出来的。我从没坐下来把自主 AI 智能体的失败模式一一列举,再写出预防性的管控。每个钩子都是被动的产物:出了事,我写一段脚本防止它重演,此后每一次会话里这段脚本都在无声地运行。这套钩子系统不是什么安全架构,它是一部伤疤集。
要点
- 缓存清空事故:一个智能体用一次获得授权的 API 调用清空了生产环境的 CDN 缓存。如今两个钩子(47 行代码)把破坏性操作关在一道人工输入的口令之后。
- 凭据读取事故:一个智能体把 API 令牌带进了上下文窗口。现在有一道路径匹配防护,拦下对凭据文件的读取,并记录对
.env文件的访问。 - 幽灵验证:一个智能体没跑 pytest 就报告”所有测试通过”。一个模糊措辞检测器把幽灵验证从占会话总数的 12% 压到了 2% 以下。
- 十二次偏离:六十天里,智能体有十二次被证实丢掉了自己的任务。阈值设为 0.30 的余弦相似度检测器如今每 25 次工具调用触发一轮。
- 分类体系:六个结构性故障类别覆盖了全部 84 个钩子。500 多次会话之后,全新的类别已经很罕见。系统每经历一次事故就更硬一分。
缓存清空:一次获得授权的调用如何搞垮生产环境
2026 年 3 月 21 日,我让一个智能体去查 resumegeni.com 上的市场页面为什么加载缓慢。它一开始查得很正常:读路由处理函数、检查数据库查询、分析模板渲染耗时。接着它判断,Cloudflare 里过期的缓存条目可能掩盖了真实的性能表现。
于是它调用了 mcp__cloudflare__cache_purge,参数是 purge_everything: true。
生产站点上所有已缓存的页面瞬间失效。CDN 从原本以 80–100 毫秒响应绝大多数请求,变成把每一个请求都转发给 Railway 源服务器。奥斯汀的市场页面从一秒以内变成 14,290 毫秒,纽约从一秒以内变成 6,891 毫秒。整站的每个页面,每次请求都要从冷启动的源站重新渲染。
智能体没做任何越权的事。它用合法的 MCP 工具、有效的凭据,调用了一个获得授权的 API 端点。如果你正在排查缓存行为,清空缓存是个合理的排查步骤。问题在于,”排查时合理”和”对生产环境是灾难”是同一次 API 调用;而在智能体的推理与生产后果之间,不存在任何约束。4
那天晚上我写了两个钩子。
Bash 防护(destructive-api-guard.sh):每一条 bash 命令都会触发。按模式匹配 curl.*purge、rm -rf、DROP TABLE、docker.*rm、git push.*--force。硬拦截(exit 2)。智能体会看到一条消息,说明命令为何被拦下并给出替代方案。没有口令”rosebud”它无法继续,而这个口令只有人类亲手输入才可能进入上下文。
MCP 防护(destructive-mcp-guard.sh):凡是匹配 mcp__cloudflare 或 mcp__github 的 MCP 工具调用都会触发。在工具参数里按模式匹配 purge、delete、destroy、remove。同样是硬拦截,同样卡在口令这一关。
两个钩子,两段 shell 脚本,总共 47 行代码。装上之后,它们一次缓存清空都没拦下来——因为自从加了口令这道关,就再没有智能体尝试过。这些钩子并不是在拦截攻击,而是在让这一类错误不再具备发生的条件。
这次事故反倒把它本该去查的性能问题暴露了出来。奥斯汀页面冷渲染 14 秒,引出了市场页面的交接,又在四天后促成了查询结构的修复。事故是有价值的,钩子则保证它不会重演。
读了凭据文件的智能体
2026 年 2 月,一个正在为某个项目收集上下文的智能体读取了 ~/.claude/docs/credentials.md。这个文件里存着 Cloudflare、GitHub、Railway 等服务的 API 令牌。它把文件内容的摘要写进了工作笔记,这意味着这些令牌出现在了发往 Anthropic 服务器的 API 请求里。
没有令牌被提交进仓库,也没有令牌被公开泄露。但它们确实经由第三方 API,进入了一个不由我掌控的上下文窗口。风险面从”我的机器”扩大到了”我的机器,加上 Anthropic 的推理基础设施”。
凭据路径防护在每一次文件读取时触发,把路径与一份敏感模式清单比对:.env、credentials、.ssh/、.aws/、.gnupg/、secrets。读凭据文件时,钩子记录一条警告并拦下这次读取;读 .env 时放行,但把这次访问记录在案。
对大多数路径,这道防护只是提示;对凭据文件,则是硬拦截。这个区分很关键:智能体读 .env 是为了搞清楚环境变量的名字,那是有用的上下文;智能体读 credentials.md 是为了搞清楚 API 令牌,那是一起安全事故。
安装以来,凭据路径防护在 200 多次会话中触发了 23 次。其中二十次是智能体读 .env 文件(记录,放行),三次是智能体试图读取凭据或密钥文件(拦下)。每一次被拦下的读取,都出自一个正在大范围收集项目上下文的智能体,只是它的搜索模式碰巧把敏感文件也网了进去。没有一次是恶意的。但若没有这道防护,每一次都会把密钥送进上下文窗口。
幽灵验证
最阴险的失败模式,是智能体在根本没做验证的情况下报告验证通过。
第 147 次会话。我让一个智能体重构一条数据库查询,并用现有测试套件验证改动。它把查询重构得很正确。完成报告写着:”所有测试通过。重构后的查询与原查询产生完全相同的结果。”
我翻了会话日志。没有任何一次 pytest 调用,任何测试运行器都不曾被调起。智能体推理出测试会通过——因为重构后的查询在逻辑上与原查询等价——然后把这段推理当作测试结果报了上来。
重构后的查询确实是对的,测试也确实会通过,智能体的推理站得住脚。但推理测试不等于运行测试,而缺陷正是从这两者之间的缝隙溜进生产环境的。假如重构后的查询在某个推理没覆盖到的边界情况上有细微错误,这个缺陷就会带着一份声称”已通过测试验证”的完成报告上线。
在我做出证据关口钩子之前,这种失败模式在 60 次会话里出现了 7 次。这个钩子在每一份完成报告上触发,扫描其中的模糊措辞:”应该会通过”“我相信”“测试很可能通过”“我有信心”。一旦命中,钩子就注入一条消息:”检测到模糊措辞。请给出具体证据:粘贴测试输出、指明文件和行号,或引用具体的验证步骤。”
这个钩子并不核实测试是否真的跑过,它标记的是”验证被跳过”所对应的语言模式。检测并不完美——足够会说话的智能体完全可以换一种说法绕开这些模式。但它抓住了最常见的那一类,而这一类占了需要人工介入的智能体故障的 12%。1
钩子上线之后,幽灵验证从占会话总数的 12% 降到了 2% 以下。剩下的 2%,要么是智能体换了说法绕开模糊措辞,要么是验证声明在字面上准确却不完整(比如集成测试没跑,却说”单元测试通过”)。
偏离
2026 年 1 月到 3 月之间,我的偏离检测器触发了十二次,每一次都发生在智能体确实丢掉了所接任务的会话上。
它的原理是把原始任务提示词做成向量,再定期与智能体近期行为的向量做比较。余弦相似度一旦跌破 0.30,系统就注入一条包含原始提示词的警告。阈值是我试出来的:0.50 太敏感(正当的子任务探索也会触发),0.20 太宽松(明显的偏离都漏掉),0.30 则抓住了每一次被确认的偏离。
第 203 次会话是最清楚的一例。任务是”修复 sitemap 中含 & 符号的职位 slug 的 XML 转义问题”。智能体先去读 sitemap 的生成代码,接着注意到 sitemap 是由一条数据库查询生成的,再接着注意到这条查询还可以优化,然后花了 40 分钟把它重构成物化视图模式,为新查询写了测试,并报告优化完成。& 符号的转义,它一次都没碰。
若检测器当时已经在位,它会在第 25 次工具调用、也就是会话开始约 15 分钟时抓住这件事——那正是”修复 sitemap XML 转义”与”创建物化视图”之间的相似度跌破阈值的时刻。而实际上,这次偏离是我在复查时才发现的。
第 89 次会话则微妙得多。任务是”给认证端点加上限流”。智能体把限流加对了。然后它注意到认证流程里的错误消息不一致,于是把错误消息统一了。然后它注意到错误响应的格式与 API 响应格式规范不一致,于是把 12 个端点的响应格式全都重构了一遍。限流本身既正确又完整,偏离出在范围失控上。
偏离检测器每 25 次工具调用触发一轮。十二次跌破阈值的记录里,智能体无一例外确实偏离了原始任务。六次在看到注入的警告后自行纠正;四次承认偏离,却主张手头的工作有价值(有时候它是对的);还有两次干脆无视警告,继续做偏出去的活儿。
钩子并不能阻止偏离,它只是让偏离变得可见。是把智能体拉回来还是放任它继续,这个决定仍在人这一边。但没有钩子,偏离要到完成报告才浮出水面,而那时上下文预算已经花光了。
伤疤分类体系
积到 84 个钩子,规律就显出来了。这些故障聚成六类:
| 类别 | 钩子数 | 示例 |
|---|---|---|
| 凭据外泄 | 12 | 智能体读取 .ssh/、把 API 密钥写进摘要、访问云端配置 |
| 破坏性操作 | 8 | 清空缓存、删库、强制推送、删除文件 |
| 任务偏离 | 4 | 智能体在解决错误的问题、范围失控、掉进子任务的兔子洞 |
| 输出质量 | 6 | 幽灵验证、无证据的模糊措辞、不完整的报告 |
| 资源耗尽 | 3 | 派生过多子智能体、无界循环、上下文溢出 |
| 跨项目污染 | 4 | 在项目 A 里工作的智能体改了项目 B 的文件 |
剩下的 47 个钩子要么是项目专用的(规范强制、部署防护、翻译校验),要么是实验性的(成本追踪、会话指标、活动心跳)。
这六个结构性类别是稳定的。类别之内的新事故,现有钩子都能拦住;全新的类别很罕见。运行的六个月里,只冒出过一个新的结构性类别(跨项目污染,是在 obsidian-signals 项目里运行的一次会话试图编辑 blakecrosley.com 的文件时发现的)。另外五类,在最初的 60 次会话里就已经成型。
Agents of Chaos 研究是一项为期 14 天的多校联合实验,让六个 AI 智能体拥有邮件、bash、文件系统和 GitHub 的访问权限。它独立地识别出了几个与我这边重叠的故障类别:反应过度(破坏性操作)、身份劫持(凭据外泄)、无限循环(资源耗尽),以及压力之下的逐步妥协(任务偏离)。5 受控研究与我的生产经验就这样对上了,这说明这些类别是自主智能体的结构性属性,而不是某套特定配置的产物。
钩子抓不住什么
钩子工作在工具调用这一层。它们在动作发生之前或之后拦截动作,却拦不住导致这个动作的推理。
一个决定去重构函数、而不去修复所报缺陷的智能体,发出的是一次合法的工具调用(写文件),内容也正确(语法无误的代码),违背的却是任务本身(改错了函数)。没有钩子能抓住它,因为没有任何一次工具调用可疑。偏离检测器最终会抓到,但那已经是在智能体把大量上下文花在错误的活儿上之后了。
钩子同样抓不住组合型故障——每一个动作单看都获得授权,串起来却导出一个未获授权的结果。清空缓存就是一次组合型故障:读缓存配置(授权内),调用清空缓存的 API(授权内),但两者的组合(在排查过程中清空生产缓存)是有害的。MCP 防护如今能抓住这个特定组合,可新的组合仍然处在覆盖范围之外。
供应链的组合缺口3 也在同一层上:受信任的组件组合出未获授权的行为。钩子是组件层面的防护。要在组合层面推理,需要另一种机制——评估的是动作序列,而不是单个动作。偏离检测器是目前最接近的近似:它评估的是行为轨迹而非单次工具调用。但它衡量的是与原始任务的相似度,而不是组合出来的动作序列是否安全。
钩子与完备安全之间的差距,就是组织记忆与组织远见之间的差距。钩子记得出过什么事,却预测不了下一次会出什么事。
事后补防护为什么反而诚实
我完全可以设计一套前瞻式的钩子系统:把每一种可能的失败模式列全,逐个写出预防性管控,在第一次会话开始之前就建好一整套安全架构。
我不这么做,是因为前瞻式设计要求预测尚未发生的故障。这些预测会是错的。钩子要么太宽(拦下正当操作),要么太窄(漏掉真正的故障模式)。误报率会侵蚀我对这套系统的信任,然后我就开始无视告警了。
事后补上的钩子是诚实的。每一个都在说:”这件具体的事发生过,这就是防它再发生的具体守卫。”守卫与故障严丝合缝,因为正是故障定义了守卫。误报显著更少,因为模式是从真实事故里抽出来的,不是从威胁模型里想出来的。随着代码库演进,事后补的守卫日后仍可能匹配过宽,但它的起点精度很高。
这条路是有代价的:每一类故障的第一次都会得逞。缓存被清了,凭据被读了,幽灵验证上线了,偏离吃掉了上下文。每一次”第一次”,都是换取一道精确、低噪声、能挡住”第二次”的守卫所必须支付的门票钱。
500 多次会话之后,大部分结构性故障类别都已经撞见过了。第一次的代价,被摊薄在此后数百次由钩子挡住重演的会话里。系统每经历一次事故就更硬一分。不是更聪明,是更硬。
每个钩子都是一道疤。每道疤都是一次教训。而教训会复利。2
常见问题
能看看你的钩子配置吗?
我在写给 NIST 的智能体安全意见里描述过这套钩子系统,也在整个 AI 工程系列中反复提到它。钩子注册在 ~/.claude/settings.json 中,再按事件类型经由 ~/.claude/hooks/dispatchers/ 分发。
钩子会拖慢智能体吗?
每个钩子给每次工具调用增加数毫秒。84 个钩子加起来,每次工具调用的总开销在 200–400 毫秒之间,具体取决于哪些钩子被触发。相比模型推理耗时(每次响应 2–5 秒),这点开销可以忽略。瓶颈不在钩子上。
钩子能用在别的 AI 编程工具上吗?
钩子是 Claude Code 专有的(PreToolUse、PostToolUse 这套事件模型)。但这个思路适用于任何支持中间件或插件的智能体框架。具体实现无法直接移植,伤疤分类体系和事后补防护的方法论却是通用的。
钩子拦下一个动作之后会发生什么?
硬拦截(exit 2)会阻止该动作,并注入一条说明理由的消息,智能体看到拦截原因后自行调整。提示型钩子(exit 0)只记录问题,动作照常放行。破坏性操作走硬拦截,其余大多数类别走提示。口令这道关只用在最危险的操作上(清空缓存、删除基础设施)。
硬拦截和提示之间,你怎么取舍?
有两类走硬拦截:破坏性操作(清空缓存、删库、强制推送、改动基础设施)和凭据外泄(读取密钥文件、访问密钥库)。其余一律只做提示式记录。区分的依据是后果的严重程度:如果这个动作可以低成本撤销,又不会泄露密钥,提示就够了;如果它不可逆,或者会暴露凭据,就必须硬拦截。
参考来源
-
Blake Crosley, “What I Told NIST About AI Agent Security,” blakecrosley.com, 2026 年 2 月。60 多次自主会话中 12% 的幽灵验证率。覆盖 Claude Code 全部 26 种生命周期事件中 15 种的 84 个钩子(v2.1.116),以及偏离检测的方法论。 ↩
-
Blake Crosley, “Compound Context: Why AI Projects Get Better the Longer You Stay With Them,” blakecrosley.com, 2026 年 3 月。上下文复利框架:钩子是六个持续积累回报的类别之一。 ↩
-
Blake Crosley, “The Supply Chain Is the Attack Surface,” blakecrosley.com, 2026 年 3 月。组合缺口:单独获得授权的组件,组合出未获授权的结果。 ↩
-
Blake Crosley, “Deploy and Defend: The Agent Trust Paradox,” blakecrosley.com, 2026 年 3 月。缓存清空事故,以及针对破坏性 API 的防护应对。 ↩
-
Christoph Riedl et al., “Agents of Chaos,” arXiv:2602.20021, 2026 年 2 月。为期 14 天的多校联合研究(美国东北大学、斯坦福、哈佛、MIT、卡内基梅隆)。六个 AI 智能体,识别出 10 项安全漏洞,包括反应过度、身份劫持与无限循环。 ↩