xAI 的 Grok Build CLI 实际向 xAI 发送了什么:一次网络层面分析
一次有节制、可复现的拆解分析。研究结果由捕获的工件(端点、HTTP 方法、状态码、字节大小、主机)和复现命令支撑;对于现场观察到但未保存为文件的发现,第 7 节明确说明了这一点。第 8 节是证据附录,包含 SHA-256 哈希值和一份“我们未证明的内容”清单。所有捕获数据均来自我本机上的个人流量,使用一个包含虚假“金丝雀”密钥的一次性仓库——未暴露任何真实凭证。
0. 摘要
xAI 的官方 Grok Build 编码 CLI(grok),在普通消费者登录状态下,会执行三件值得精确记录的事情:
- 它会将其读取的文件内容——包括 .env 密钥文件——逐字且未经编辑地传输给 xAI。该密钥出现在两个通道中:实时模型交互轮次(POST /v1/responses)以及通过 POST /v1/storage 上传并被接受(HTTP 200)的 session_state 存档——该二进制文件将流量路由至 grok-code-session-traces GCS 存储桶(参见第 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 库。对二进制文件执行 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
mitmdump addon 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 文件会像其他任何文件一样被发送。
网络流量工件——一个已解密的 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 密钥、数据库密码)均可从解密后的 `/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_tracker 库的机制,上传是由读取行为驱动的,但这个特定场景未经测试。(ii) 金丝雀值位于 .env/secrets.env 文件中的 API_KEY=/DB_PASSWORD= 键值对里,但它们并非真实格式的高熵令牌;我证明了这个 .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,请求体=50548145b(48 MB) | 200 | 已观测,未保留 |
| ~600 MB | 以约 7.5 MB 为分片多次 POST /v1/storage(数十次) | 全部 200 | 已观测,未保留 |
| ~3 GB | POST /v1/storage/multipart/init → PUT storage.googleapis.com/grok-code-session-traces/multipart/<id>,以 50 MB 为分片(直接 GCS PUT 行未保留 — §7) | 全部 200 | 已观测,未保留 |
| ~12 GB | 以 75 MB 为分片 POST /v1/storage(请求体≈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 级别的数据,只可能是整个仓库的快照。
- (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 运行)。在账户升级(配额解除)后,我使用明确的提示词“请仅回复:OK。不要读取或打开任何文件。”对真实的 cereblab_api 仓库重新运行,并在 src/_probe/never_read_canary.txt 中植入了一个唯一的标记 CANARY-XR47P2-NEVERREAD-UNIQUE。Grok 通过 POST cli-chat-proxy.grok.com/v1/storage 将整个仓库作为 git bundle 上传,返回 200 状态码(请求 ID=152102b)(附录:uploaded_repo.bundle,SHA-256 73b9c0af06311bae35c3ed03274d0eec2846e76762828d10b09757ca41bd6024)。运行 `git clone uploaded_repo.bundle` 可重建该仓库,且 src/_probe/never_read_canary.txt 中包含逐字的标记——这是一个智能体被明确告知不要打开的文件。该 bundle 还携带了完整的 git 历史(4 次提交,47 个文件)。这是无可辩驳的逐文件内容证明:整个仓库——包括未读取的文件——离开了机器并被接受(200 状态码)。上传机制是 git bundle,因此“整个仓库”是字面意义上的(每个被追踪的文件加历史记录)。在第二个不相关的代码库上进行了复现:对 cereblab_auth Cloudflare-Worker 仓库进行了完全相同的捕获,产生了 git-bundle 上传(POST /v1/storage → 200,31,743 字节),从中通过 `git clone` 恢复出了其自身的从未读取标记 CANARY-AUTH-4T8K2-NEVERREAD(附录:uploaded_repo_auth.bundle,SHA-256 0ee536538bcd1ee72a258f9977ab69f8a9b1ac240491b91a4e94335b4d83c768)。两个独立的仓库,结果相同。
(提示词说明:12 GB 的会话是交互式的,我没有记录其逐字提示词,但 192 KB 的 Channel-A 总量足以证明无论提示词是什么,都没有发生批量读取;另一次独立的无头控制运行使用了明确的提示词“请仅回复 OK,不要读取任何文件”,并确认未读取的文件在 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 状态上传(在保存的 wire_12gb.log 中,`/v1/storage` 请求体大小为 7.5–75 MB;向 storage.googleapis.com 发起的 3 GB 的 50 MB PUT 请求也在抓包中看到,但该日志未被保留——见第 7 节)。它不依赖于 `~/.grok/upload_queue` 的队列清空——队列清空含义模糊(它在成功和丢弃时都会清空),此处明确不作为证据使用。(早期版本根据队列清空推断上传的做法是错误的,已被撤回;见第 7 节。)
5. 发现 3——目标位置、遥测数据以及文档中未提及的内容
- 存储目标位置是 Google Cloud Storage,存储桶名为 grok-code-session-traces。这一结论基于保存的二进制字符串 grok-code-session-traces、storage.googleapis.com 以及“通过代理向 GCS 上传字节”(crate_strings.txt),还基于一份保存的分阶段 metadata.json,其中每个文件的 fileId 字面值为 `gs://grok-code-session-traces/repo_changes_dedup/v2/…/sha256_…`(staged_metadata.json),并且与在 3 GB 流量中观察到的直接向 storage.googleapis.com 发起的 multipart 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 并耗尽磁盘空间。这是一个真实存在的缺陷,与上传是否成功无关。
6. 同意与政策——如实陈述