返回所有文章
发布于 · 作者 Renaud Deraison

那个 hook 从未抵达模型

8 月 31 日,Token Security 在 The Hacker News 上逐一介绍了 Anthropic 为本地 Claude Code 会话新推出的 Compliance API 端点。它们是实打实的进步,而那篇文章对于记录在哪里停下讲得很精确:在工具真正执行前就触发的 hook、躺在磁盘上的文件、在会话之外启动的进程,以及一切由 Bedrock 或 Vertex 提供服务的东西。上周那只 npm 蠕虫,一辈子都活在那道缝隙里。Bromure Agentic Coding 把探针接在 VM 与外界之间的那条线上,所以记录下来的是实际发生了什么,而不是有人对模型说了什么。

一份记录只能装下有人对模型说过的话。一场代理会话里有趣的那一半,正是没有人 说出口的那一半。

今年大半时间里,一位想知道 Claude Code 在某台笔记本上到底做了什么的工程主管, 只有两个选择:读开发者的终端回滚记录,或者买一套第三方封装工具。Anthropic 的 合规端点覆盖 claude.ai 和 Claude Desktop,对 Claude Code 几乎没有着墨。

这在 8 月 11 日变了。Anthropic 为本地会话推出了三个 Compliance API 端点:一个 列出会话,一个取某个会话的元数据,第三个则从 GET /v1/compliance/apps/sessions/local/{session_id}/messages 拉出完整记录。 8 月 31 日,Token Security 在 The Hacker News 上发表了 一篇讲它们能给你什么的导览

那篇文章由 Token Security 赞助,但它依然是我读过的、谈「一份日志的视界在哪里」 最细心的说明之一。

三种块类型,以及它们覆盖了什么

记录以三种块的形式送达。用文章自己的话说:“凡是传达给模型的内容,都会被记录 成三种块类型:text、tool_usetool_result。三者合起来覆盖了用户提示、 bash 命令、读取与写入,甚至包括 MCP 命令。”

这已经很多了:模型选过的每一条 shell 命令、它读过的每一个文件、它调用过的每一个 MCP 工具。如果你的问题是“这个代理在星期二决定做了什么”,记录能回答你,而且是 从 Anthropic 的服务器回答,而不是从你正在调查的那台机器上的某个文件。

真正在干活的是第一个从句:凡是传达给模型的内容

视界

有四样东西落在那道边界之外,文章把四样都点名了。

Hook。 “Hook 是最清楚的例子:它们在本地执行,位于模型做出决定与工具真正 执行之间,而且可以阻止某个工具执行或某个提示被发出。” Hook 是开发者磁盘上的 一段代码,在模型选定某个动作到该动作真正发生的缝隙里触发。模型从来没听说过它, 所以记录也从来不会载着它。

磁盘。 “两者都看不到磁盘上放着什么:配置文件、已安装的 skill 与插件及其 .md 文件(除非它们在某次会话中被用到),或者在会话之外启动的进程。” 一个你装好 却从没唤起过的 skill 不会留下任何痕迹,代理一小时前启动又忘掉的后台进程也一样。

其他模型提供方。 “如果你在非 Anthropic 的模型上运行 Claude Code,你完全拿 不到 Compliance API 的覆盖,因为它只记录与 Anthropic 模型的交互。跑在 Bedrock、 Foundry 或 Google Cloud 上的会话不会被覆盖。” 覆盖止于 Anthropic 的模型,于是 Codex、Gemini CLI、Grok,以及你的团队下个季度会装的任何东西,都被留在外面。

权限决定。 开发者对一条危险命令的批准,或者让整段提示直接放行的绕过标志, 都落进 OpenTelemetry,而不是合规记录。

这一切都是正确的行为。一份锚定在模型上的日志,记下的就是模型看见的东西。给你 下一次安全评审的问题是:你最需要看见的那些东西,落在这条线的模型那一侧吗?

