安装了安全钩子,不等于 AI Agent 已经处于控制之下。配置文件可能存在却没有被宿主加载,事件可能在动作执行后才到达,规则命中可能只用于监控,一条写着 deny 的记录也不能证明宿主最终执行了拒绝。
这些不是少数边缘情况,而是几种完全不同的能力。把它们压缩成一个“已支持”复选框,会让安全团队把可见性误当成控制力。
Agent 安全首先需要一张能力矩阵,然后才是更多规则。矩阵要说明何时采集、载荷有多完整、回调能否同步决策、失败时发生什么、证据能否持久保存。只有这些边界都清楚,团队才知道哪些策略适合阻断,哪些只能告警。
Numbat 是一个很适合观察这些边界的开源案例。它把 Agent hook、plugin、OTLP 日志和磁盘工件统一成一套事件模型,再用 CEL 规则检测。它的文档同时明确说明,内置规则默认全部只监控,阻断只适用于宿主支持的同步动作前回调。
这个架构原则并不限于某个工具:先统一证据,再声明宿主能力,最后只在完整控制链已经可测试的位置执行阻断。
为什么每个 Agent 各写一套规则无法扩展
不同编码 Agent 使用不同生命周期名称、载荷结构、信任机制和响应协议。同一条 shell 动作,可能以 PreToolUse、BeforeTool、preToolUse、tool.call 或插件回调出现。有的宿主会提供结构化参数与调用 ID,有的只能给出通用工具名。有的云端路径还会绕过本地桌面端使用的 hook。
如果每个回调都单独编写策略,策略层就与厂商传输层紧密耦合。宿主一升级,可见性和阻断语义都可能变化。规则开始漂移,例外越来越多,最终连两个告警是否描述同类动作都难以确认。
Numbat 的事件模型采用基础设施优先的做法,把支持的证据映射到一组封闭类型:
command.exec与command.resultfile.read、file.write与file.deletenetwork.indicatortool.call与tool.result- 权限、提示、消息、配置和会话事件
分类刻意保持保守。无法安全识别的工具继续保留为 tool.call,系统也不会从自然语言描述里臆测结构化动作。归一化事件只陈述来源证据真正支持的内容,而不是检测器希望发生的内容。
统一事件模型让一条规则可以复用,但不会让所有来源突然具备相同能力。来源、置信度、采集时点和缺失字段仍然必须保留。能力矩阵的作用,就是让这些差异不会在标准化过程中被抹掉。
这也补充了我们对 Agent 生产可观测性缺口的讨论:真正可用的遥测,是 runtime 实际发出的证据,而不是平台希望能够读取的推理过程。
能力矩阵必须有七列
| 维度 | 必须回答的问题 |
|---|---|
| 采集时点 | 动作前、动作后、遥测阶段,还是只能从磁盘工件恢复? |
| 载荷精度 | 工具名、参数、路径、URL、调用 ID、结果和退出码是否真实存在? |
| 归一化类型 | 能否安全分类,还是必须保留为通用工具调用? |
| 决策能力 | 这个回调能否同步请求拒绝,还是只能监控? |
| 激活与信任 | 配置是否存在,宿主是否加载、信任并实际触发过? |
| 失败语义 | 超时、解析错误、写入失败或 hook 崩溃时,是放行还是阻断? |
| 持久证据 | 是否有本地记录、HTTP 直送,或可重新打开的磁盘工件? |
矩阵行应细化到宿主和执行路径,不能只写产品名。比如“支持 Codex”过于宽泛,因为大多数本地 function tool 会进入 hook,hosted tool 却可能绕过本地路径。“支持 Cursor”也不够,因为本地 Agent、Cloud Agent 与 Tab 暴露的回调并不相同。
一张紧凑的宿主矩阵
以下示例来自 Numbat 当前的覆盖文档,目的不是给产品排名,而是展示可见性和控制力为何必须分列。
| 宿主或表面 | 证据来源 | 动作前阻断 | 关键限制 |
|---|---|---|---|
| Claude Code | 本地工件、生命周期 hook、OTLP | 支持 PreToolUse |
项目 hook 依赖信任,托管策略还要核对实际生效层 |
| Codex | 会话与历史、本地 hook、OTLP | 支持 PreToolUse |
hosted tool 绕过本地 hook,非托管 hook 需要审核 |
| Gemini CLI | journal、hook、OTLP | 支持 BeforeTool |
rewind 不会删除此前动作,项目级配置仍受信任控制 |
| Cursor 本地 Agent | transcript 与通用 pre/post tool hook | 支持 preToolUse |
Cloud Agent 不加载用户 hook,且可能在 hook 启动前做只读探索 |
| GitHub Copilot CLI | event journal 与共享 hook 文件 | 支持 preToolUse |
某些启动失败由宿主拒绝,超时却会放行 |
| VS Code Copilot Agent Mode | 共享 Copilot hook 的实时事件 | 支持 PreToolUse |
hook 仍是 Preview,该集成没有磁盘解析器 |
| OpenCode | plugin、可选 OTLP、早期 JSON 工件 | 不支持 | 实时采集仅监控,当前数据库取证仍未支持 |
| Claude Cowork | macOS 审计工件 | 不支持 | 只有事后可见性,没有实时 hook |
这张表不能被理解为覆盖率排行榜。范围很广的 hook 仍可能只有低精度载荷,磁盘解析器可以提供优秀取证上下文但没有任何预防能力,动作前回调可以请求拒绝却不提供动作后的确认。
应用层 hook 与内核控制是互补关系
应用层 hook 拥有更丰富的语义位置。它可能在动作进入操作系统前获得工具名、参数、项目、会话和用户批准上下文。它的弱点是覆盖范围,hosted tool、被禁用的插件、未信任项目或未公开执行路径都可能绕过回调。
内核和 runtime 控制看到的是另一条边界。Linux 的 BPF LSM 程序可以从安全 hook 返回错误,Tetragon等系统也能在支持的资源事件上选择监控或执行动作。这一层更接近文件、进程、socket 与 workload 的实际副作用,也更难被某个应用路径遗漏。但它通常无法只凭 syscall 判断一个请求来自 prompt injection 还是用户的合法意图。
两层应该进入同一条审计链,却不能被当成可互换能力。应用事件解释 Agent 准备做什么,资源边界事件说明进程尝试或完成了什么,再由会话、进程、调用、路径和时间标识关联。高风险策略可以同时要求应用 hook 的语义判断与操作系统边界的防护。
监控与阻断是三组不同证明
Numbat 的阻断模型把控制过程分成五步:
- 模型提出工具动作。
- 宿主在执行前调用 hook。
- Numbat 验证并归一化载荷,计算规则并写入操作记录。
- 如果发生干净的阻断命中,Numbat 返回该宿主的原生拒绝响应。
- 宿主解释响应,决定执行、询问或拒绝。
Numbat 本身不会取消工具,它请求宿主拒绝。enforcement 记录只能证明 Numbat 算出了 deny 或 no_override,不能证明响应已经送达,也不能证明宿主最终采取了什么动作。
因此,真实部署至少要完成三种证明:
- 传感器证明:目标执行路径确实触发 hook,并传入预期载荷。
- 决策证明:归一化事件命中了预定策略,并产生正确响应。
- 结果证明:宿主执行了响应,或者动作后事件与其他端点信号确认了结果。
只要其中一项没被测试,系统就不能被视为具备可靠阻断能力。配置状态命令只能帮助完成安装检查。Numbat 文档明确说明,hook status 只证明其配置存在且可读,不能证明宿主已激活、事件已触发或证据已送达。
阻断范围应小于检测范围
检测可以容纳一些不确定性,因为人可以继续调查。阻断会中断真实工作,因此需要更强证据。
Numbat 的规则系统体现了这种不对称。CEL 规则可以检查原始命令文本,也可以检查经过解析的 shell_commands。检测支持更广泛的 shell 结构,但阻断只允许较小的静态子集。动态替换、多语句、循环、条件、子解释器、含歧义的管道、解析诊断和被截断的投影都只用于监控。
原则很简单:不要把不确定观察转换成确定控制。
一次干净拒绝还需要满足多项条件:回调必须是支持的同步动作前事件,载荷必须有效,规则要显式写入 enforce: true,相关投影必须成功计算,所选记录必须被输出端接受,依赖序列的规则还需要可用状态。畸形或超限载荷、相关规则错误和输出失败都会使 Numbat 放弃返回拒绝。
内置规则仍然全部只监控。即使安装时启用 enforce 模式,也不会把高严重级别发现自动变成阻断。严重性与控制效果是两个独立字段。
失败语义不能藏在脚注里
“失败时放行”经常被描述成一个全局开关,但 hook 生态里至少有这些失败边界:
- 宿主未能启动 hook
- hook 超时
- 载荷无法归一化
- 规则计算错误
- 证据输出端不可用
- 宿主拒绝或忽略返回的控制响应
Numbat 在自己的干净决策条件失败时,通常不会返回拒绝。但这不保证动作一定执行,因为宿主还可能套用其他规则或询问用户。宿主协议也存在例外。例如文档说明,Copilot CLI 的动作前 hook 如果启动失败、崩溃或非零退出,宿主可能直接拒绝;超时则允许。OpenClaw 的子进程和宿主超时又有不同边界。
所以能力矩阵必须逐个失败点记录语义。否则一句“fail open”可能掩盖一套混合系统,其中一种错误放行动作,另一种错误中断所有工作,而两者都没有留下完整证据。
持久记录决定阻断质量
没有审计轨迹的控制很难校准,也很难获得信任。Numbat 默认把 finding 和适用的 enforcement decision 写入本地 NDJSON。完整实时事件流需要显式启用。HTTP 直送只是一次发送尝试,并不是磁盘队列,接收端不可用时不会自动积压等待重发。磁盘取证也只能恢复宿主实际保存过的内容。
由此可以建立一条证据阶梯:
- 规则命中一个被观察事件。
- enforcement 记录显示系统计算了拒绝。
- 本地输出端持久接收了记录。
- 宿主响应路径已经通过实际测试。
- 动作结果或独立端点信号确认最终状态。
即使到了最后一步,措辞仍需精确。finding 是策略匹配,不是入侵证明。缺少退出码只代表来源没有提供,不能被解释为命令执行成功。
一条安全的部署顺序
1. 盘点全部执行路径
列出本地 CLI、IDE、桌面端、远程端、云端、gateway、subagent、MCP 与 hosted tool。只要 hook 行为不同,就应拆成独立矩阵行。
2. 先用监控模式安装
触发已知无害动作,确认来源、事件类型、参数、项目、会话、调用 ID 和持久输出。可以时,与宿主自身日志交叉比较。
3. 证明激活和交付
先检查配置存在,再验证宿主有效配置与信任状态。触发一个已知不命中和一个已知命中,确认本地记录与转发路径都能收到。
4. 测量误报成本
从定义狭窄、信号明确的规则开始。复杂原始命令模式应继续保持监控,直到团队理解解析语义和真实工作负载分布。
5. 测试负路径
主动测试畸形输入、超时、输出端不可用、插件禁用、项目未信任和宿主批准行为。记录每个边界是放行还是阻断。
6. 只阻断一小组策略
只有显式控制意图、静态证据、经过测试的原生拒绝传输和清晰恢复路径同时存在时,才启用阻断。宽泛异常规则继续留在监控层。
7. 宿主变化后重新校准
Agent 版本、回调结构、信任控制和云端路径会独立变化。能力矩阵应是有版本的运维工件,不是一次性架构图。
真正的基础设施是证据合同
安全 hook 只是组件。可持续系统是一份证据合同,让不同组件用一致语言描述动作,同时保留各自限制。
这份合同应该让人无法混淆:
- 配置与激活
- 动作前意图与已完成动作
- finding 与入侵事实
- 已计算拒绝与宿主已执行拒绝
- 监控覆盖与阻断覆盖
- 直送成功与持久留存
当这些差异进入能力矩阵,策略就可以在不同宿主间复用,同时不会制造虚假的统一性。团队可以共享一套归一化规则语言,再根据各宿主的证据质量、阻断能力、失败语义和审计能力做不同决策。
常见问题
所有 Agent hook 都能阻断动作吗?
不能。阻断要求同步动作前回调,以及宿主可以接受拒绝响应的协议。动作后 hook、OTLP 日志和磁盘工件都只能观察。Numbat 当前覆盖矩阵中的 OpenCode 也是只监控集成。
hook status 成功是否证明阻断有效?
不能。它只证明预期配置存在且可读。还需要验证宿主加载与信任、触发已知事件、确认持久交付,并实测宿主是否执行拒绝。
为什么要先统一事件,再写安全规则?
统一事件可以把策略含义与厂商回调名称分离。规则面向稳定的 file.write 或 command.exec,同时用来源和能力元数据保留宿主差异。
Agent 安全 hook 应该失败时放行还是阻断?
没有统一答案。选择取决于动作风险、证据置信度、可用性要求、恢复路径和宿主语义。关键是逐层记录并测试失败行为。