xAI Grok Build CLI 网络流量分析:上传仓库全部文件及 git 历史
xAI 的 Grok Build CLI 实际上向 xAI 发送了什么
对 xAI 官方 Grok Build 编码 CLI(grok 0.2.93)的网络流量分析显示,该工具在消费者登录后会向 xAI 发送三类数据:一是它读取的文件内容(包括 .env 密钥文件)以明文形式通过 POST /v1/responses 传输,并同时打包成 session_state 存档通过 POST /v1/storage 上传并获 HTTP 200 确认;二是整个仓库的全部文件内容及 git 历史,独立于 AI 智能体实际读取的文件——即使提示“不要读取任何文件”,Grok 仍将整个仓库作为 git bundle 上传至 Google Cloud Storage 的 grok-code-session-traces 存储桶;三是该上传机制默认开启,且关闭“改进模型”设置不会禁用(/v1/settings 仍返回 trace_upload_enabled: true)。在 12 GB 仓库测试中,/v1/storage 传输了 5.10 GiB 数据,而模型对话通道仅传输 192 KB,比例约 27,800 倍。分析未证明 xAI 使用这些数据进行训练,但证实了数据被传输、接收并存储。
这是我见过最严谨的隐私调查,每步可复现——Grok CLI 会在用户不知情下将完整仓库、.env 密钥甚至未读文件原样上传至 xAI 的 GCS,默认开启且无法真正关闭,所有用 Grok Build 的开发者都得重审自己的 secrets。
xAI 的 Grok Build CLI 究竟向 xAI 发送了什么:一次线路级分析
一次审慎、可复现的拆解。结论均有捕获的产物(端点、HTTP 方法、状态码、字节大小、主机)和复现命令作为支撑;对于曾实时观察到但未留存为文件的观察结果,第 7 节会明确说明。第 8 节是证据附录,包含 SHA-256 校验值以及一份"我们未能证明的内容"清单。所有捕获均为我在自己机器上、使用一个包含伪造"金丝雀"密钥的一次性仓库所产生的自有流量——没有暴露任何真实凭据。
0. 摘要
xAI 官方的 Grok Build 编程 CLI(grok),在普通消费者登录下,做了三件值得精确记录的事:
- 它会将它所读取文件的内容——包括一个
.env机密文件——原封不动、未经脱敏地传输给 xAI。该机密出现在两个通道中:实时模型回合(POST /v1/responses)以及一个session_state上传并被接受(HTTP 200)经由POST /v1/storage——该二进制文件所路由到的端点,指向grok-code-session-tracesGCS bucket(见 §5)。 - 它会上传整个仓库——每个被跟踪文件的内容加上 git 历史——与智能体实际读取了什么无关。Grok 将工作区打包,并通过
POST /v1/storage上传。已直接证实:在一个真实代码库上,使用提示词“回复 OK,不要读取任何文件”,Grok 上传了整个仓库作为 git bundle (POST /v1/storage → 200);git clone对捕获的 bundle 进行解包,可恢复出一个智能体被告知不要打开的文件 —src/_probe/never_read_canary.txt——连同其唯一标记逐字,外加完整的 git 历史(附录uploaded_repo.bundle)。而且它具有可扩展性:在一个12 GB、由从未被读取的随机文件组成的仓库上,/v1/storage移动了5.10 GiB,全部 HTTP 200(在流中途被截断),而模型轮次通道仅移动了192 KB——一个约 27,800× 的比例这表明上传被绑定到了代码库,而非所读取的内容。没有任何存储上传失败;唯一的非 200 响应是一个模型使用配额(402/429)出现在/v1/responses以及一个无关的 404——并非存储大小上限。 - 存储目的地是一个 Google Cloud Storage 存储桶,
grok-code-session-traces(不是 AWS S3)——其名称在二进制文件中以及一份捕获到的metadata.json(gs://grok-code-session-traces/…)中被逐字提及。我在该 CLI 的安装/快速入门材料中并未发现这一机制被公开说明(这并非一次详尽的文档审查——见 §7),它默认处于启用状态,并且禁用“改进模型”并不会将其关闭(/v1/settings仍返回trace_upload_enabled: true;见 §6)。
这一切都不能证明 xAI 训练使用了这些数据——那是一个政策问题,在第 6 节中讨论。已被证明的是传输、接收和存储。
1. 受测对象(来源)
Install: curl -fsSL https://x.ai/cli/install.sh | bash # → ~/.grok/bin/grok
Auth: first launch opens a browser → login to X / SuperGrok (consumer account, not an API key)
二进制身份(复现:file $(readlink -f ~/.grok/bin/grok); ~/.grok/bin/grok --version; shasum -a 256 $(readlink -f ~/.grok/bin/grok)):
~/.grok/bin/grok -> ../downloads/grok-macos-aarch64
Mach-O 64-bit executable arm64
grok 0.2.93 (f00f96316d4b)
SHA-256: 2a97ba675bd992aa9b981e2e83776460d94f469b510c0b8efe28b50d236d767c
上传机制是一个第一方 Rust crate。对二进制文件执行 strings 会得到以下源码路径和常量(复现:strings <binary> | grep -E 'xai-data-collector|grok-code-session-traces|storage.googleapis'):
crates/codegen/xai-data-collector/src/gcs.rs
crates/codegen/xai-data-collector/src/storage_client.rs
crates/codegen/xai-data-collector/src/queue.rs
crates/codegen/xai-data-collector/src/file_access_tracker.rs
crates/codegen/xai-data-collector/src/circuit_breaker_observer.rs
crates/codegen/xai-grok-shell/src/upload/{gcs,turn,trace,manifest}.rs
grok-code-session-traces
storage.googleapis.com
"Uploading bytes to GCS via proxy"
2. 方法(可复现)
环境:macOS、Apple Silicon、grok 0.2.93,2026 年 7 月。
brew install mitmproxy;运行一次以在~/.mitmproxy/生成其 CA。- Trust the CA in the keychain (no sudo; Grok does not certificate-pin against it):
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db \ ~/.mitmproxy/mitmproxy-ca-cert.pem - Run Grok routed through the proxy (a
mitmdumpaddon logs, per request: method, host, path, response status, request byte size; and saves request bodies for xAI hosts):HTTPS_PROXY=http://127.0.0.1:8080 SSL_CERT_FILE=~/.mitmproxy/mitmproxy-ca-cert.pem \ grok -p "<prompt>" --cwd <repo> - 对于暂存产物的检查,在运行期间竞态复制
~/.grok/upload_queue/*,然后执行gzip -dc | tar -xO。
金丝雀仓库:每个文件都带有一个唯一标记,因此任何出现在捕获流量中的内容都可以明确追溯到某个文件。密钥文件 secrets.env / .env:
API_KEY=CANARY7F3A9-SECRET-should-not-leave
DB_PASSWORD=CANARY7F3A9-DBPASS
3. 发现 1——文件内容(包括一个密钥文件)被传输并接收(200)
声称:当 Grok 读取一个文件时,其内容会被传输至 xAI——被序列化进POST /v1/responses模型轮次请求体中,并打包成一个session_state归档文件,该归档文件被上传并被接受(HTTP 200)经由POST /v1/storage——文件内容未做任何脱敏处理。一个.env会像任何其他文件一样被发送。
Wire 数据产物——一个解密后的 48,070 字节 POST cli-chat-proxy.grok.com/v1/responses 请求体(可通过其内嵌的 "messages":[…]"model":"grok-4.5" JSON 识别为一次模型轮次)。它逐字包含了密钥文件(附录:secrets_responses_body.bin、secret_verbatim.txt):
…API_KEY=CANARY7F3A9-SECRET-should-not-leave\nDB_PASSWORD=CANARY7F3A9-DBPASS\n…"model":"grok-4.5"…
复现: grep -a "CANARY7F3A9-DBPASS" secrets_responses_body.bin → 匹配。全部六个文件标记(source、logic、README、嵌套 JS、API key、DB password)均可从解密后的 /v1/responses 正文中恢复。(该产物证明密钥已被传输至 /v1/responses 端点;原始正文文件不携带响应状态,因此接受(200)这一说法锚定于紧接其下的 /v1/storage 通道,该通道在 wire_12gb.log 中有状态映射。)
第二条通道——持久化到 Google Cloud Storage。 相同的内容被打包成一个session_state归档文件,通过POST /v1/storage上传。通过在暂存产物被清空之前对其进行解压来验证(附录:secrets_session_state.tar.gz):
gzip -dc secrets_session_state.tar.gz | tar -xO | grep -ao 'CANARY7F3A9-[A-Z]*'
→ CANARY7F3A9-SECRET, CANARY7F3A9-DBPASS, + all others
因此,该密钥不仅在传输过程中被处理;它还被写入一个将要存储的归档文件中。
先发制人地回应“是你让它去读那些密钥的”。一次对照运行,使用一个被明确告知不要打开的文件(untouched_secret.txt)以及提示词“只回复 OK,不要读取任何文件”产生的结果是没有在任何捕获到的正文中出现该文件的标记。因此,泄露范围仅限于 Grok确实读取的文件——但它读取得很随意(任何与任务相关的文件,包括一个.env),并且对该文件内容没有做任何脱敏处理。缺陷在于一个密钥文件被未脱敏地传输了出去,而不是读取这一行为本身。重要的范围澄清:这次对照表明,那个未被读取的文件并未出现在/v1/responses正文——也就是说通道 A(智能体读取的文件)。它并不清除 §4 中单独的全仓库/v1/storage快照(通道 B),而根据那里的体量证据,该快照确实会扫入从未被读取的文件;我无法解压/v1/storage代码库数据块来检查这个特定文件。因此,"未读取的文件不会被上传"这一说法仅对模型轮次通道成立,对代码库快照则不成立。(另有两点范围说明:(i) 在我的运行中,.env/secrets.env是被 git 跟踪的;我没有单独测试一个.gitignore文件是否仍会被上传,因此我不对 gitignore 作任何断言——根据file_access_trackercrate,该机制是由读取驱动的,但那个具体情形未经测试。(ii) 金丝雀值位于API_KEY=/DB_PASSWORD=一个.env/secrets.env但并非真实格式的高熵 token;我证明了这个.env是未经脱敏传输的,而不是说不存在针对比如sk-…形状密钥的脱敏器。)
4. 发现 2 —— 整个仓库以多 GB 规模被上传;唯一的限制是模型配额,而非存储大小
主张:Grok 会上传整个仓库的快照,在测试范围内没有存储大小上限。随着仓库增长,它会切换上传策略并持续返回 200;在一个 12 GB 的仓库上,73 个约 75.0 MB 的分块(5.10 GiB)上传零失败,随后抓包在流中途被截断。
线上抓包的大小扫描(使用不可压缩内容,因此 tar 无法缩小;每一步都使用全新会话)。只有 12 GB 那一行被保留为文件(wire_12gb.log);较小的行在扫描过程中被实时观察到,但其日志未被保存(见 §7):
| 仓库大小 | 上传行为(线上观察) | 状态 | 产物 |
|---|---|---|---|
| 64 MB | 单个 POST /v1/storage,req=50548145b(48 MB) | 200 | 已观测,未保留 |
| 约 600 MB | POST /v1/storage 以约 7.5 MB 的分块(数十个) | 全部 200 个 | 已观测,未保留 |
| 约 3 GB | POST /v1/storage/multipart/init → PUT storage.googleapis.com/grok-code-session-traces/multipart/<id> 以 50 MB 分片(direct-GCS PUT 行未保留 — §7) | 全部 200 个 | 已观测,未保留 |
| 约 12 GB | POST /v1/storage 以 75 MB 分块(req≈75014840b);在我停止运行前捕获了 73 个分块(约 5.1 GB) | 全部 200 个,0 次失败 | wire_12gb.log |
保留的产物: wire_12gb.log(附录)。其中包含 83 /v1/storage* 200 响应:82 个内容上传 POST …/v1/storage 请求——其中 73 个是每个约 75.0 MB 的分块(字节大小最小 75,014,811 / 最大 75,014,871,合计 5,476,083,317 B = 5.10 GiB / 5.48 GB),外加 9 个较小的 POST——以及 1 个 /v1/storage/batch_exists 去重检查。/v1/storage* 请求字节总计:5,476,228,005 B。零存储请求失败。抓包在上传仍在流式传输时被停止(最后一行是另一个约 75 MB 的分块 → 200),因此这证明的是 已上传 ≥5.1 GiB,在被截断时仍在增长——而非完整的 12 GB 已完成。复现(三条 grep,因此计数不存在歧义):grep 'cli-chat-proxy.grok.com/v1/storage' wire_12gb.log | grep -c '> 200' → 83(全部 /v1/storage*);grep 'POST cli-chat-proxy.grok.com/v1/storage ' wire_12gb.log | grep -c '> 200' → 82(仅内容 POST);grep req=75014 wire_12gb.log | grep -c '> 200' → 73(约 75 MB 的分块)。
范围——这是整个仓库,而不仅仅是智能体读取过的文件。通道 A(§3,/v1/responses)承载的是智能体打开的文件。§4 中的这次上传是一个独立的通道 B:整个工作区的快照。两条证据:
- (a) The decisive byte split (load-bearing). In the same captured 12 GB session — a repo of 100 % random files the agent never read — the two channels moved wildly different volumes:
- 通道 A
/v1/responses(模型轮次):196,705 B = 总计 192 KB,分布在 5 个请求中,单轮最大 60,394 B。 - 通道 B
/v1/storage:5,476,228,005 B = 5.10 GiB。 - 这大约是 27,800 倍的比率(5,476,083,317 ÷ 196,705)。该模型 显然从未读取过这些文件(192 KB 无法承载 5 GiB 的内容),然而其中 5.10 GiB 却经由
/v1/storage流出——而在整个扫描过程中,/v1/storage的数据量 与仓库总大小同步变化(64 MB → 12 GB)。从一个从未被读取的仓库中流出 GB 级别的字节,只可能是一份整仓库快照。
- 通道 A
- (b)该二进制文件自身的路径/字符串佐证了这一机制:
after_codebase.tar.gz、xai-grok-shell/src/upload/{trace,turn}.rs、repo_state.upload、"collecting workspace files"、"spawning background coordinator"。 - (c) 一份分阶段生成的代码库清单枚举了一个从未被读取的文件,并将其内容寻址到该 GCS 存储桶。在一次单独的运行中,针对一个真实的 298 个文件
cereblab_api代码库,我竞速复制了 Grok 为上传而分阶段准备的快照(附录:staged_base_tree_manifest.json,staged_metadata.json)。该清单列出了src/_probe/never_read_canary.txt——一个我植入且从未要求 Grok 打开的文件——与真实源文件并列(src/apns.ts,src/index.ts,……);并且metadata.json为每个文件指定了一个目标位置fileId,其形式为gs://grok-code-session-traces/repo_changes_dedup/v2/supplemental/sha256_…。因此整棵目录树,包括未被读取的文件,都被枚举并内容寻址,以便上传到指定的 GCS 存储桶。(那次运行在上传内容数据块之前就被配额限制阻断了;上面那次 12 GB 的运行独立证明了GB 级别的数据确实离开了网络,全部 200.) - (d) 确凿证据——一个从未被读取的文件的内容从上传的 git bundle 中逐字恢复(单次 SuperGrok 运行)。在账户升级(配额提升)后,我在真实的
cereblab_api仓库上重新运行,使用了明确的提示词Reply with exactly: OK. Do not read or open any files.,并事先植入了一个唯一标记CANARY-XR47P2-NEVERREAD-UNIQUE在src/_probe/never_read_canary.txt中。Grok 上传了整个仓库作为 git bundle通过POST cli-chat-proxy.grok.com/v1/storage → 200 (req=152102b)(附录uploaded_repo.bundle,SHA-25673b9c0af06311bae35c3ed03274d0eec2846e76762828d10b09757ca41bd6024)。运行git clone uploaded_repo.bundle可重建该仓库,而src/_probe/never_read_canary.txt包含了该标记逐字——这是一个智能体被明确告知不要打开的文件。该 bundle 还携带了完整的 git 历史(4 个提交,47 个文件)。这是针对每个文件内容的无懈可击的证明:整个仓库——包括未被读取的文件——都离开了这台机器并被接受(200)。上传机制是一个git bundle,所以“整个仓库”是字面意义上的(每一个被跟踪的文件 + 历史记录)。在第二个无关的代码库上复现:在cereblab_authCloudflare-Worker 仓库上进行的相同捕获产生了一次 git-bundle 上传(POST /v1/storage → 200,31,743 B),从中git clone逐字恢复了它自己从未被读取的标记CANARY-AUTH-4T8K2-NEVERREAD(附录uploaded_repo_auth.bundle,SHA-2560ee536538bcd1ee72a258f9977ab69f8a9b1ac240491b91a4e94335b4d83c768)。两个独立的仓库,相同的结果。
(提示词说明:12 GB 会话是交互式的,我没有逐字记录其提示词,但 192 KB 的 Channel-A 总量足以证明,无论提示词是什么,都没有发生批量读取;另一次单独的无头对照运行使用了显式提示词 Reply exactly OK, do not read any files,并确认未读取的文件不会出现在 Channel A 中。)
(此前所说的"唯一缺口"现已被证据 (d) 填补:一次单独的 SuperGrok 运行中,某个从未被读取过的文件的内容,通过线路捕获的、返回 200 状态的 git-bundle 上传被还原出来。那次 12 GB 的运行仍然是证明此方法可扩展至 GB 级数据量的依据。)
没有任何存储/上传请求失败——82 次 /v1/storage 调用全部返回 200。整个捕获中唯一的非 200 响应出现在模型端点上,外加一次会话记账调用(完整集合见 wire_12gb.log;/v1/responses 行也在 model_limit.txt 中):
POST /v1/responses -> 402 (Payment Required) ×1
POST /v1/responses -> 429 (Too Many Requests) ×3
POST /v1/sessions/<id>/replicas/update -> 404 ×1 (session bookkeeping, not an upload)
最后,以纯文本形式输出到 stdout:
You've reached your free Grok Build usage limit for now. Get SuperGrok for much higher limits…
402/429 是模型使用配额;那个孤立的 404 与存储无关。值得注意的是,在模型轮次被限流之后,存储上传仍持续返回 200(76 次 /v1/storage 200 响应出现在首次 429 之时或之后)——代码库上传与模型是否作答无关。
先发制人地回应"你把本地磁盘缓存和上传搞混了"。这一论断严格依据线路抓取的 200 状态上传,即文件字节离开本机的记录(/v1/storage 在保留的 wire_12gb.log 中请求体为 7.5–75 MB;3 GB 的 50 MB PUT 到 storage.googleapis.com 也在线路上被观察到,但该日志未被保留——见 §7)。它不依赖 ~/.grok/upload_queue 的排空——队列排空是有歧义的(无论成功还是丢弃都会清空),此处明确不将其用作证据。(早先一份从队列排空推断上传的草稿是错误的,已被撤回;见 §7。)
5. 发现 3——目标位置、遥测,以及文档中未披露的内容
- 存储目标是 Google Cloud Storage,存储桶为
grok-code-session-traces。这一结论依据的是保留的二进制字符串grok-code-session-traces、storage.googleapis.com,以及 "Uploading bytes to GCS via proxy"(crate_strings.txt),还依据一份保留的分阶段metadata.json,其逐文件的fileId字面上就是gs://grok-code-session-traces/repo_changes_dedup/v2/…/sha256_…(staged_metadata.json),并得到在 3 GB 时观察到的直接storage.googleapis.com分段 PUT 请求的佐证(实时观察到;该日志未被保留——见 §7)。它不是 AWS S3(二进制文件中链接了aws-sdk-s3作为备用路径,以及 AWS STS/SSO 用于认证,但二进制文件中指明的目标——且在 3 GB 时于线路上观察到的——是 GCS)。 - 第三方遥测:
POST api.mixpanel.com/track和/engage(Mixpanel),外加POST grok.com/_data/v1/events—— 全部返回 200。 - 未在安装文档中披露(范围有限的论断): 在我所查阅的 CLI 安装脚本或快速入门材料中,我没有找到关于
repo_state/session_state上传至grok-code-session-traces,或~/.grok/upload_queue暂存的相关描述(这并非对全部 xAI 文档的详尽审计——见 §7)。该机制在标准消费者登录下默认处于激活状态。 - 可靠性说明(与隐私无关):
~/.grok/upload_queue每轮会暂存约 3 GB 的快照,在高负载下可能增长到数十 GB 并耗尽磁盘空间。这是一个真实的 bug,与上传是否成功无关。
6. 同意与政策——如实陈述
来源:Hacker News 热门(buzzing.cc 中文翻译) · gist.github.com