一句「回复OK」测试曝光Grok Build CLI暗中上传整个Git历史

Grok Build CLI 在声称不读文件时,仍把完整 git bundle 上传到 xAI 存储,暴露了代理客户端的结构性盲区。
研究者用 mitmproxy 抓包 xAI 的 Grok Build CLI 0.2.93,发现它在只被要求回复「OK」、不读任何文件的情况下,仍把包含 4 次提交、47 个文件及完整历史的 git bundle 推送到 xAI 存储端点,被禁止读取的文件标记字符串完好存在。12GB 仓库测试中约 192KB 走模型通道、约 5.10GiB 走存储通道,分 73 个约 75MB 分块全部返回 200。隐私开关只控制训练而非上传,deny 规则只作用于模型工具调用层,拦不住客户端自己的网络代码。xAI 于 7 月 13 日通过服务器端标记关闭上传,7 月 15 日开源代码,但上传逻辑仍在,服务器可随时重新打开。作者建议用 canary 仓库加 mitmproxy 自行审计任意代理 CLI。
只要回复「OK」,别读任何文件——这本该是终端AI编程代理最简单的自证测试。研究者cereblab把这句话发给了xAI的Grok Build CLI 0.2.93,让mitmproxy在中间抓包。模型确实照做了。但在后台,这个CLI同时把一个完整的git bundle推送到了xAI的存储端点:4次提交、47个文件、完整的仓库历史。克隆被截获的bundle后,里面赫然躺着那个被明确禁止打开的文件,标记字符串完好无损。

