# xAI Grok Build CLI 网络流量分析：上传仓库全部文件及 git 历史

- 来源：Hacker News 热门（buzzing.cc 中文翻译）
- 作者：jhoho
- 发布时间：2026-07-12 11:59
- AIHOT 分数：74
- AIHOT 标记：精选
- AIHOT 链接：https://aihot.virxact.com/items/cmrhagju201pqbir7t0tnsgfy
- 原文链接：https://gist.github.com/cereblab/dc9a40bc26120f4540e4e09b75ffb547

## 精选理由

这是我见过最严谨的隐私调查，每步可复现——Grok CLI 会在用户不知情下将完整仓库、.env 密钥甚至未读文件原样上传至 xAI 的 GCS，默认开启且无法真正关闭，所有用 Grok Build 的开发者都得重审自己的 secrets。

## AI 摘要

对 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 使用这些数据进行训练，但证实了数据被传输、接收并存储。

## 正文

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. 同意与政策——如实陈述