Hypervisor 边界 — 主机 proxy 与虚拟交换机VM 发出或收到的每一个字节,不论来自哪个进程、哪个代理抵达模型的内容text · tool_use · tool_result用户提示它选定的 bash 命令它请求的读取与写入MCP 工具调用两条线之间在工具执行之前就触发的 hook安装脚本与 node-gyp,同一条命令的子进程送达的 tarball,来自npm、PyPI、Cargo、MavenMCP 服务器自己的对外请求在任何会话之外启动的进程跑在 Bedrock、Foundry或 Vertex 上的会话两份记录都准确。它们回答的是不同的问题,因为它们锚定在不同的边界上。
同一场代理会话外围的两道边界。内侧那道是模型:凡是传达给它的内容都在 Compliance API 的记录里。外侧那道是 Hypervisor:VM 发出或收到的一切都会穿过 Bromure 的 proxy 与虚拟交换机。有趣的区域是两者之间的空间,hook、安装期间产生的子进程、包 tarball、MCP 服务器自己的流量,以及非 Anthropic 的后端,全都住在那里。

上周那只蠕虫就住在那道缝隙里

四天前,一个每周下载量超过 15 万次的 npm 代码生成器,开始夹带凭据窃取程序。 我们 在星期六写过那条发布链: 一位陌生人在某个 pull request 上留言,项目自己的发布工作流读了那条留言、却没有 核对是谁写的,于是十个带签名的版本就发出去了。

把那份恶意代码再读一遍,另一只手上拿着合规记录。追一下每个步骤会出现在哪里。

代理执行 npm install。那是一个 tool_use 块,记录里有。之后的一切都是子进程。

第一波压根没有声明任何安装脚本。它的载荷藏在 binding.gyp 里,也就是 node-gyp 会读的构建描述文件,位于 node-gyp 会拿 Python 去求值的 conditions 字段中。 模型从来没有为它轮到过一次,也没有任何工具调用点到它的名字:一段构建文件里的 Python 表达式,以那条被记录成再平常不过的命令的孙进程身份运行。

跟着来的扫描,在主目录底下走过超过 150 个 glob 模式,寻找 SSH 密钥、.env 文件、云凭据和 registry 令牌。这一切,没有人问过模型半句。

然后是常驻。在每一个能碰到的仓库里,蠕虫写下一个 .claude/settings.json,里面 带着一个 SessionStart hook,每当有开发者在 Claude Code 里打开这个项目,就执行 setup.mjs。一个 hook、放在磁盘上、在任何人征询模型之前触发:四个盲点里的两个, 挤在同一个文件里。挑中那个位置的攻击者,几乎肯定从没读过任何厂商的合规更新日志。

整段敌意窗口持续了三小时十一分钟,其中没有任何一部分抵达过模型。

Bromure 把探针放在哪里

Bromure Agentic Coding 让每个工作区以自己的虚拟机跑在 Apple Silicon 上,离开那台 VM 的一切都会经过一个 proxy 和一个虚拟交换机,而它们住在 Hypervisor 的 Mac 那 一侧。交换机把 VM 的 80 端口和 443 端口流量导进 proxy,不需要设置任何环境变量, 客户机也没有任何东西可以关掉它。因此这份记录不依赖代理愿意配合,也不依赖代理 知不知道 proxy 在那里。

Activity only 这个追踪级别及以上,每一个请求都会得到一条元数据记录: 时间戳、主机、端口、方法、路径、状态码、延迟、在任何凭据替换之前量到的请求 字节数、响应字节数、proxy 在发出时替换掉了哪些凭据,以及当请求带着一个并非 Bromure 签发的 bearer 令牌时的告警。不论这个请求来自代理、来自某个 postinstall 脚本、来自某个自顾自忙碌的 MCP 服务器,还是来自某人三小时前启动后就忘掉的进程, 这条记录都存在。

Security Timeline 窗口(Window → Security Timeline…)就在旁边,回答的是另一个 问题。Trace Inspector 告诉你代理发出了什么;时间线告诉你 Bromure 的引擎决定了 什么。一张按时间排列的表格,最新的在最上面,用颜色区分:绿色代表放行,红色代表 拦截,蓝色代表信息——凭据代理、防火墙、供应链、护栏、提示注入、凭据使用、上游 TLS。它在内存里保留 5,000 行。持久副本则是 ~/Library/Application Support/BromureAC/traces/ 下加密过的会话追踪文件,用与你 工作区机密相同的钥匙串密钥以 AES-GCM 封存。

同一次安装,在 Bromure 工作区里读起来是这样。

