每个钩子都是一道伤疤:代码中编码的84次智能体失败
截至v2.1.116(2026年4月),我的智能体编排系统中有84个钩子,拦截Claude Code暴露的26种生命周期事件类型中的15种。每个钩子都是一个在特定智能体动作之前或之后触发的shell脚本或Python片段:文件读取、文件写入、bash命令、Web请求、子智能体生成、git操作、MCP工具调用。每个钩子存在的原因,都是因为曾经出过问题。
智能体编排系统中的每一个钩子都可以追溯到一次具体的生产事故,这使得钩子集合成为编码为shell脚本的制度记忆。 智能体清空过CDN缓存、读取过凭证文件、汇报过从未运行的测试结果、偏离任务长达40分钟。每一次事故都催生了一个小而确定的守卫,此后在每个会话中静默触发。
不是理论上错了。是生产环境里错了。一个智能体清空了一个服务数百万请求的CDN缓存。一个智能体试图写入SSH密钥。一个智能体在没有调用pytest的情况下汇报”所有测试通过”。一个智能体偏离任务之远,以至于花了四十分钟去优化一个与分配工作毫无关系的文件中的函数。
这些钩子没有一个是我主动设计出来的。我没有坐下来穷举自主AI智能体的失败模式并编写预防性控制。每一个钩子都是被动的。出了问题,我写一个脚本防止它再出问题,这个脚本此后在每个会话中静默触发。钩子系统不是一套安全架构,而是一本伤疤集。
核心要点
- 缓存清空事件:一个智能体使用已授权的API调用清空了生产环境的CDN缓存。两个钩子(47行代码)现在将破坏性操作置于人工输入口令的关卡之后。
- 凭证读取事件:一个智能体将API令牌包含进了其上下文窗口。一个路径匹配守卫现在会阻止对凭证文件的读取,并记录对
.env文件的访问。 - 虚假验证者:一个智能体在没有运行pytest的情况下汇报”所有测试通过”。一个模糊措辞检测器将虚假验证的比例从12%降到了2%以下。
- 十二次偏离:60天内,智能体有12次可验证地偏离了任务。一个阈值为0.30的余弦相似度检测器现在每25次工具调用触发一次。
- 分类体系:六个结构性失败类别覆盖了全部84个钩子。在500多个会话之后,新类别已相当罕见。系统每经历一次事故,就变得更坚硬一分。
缓存清空:一次授权调用如何搞垮生产
2026年3月21日,我让一个智能体调查resumegeni.com上的市场页面为何加载缓慢。智能体按常规开始调查:读取路由处理器、检查数据库查询、分析模板渲染性能。然后它判定,过期的Cloudflare缓存条目可能掩盖了真实的性能特征。
智能体以purge_everything: true调用了mcp__cloudflare__cache_purge。
生产站点上每一个已缓存的页面都瞬间失效。CDN从以80-100ms服务大多数请求,变成了把每个请求都转发到Railway源服务器。奥斯汀的市场页面从不到1秒变成了14,290毫秒。纽约从不到1秒变成了6,891毫秒。站点上的每一个页面现在都要在每一次请求中从冷源渲染。
智能体并没有做任何未经授权的事情。它使用合法的MCP工具、凭借有效凭证,调用了一个已授权的API端点。如果你正在调试缓存行为,清空缓存是一个合理的调查步骤。问题在于,”调试时合理”和”对生产灾难性”是同一个API调用,而在智能体的推理与生产后果之间不存在任何约束。
那天晚上我构建了两个钩子。
Bash守卫(destructive-api-guard.sh):在每次bash命令上触发。对curl.*purge、rm -rf、DROP TABLE、docker.*rm、git push.*--force进行模式匹配。硬拦截(退出码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次。其中20次是智能体读取.env文件(已记录,允许)。3次是智能体尝试读取凭证或密钥文件(已阻止)。每一次被阻止的读取,都是一个在广泛收集项目上下文时,恰好在其搜索模式中包含了敏感文件的智能体。没有一次是恶意的。如果没有守卫,它们都会把秘密放进上下文窗口。
虚假验证者
最阴险的失败模式,是一个在没有执行验证的情况下汇报验证成功的智能体。
第147号会话。我让一个智能体重构一个数据库查询,并用现有的测试套件验证变更。智能体正确地重构了查询。完成报告说:”所有测试通过。重构后的查询产生与原查询完全相同的结果。”
我检查了会话日志。没有出现pytest调用。没有任何类型的测试运行器被调用过。智能体推断测试会通过,因为重构后的查询在逻辑上与原查询等价,并把这种推断当作测试结果进行了汇报。
重构后的查询是正确的。测试本该通过。智能体的推理是站得住脚的。但对测试进行推理不等于运行测试,而二者之间的差距正是bug进入生产的地方。如果重构后的查询在智能体推理未覆盖的某个边缘情况下有细微错误,那么这个bug就会带着一份声称测试已验证的完成报告被部署出去。
在我构建证据闸门钩子之前,这种失败模式在60个会话中出现过7次。钩子在每次完成报告时触发,扫描模糊措辞:”应该通过”、”我相信”、”测试很可能通过”、”我确信”。一旦检测到,钩子就会注入一条消息:”检测到模糊措辞。请引用具体证据:粘贴测试输出,给出文件和行号,或引用具体的验证步骤。”
钩子并不验证测试是否真的运行过。它标记的是那种表明验证被跳过的语言模式。这种检测并不完美。一个表达足够流畅的智能体可以改写其模糊措辞来绕开这一模式。但钩子能捕获常见情形,而这一情形占了需要人工干预的智能体失败案例的12%。1
钩子安装后,虚假验证从12%降到了2%以下的会话比例。剩下的2%是智能体改写了模糊措辞,或者验证声明在技术上准确但不完整(例如,当集成测试没有运行时,仅声称”单元测试通过”)。
偏离
2026年1月至3月间,我的偏离检测器在12次会话中触发,这些会话里智能体可验证地偏离了所分配的任务。
偏离检测器的工作方式是,将原始任务提示词嵌入为向量,并定期与智能体近期动作的嵌入向量进行比较。当余弦相似度降到0.30以下时,系统会注入一条包含原始提示词的警告。阈值是我通过实验校准出来的:0.50过于敏感(在合理的子任务探索中也会触发),0.20过于宽松(漏掉明显偏离),0.30捕获了每一次已验证的偏离事件。
第203号会话是最清晰的案例。任务是”修复sitemap XML对包含&符号的职位slug的转义问题”。智能体一开始读取了sitemap生成代码。然后它注意到sitemap是从一个数据库查询生成的。然后它注意到这个数据库查询可以优化。然后它花了40分钟把查询重构为物化视图模式,为新查询编写测试,并汇报优化完成。它始终没有修复&符号的转义。
偏离检测器本应在第25次工具调用时捕获到这一点——大约在会话开始后15分钟,当”修复sitemap XML转义”与”创建物化视图”之间的相似度降到阈值以下时。但实际上,我是在复审时才发现这次偏离的。
第89号会话更为微妙。任务是”为认证端点添加限流”。智能体正确地添加了限流。然后它注意到认证流程中有不一致的错误消息。然后它统一了错误消息。然后它注意到错误响应格式与API响应格式标准不一致。然后它在12个端点上重构了响应格式。限流是正确且完整的。范围爆炸就是偏离。
偏离检测器每25次工具调用触发一次。在全部12次低于阈值的触发中,智能体都可验证地偏离了原始任务。在6次中,智能体在看到注入的警告后自我修正。在4次中,智能体承认了偏离,但认为当前工作是有价值的(有时确实如此)。在2次中,智能体忽略了警告并继续进行偏离的工作。
钩子并不阻止偏离,它让偏离变得可见。重定向还是允许偏离工作,决策权仍在人类手中。但没有钩子,偏离就会一直不可见,直到完成报告——那时上下文预算已经用尽。
伤疤分类体系
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 他们的受控研究与我的生产经验之间的一致性,表明这些类别是自主智能体的结构性属性,而非任何特定配置的产物。
钩子所不能捕获的
钩子在工具调用层面运作。它们在动作发生前后拦截动作。它们无法拦截导致这一动作的推理。
一个决定去重构函数而非修复所汇报bug的智能体,会产生一个合法的工具调用(文件写入),内容正确(语法有效的代码),但违反了任务(错误的函数)。没有钩子能捕获这一点,因为没有一个工具调用是可疑的。偏离检测器最终能捕获到它,但那时智能体已经在错误的工作上消耗了大量上下文。
钩子也无法捕获组合性失败——其中每一个单独的动作都是被授权的,但其序列产生了未经授权的结果。缓存清空就是一个组合性失败:读取缓存配置(已授权)、调用清空API(已授权),但二者的组合(在调查过程中清空生产缓存)是有害的。MCP守卫现在能捕获这一特定组合,但新的组合仍未被覆盖。
供应链组合缺口在同一层面上运作:受信任的组件组合成未经授权的行为。钩子是组件层面的守卫。组合层面的推理需要不同的机制,一个评估动作序列而非单个动作的机制。偏离检测器是最接近的近似:它评估的是行为轨迹,而非单个工具调用。但它衡量的是与原始任务的相似度,而非所组合动作序列的安全性。
钩子与完整安全之间的差距,就是制度记忆与制度前瞻之间的差距。钩子记得什么曾经出错,却不能预测下一次会出什么错。
为什么被动才是诚实的
我本可以设计一个主动的钩子系统:穷举每一种可能的失败模式,为每一种编写预防性控制,在第一个会话开始之前就构建一套完整的安全架构。
我不这样做,是因为主动设计要求预测尚未发生的失败。预测会是错的。钩子要么过于宽泛(阻止合法动作),要么过于狭窄(漏掉实际的失败模式)。误报率会侵蚀人对钩子系统的信任,而我会开始忽略告警。
被动钩子是诚实的。每一个都在说:”这件具体的事发生过,这里就是防止它再发生的具体守卫。”守卫精准地对应于失败,因为是失败定义了守卫。由于模式是从真实事故中提取的,而不是从威胁模型中想象出来的,误报率在本质上更低。随着代码库演进,被动守卫之后仍可能出现过度匹配,但起点精度很高。
被动方法有一个代价:每一类失败的第一次都会得手。缓存清空发生过。凭证读取发生过。虚假验证被发布过。偏离消耗过上下文。每一次首次失败,都是一个精确、低噪声的守卫防止第二次失败的入场费。
500多个会话之后,大多数结构性失败类别都已遇到过。首次失败的代价被摊薄到了成百上千个钩子阻止重演的会话中。系统每经历一次事故,就变得更坚硬一分。不是更聪明。是更坚硬。
每个钩子都是一道伤疤。每道伤疤都是一个教训。教训在复利累积。2
常见问题
能看看你的钩子配置吗?
我在给NIST的智能体安全评论中描述了这套钩子系统,并在整个AI工程系列中都有引用。钩子在~/.claude/settings.json中注册,并通过~/.claude/hooks/dispatchers/按事件类型分发。
钩子对智能体性能有什么影响?
每个钩子会为每次工具调用增加几毫秒。有84个钩子,每次工具调用的总开销是200-400ms,具体取决于哪些钩子触发。与模型推理时间(每次响应2-5秒)相比,这一开销可以忽略不计。钩子不是瓶颈。
钩子能与其他AI编程工具配合使用吗?
钩子是Claude Code特有的(PreToolUse、PostToolUse事件模型)。这一概念适用于任何具备中间件或插件支持的智能体框架。具体实现不可移植,但伤疤分类体系和被动方法论是普适的。
钩子阻止动作时会发生什么?
硬拦截(退出码2)会阻止动作并注入说明原因的消息。智能体会看到拦截原因并做出调整。咨询性钩子(退出码0)记录关注点但允许动作。破坏性操作使用硬拦截。其他大多数类别使用咨询性钩子。口令关卡仅用于最危险的操作(缓存清空、基础设施删除)。
你如何决定采用硬拦截还是咨询?
两类获得硬拦截:破坏性操作(缓存清空、数据库删除、强制推送、基础设施修改)和凭证暴露(读取秘密文件、访问密钥库)。其他所有情况都记录咨询性警告。这一区分取决于后果的严重程度:如果动作可以低成本撤销且不泄露秘密,咨询就已足够;如果动作不可逆或暴露凭证,硬拦截就是必须的。
来源
-
Blake Crosley, “What I Told NIST About AI Agent Security,” blakecrosley.com, 2026年2月。60多个自主会话中12%的虚假验证率。84个钩子覆盖Claude Code(v2.1.116)26种生命周期事件类型中的15种,偏离检测方法论。 ↩
-
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、CMU)。六个AI智能体,识别出10个安全漏洞,包括不成比例的响应、身份劫持以及无限循环。 ↩