这个发现之所以值得关注,不是因为xAI做了什么离奇的事,而是因为它把AI编程代理的一个结构性盲区摆到了台面上:你以为自己在控制代理能读什么,但真正上传数据的,是CLI自己的网络代码。
抓包还原了怎样的真相
第二次测试更有说服力。cereblab准备了一个约12GB的随机文件仓库,代理从未读取过其中任何内容。结果数字直接把差距拍在脸上:约192KB流向模型通道,而约5.10GiB流向存储通道,分成73个约75MB的分块,每一个都返回HTTP 200,直到抓包被中途停止。
一次抓包揭示了同一二进制发出的两条并行通道。模型对话走cli-chat-proxy.grok.com/v1/responses;存储走同一主机的/v1/storage,外加multipart初始化和batch_exists检查。在3GB的运行中,cereblab还看到分块直奔一个名为grok-code-session-traces的Google Cloud Storage桶。Mixpanel和grok.com事件调用也一同搭车。
最让人不安的细节是隐私开关形同虚设。关闭「Improve the model」后,git bundle上传依然以200成功通过,服务器的/v1/settings响应里仍然带着"trace_upload_enabled": true。cereblab的解读是:这个开关控制的是训练,不是上传。
秘密数据两条通道都没躲过。在一次代理真正读取文件的真实任务中,一个被git追踪的.env文件带着假canary值,原封不动地进入了模型对话轮次;同一批值也落进了一个准备发往存储的session_state归档。
git bundle 是 Git 内置的一种打包格式,能把一个仓库的完整对象图——提交、树、blob、引用——序列化进单个文件,相当于把整个仓库压成一份可离线克隆的归档。文件开头带有 # v2 git bundle 或 # v3 git bundle 的魔术字符串,因此很适合作为网络流量的识别特征。
它的危险之处在于「完整」二字:一次 bundle 上传带走的不是你当前工作目录里看得见的几个文件,而是仓库的全部历史,包括所有曾经提交过、后来又被删除的内容。所以只要密钥、令牌、内部地址曾经出现在任何一次提交里,它就会永远留在这条历史链上,并随每一次 bundle 导出而离开本地。删除文件、改写工作区都无济于事,唯一的补救是让该密钥失效并轮换。这也是为什么在讨论数据外泄时,「被追踪的文件」和「磁盘上存在的文件」是两个完全不同的集合。
7月13日:服务器端悄悄扳动了一个开关
同一个0.2.93二进制,在7月13日突然停止了存储请求。cereblab重测六次,零次/v1/storage上传;服务器现在返回disable_codebase_upload: true和trace_upload_enabled: false。
xAI在X上做出了回应。据The Hacker News报道,xAI表示企业零数据保留团队从未存储过代码或trace数据,消费者可以在CLI里运行/privacy关闭保留并删除已同步数据。马斯克称之前上传的数据将「被彻底、完全删除」。但cereblab实测/privacy后得出结论:它控制的是保留,而非发送什么。
7月15日,xAI以Apache 2.0协议开源了Grok Build的源代码(针对一方代码)。Tech Times报道称,已发布的源码里仍然保留着Cloud Storage上传代码。Simon Willison也发现了「代码的残骸」:upload_session_state()现在硬编码返回session_state_upload_unavailable错误。上传路径被关掉了,但它依然躺在代码里。
这正是问题的关键所在:服务器下发的开关,服务器可以随时扳回来——你机器上不会出现新二进制,changelog里也不会有任何记录。截至The Hacker News报道时,xAI尚未说明该变更是否覆盖所有账户、是否永久、上传数据被保留多久、以及多少用户受影响。
服务器端开关(server-side feature flag) 指客户端行为不由本地配置决定,而是由服务端返回的响应字段实时控制。CLI 启动或运行中会向 /v1/settings 之类的端点询问配置,拿到 disable_codebase_upload: true、trace_upload_enabled: false 之类的值后才决定是否上传。这一模式的治理含义很关键:字节流的去向可以在不更新你机器上任何二进制、不在 changelog 留痕的情况下被改变。今天被关掉的上传路径,明天可以通过一次响应字段的翻转重新打开。
因此,对这类代理做安全评估时,一次抓包只能说明「此刻」的行为,不能保证「此后」的行为。审计必须可重复——把它纳入 CLI 升级流程,而不是当作一次性的取证结论。
Deny规则拦得住代理,拦不住客户端
cereblab的复现仓库里藏着最锋利的细节:像Read(secret.txt)这样的deny规则确实阻止了代理读取文件,但被追踪的该文件依然存在于上传的bundle中。
每个代理CLI里都有两层。一层是模型的工具调用:读这个文件、运行那个命令——你的审批提示和deny规则活在这里。另一层是客户端自己的网络代码,它执行的是模型的任何决定,但第一层根本看不到第二层。
比较笔记还厘清了bundle到底携带什么:被追踪的文件及其历史。从未提交过的gitignored文件不在其中;提交过之后又被删除的文件则一同带上。如果一个密钥曾经碰过任何一次提交,那个仓库的每个bundle都会带着它——删文件没用,得轮换密钥。
cereblab还用同一个canary仓库在7月13日测了另外三个CLI。Claude Code 2.1.204只发送POST api.anthropic.com/v1/messages。Codex通过WebSocket把模型对话发往chatgpt.com,附带遥测。Gemini 0.38.2在API-key模式下,空闲运行没看到bundle,真实任务运行撞上了429配额错误。这几次抓包里,三个都没上传仓库bundle——但这是同一天、四个CLI的快照,下周的版本可能就不同了。
canary(金丝雀标记) 是一段刻意植入的、全局唯一且易于搜索的字符串,用假密钥、假令牌的形式放进文件里。它的作用不是防止泄露,而是让泄露变得可检测:只要这段字符串出现在任何出站请求体、bundle 或归档里,就证明该文件的内容离开了本机,无论代理是否声称读过它。
用 canary 审计之所以有效,是因为它对「路径」不做假设:不依赖厂商公布的 host 清单,也不依赖对工具调用日志的信任。三个 canary 正好覆盖三种情形——被明确禁止读取的文件(检验是否绕过 deny 规则)、只存在于 git 历史中的密钥(检验历史是否随包外流)、从未提交过的 gitignored 密钥(检验是否只导出了被追踪内容)。标记检查也有边界:git 会压缩 bundle 内的对象,所以压缩后的内容不会以明文命中字符串,此时「能成功克隆的 bundle 本身」才是最强证据。
用代理和canary审计任何代理CLI
你不需要信任任何人的说法,包括我的。按cereblab的建议,尽量用丢弃式账户。
建一个带三个canary的仓库:一个提示明确禁止读取的文件、一个只活在历史里的密钥、一个从未提交过的gitignored密钥。然后写一个mitmproxy addon,按主机统计出站字节、标记请求体里出现的任何canary字符串、并保存任何以# v2 git bundle或# v3 git bundle开头的请求体(bundle文件以此开头,钩子用的是标准mitmproxy事件)。
用mitmdump -p 8080 -s count_egress.py启动代理,第一次启动会把CA写进~/.mitmproxy/mitmproxy-ca-cert.pem。然后在另一个终端把CLI路由过去。Claude Code读NODE_EXTRA_CA_CERTS找额外CA;cereblab为Grok设置的是SSL_CERT_FILE。具体变量看你所用CLI的文档。
对你要审计的CLI跑两个提示:空闲那条(「别读任何文件」)告诉你代理什么都不干时有什么离开;真实任务那条告诉你它工作时有什么离开。Ctrl-C停掉mitmdump,done钩子会按主机打出总量。如果保存了bundle文件,克隆它验证:
git clone -q bundle-1.bundle recovered
cat recovered/src/_probe/never_read.txt
git -C recovered log --all --oneline -- old.env
怎么读结果?模型主机必须出现——不出现说明CLI忽略了代理,安静的抓包什么都证明不了。跟厂商自己公布的host清单比对,不在清单上的host就是该向厂商追问的问题。空闲运行里出现NEVERREAD,意味着文件内容在未经读取的情况下离开了;任何地方出现HISTORY,意味着历史在旅行;真实任务运行里模型主机出现IGNORED,意味着代理读了你的.env。一个能克隆的bundle,是你能拿到的最强证据。
这套方案也有盲区。标记检查只能看到未压缩的请求体,而git会压缩bundle里的对象,所以对Grok这类上传,保存下来的bundle和按主机的字节总量才是证据。没有canary告警本身什么都证明不了。固定证书的CLI会拒绝代理——这本身就是个发现。request钩子只统计HTTP body,对Codex那种通过WebSocket传输模型对话的情况,得加websocket_message钩子。大上传可能分块到达,bundle头只出现在第一块里,multipart body需要先去掉尾部边界才能让git clone接受。CLI可能忽略这些变量只读操作系统信任库——那就用丢弃式用户账户在系统里信任CA,用完删掉。
这件事真正改变了什么
Grok Build的上传已经被一个服务器端标记关掉了。但方法留了下来:一个代理、一个canary仓库,再加一小段mitmproxy addon,就能让你看清任何代理CLI到底发送了什么——Grok也好,别的也罢。
工具本身没有变坏。变的是我们该默认什么。当你把一份代码库交给一个终端代理时,模型看到的和你以为它看到的,本来就是两件不同的事。而现在我们知道,这两件事之间的差距,可能是几KB和几GB的量级。
下次升级CLI时,把canary再跑一遍。
相关推荐

OpenAI与Shield合作:Astra智能体如何革新IT故障修复
OpenAI与Shield合作推出Astra智能体,可自动登录电脑和遗留系统,实时修复IT故障,比传统模型快35%到40%,仅在需要人类判断时引入人工。

iPhone Duo续航实测:一天一充跑不掉,大屏是耗电元凶
iPhone Duo续航实测:重度使用一天需至少充电一次,6.5小时亮屏约剩30%电量。大屏展开频率是耗电关键,MagSafe电池背夹可救场,长期衰减同样值得关注。

Kimi K3.1 Pro开源发布:2.8万亿参数逼近Opus
Moonshot发布开源MoE模型Kimi K3.1 Pro,2.8万亿总参数、1040亿激活参数,Terminal Bench 4.0仅落后Opus 5.5不到1分,单任务成本约三分之一,API价格减半,开源权重11月4日开放。