Security Timeline引擎它的判定20:31:02供应链npm [email protected] — 已拦截版本只有 9 分钟大,闸门是 2 天20:31:02供应链npm [email protected] — 已改写安装脚本已剥除,元数据哈希已更新20:31:07防火墙objects.githubusercontent.com:443 tcp — 已拦截没有匹配规则,未匹配流量为拒绝20:31:09凭据代理brm_a1b2… → paste.example — 已拦截外泄企图,VM 已暂停20:31:09护栏git-receive-pack github.com — 已拦截这个工作区的 GitHub 是只读20:33:41提示注入rules FLAG source=/repo/AGENTS.md
在那次被记录看作单个 tool_use 块的安装过程中,一份 Bromure Security Timeline 记下了什么。每一行都来自主机端盯着线路的引擎,所以这些都不需要载荷经过模型,也没有一行能被 VM 内部的代码改写。

那些行里没有一行需要载荷曾经经过模型,也没有一行能被 VM 内部的代码改写,因为 写下它们的引擎跑在 Mac 上,而客户机没有任何通往它们的路径。

不管你跑的是哪个代理,都是同样那些行

一个团队统一用 Claude Code。接着有人为了某个项目带进 Codex,平台组为了数据驻留 把一个工作区导向 Bedrock,而一位研究员开始跑 Grok。按照文章自己的说法,这四个里 有三个在 Compliance API 中什么都不会产生。

一个读取 TLS 服务器名称的 proxy,用同一种方式看待这四个,而同样的 llm.requesttool.usecommand.runfile.readfile.write 事件会从每一个里面出来 ——是从流量中抽出来的,而不是某家厂商赏给你的。在一台已加入 bromure.io 工作区的 Mac 上,它们会通过双向 TLS 流向组织,一并还有每个防火墙判定的 egress.firewall、 当某个诱饵凭据流向从未为它签发的主机时的 credential.exfiltration,以及工作区 拉取的每一个包的 supply_chain.fetch。最后这一项,就算把所有供应链强制层都关掉 也照样发出,因为 Bromure 把「看」和「拦」分开了。这道流不带原始提示。它回答的是 代理做了什么,而不是开发者问了什么。

Claude Code · Anthropic在 Compliance API 内Claude Code · Bedrock未覆盖Codex · OpenAI未覆盖Grok · xAI未覆盖主机 proxy + 交换机读的是 TLS 服务器名称,不是厂商账号 ID单一事件流llm.request · tool.usecommand.run · file.readegress.firewallsupply_chain.fetchcredential.exfiltration
四个工作区跑在四种不同的后端上。锚定在 Anthropic 模型上的记录只覆盖第一个。锚定在线路上的记录覆盖全部四个,因为它是从流量中提取出来的,而不是由碰巧在提供令牌的那家厂商赏给你的。

文章收尾的那一句

全文最锋利的一句话,把覆盖范围抛在了身后:“光靠活动日志,无法告诉你一个代理的 访问是否正当。”

这对任何日志都成立,也正因如此,Bromure 的引擎自己写时间线,而不是把这件事交给 另一个旁观者。那个窗口里每一行红色,记下的都是一个已经发生的决定。一行供应链, 意味着一个代理从未收到的包。一行防火墙,意味着一条从未打开的连接。护栏那一行 是主机以 403 拒绝的推送,而代理把它读成一次普通的 API 失败;凭据代理那一行则是 一个从未变成真正机密的诱饵——因为真正的那一份从来就不在 VM 里。

策略仍然得由你来定。但你回答「这次访问正当吗」的时机,是在配置工作区的当下, 而不是几个星期后坐着读那些行。

把你的探针放在哪里

如果你在任何规模上跑编码代理,就把 Compliance API 打开。四个星期前,这个面几乎 毫无可见度,而一个星期的记录会告诉你一些关于自己团队、你原本不知道的事。

然后问第二个问题:一台记录看起来干干净净的机器,你要拿它怎么办?在 Bromure 工作区里,答案已经在屏幕上了。要看主机就打开 Window → Trace Inspector,要看 判定就打开 Window → Security Timeline,或运行 bromure-cli trace hostnames, 把那个工作区自启动以来连过的每一个域名读一遍。

代理不必提起任何一个字,它们也在那里。 安装 Bromure Agentic Coding,打开时间线,看一次安装。