xAI Grok Build CLI のデータ送信実態
独立したセキュリティ研究者による分析により、xAI の Grok Build CLI がユーザーのコードリポジトリ全体と機密情報(.env ファイルなど)を明示的な同意なくサーバーへ送信していることが実証された。
キーポイント
機密情報の無差別送信
CLI が読み込んだファイルの内容、特に .env ファイルに含まれるシークレットキーが、モデルへのプロンプトとセッション状態のアーカイブとして、未加工のまま xAI サーバーへ送信されることが確認された。
リポジトリ全体のアップロード
特定のファイルを読み込むよう指示しなくても、Grok はワークスペース内のすべての追跡済みファイルと Git 履歴を含むリポジトリ全体を /v1/storage エンドポイント経由でアップロードする挙動を示した。
プライバシーオプトアウトの限界
xAI が導入した「/privacy opt-out」機能は、送信されるデータのブロックではなく、単にデータ保持期間を制御する設定であり、送信自体を防止するものではないことが実証された。
再現可能なセキュリティリスク
研究者は架空のシークレット(canary secrets)を用いたテスト環境でこの挙動を検証し、実際の認証情報が漏洩する可能性を裏付ける具体的な証拠(HTTP 200 レスポンスなど)を提供している。
機密情報の無修正送信と保存
Grok Build CLI は、読み込まれたファイル(.env などのシークレットファイル含む)の内容を、赤文字化やマスキングなしで xAI サーバーへ送信し、Google Cloud Storage に永続的に保存します。
エージェントの指示に無関係な全リポジトリのアップロード
「ファイルを読まないで」というプロンプトを与えても、CLI はワークスペース全体(未読ファイルや git 履歴を含む)を自動的に Git バンド形式でアップロードし、サーバー側で正常に受け付けられています。
モデル改善オプトアウト機能の無効性
設定で「モデル改善」をオフにしても、リポジトリ全体のアップロードと保存は継続され、ユーザーがコードをサーバーへ送信する挙動自体を停止させる設定は見つかりませんでした。
重要な引用
It transmits the contents of files it reads — including a .env secrets file — to xAI, verbatim and unredacted.
It uploads the whole repository — every tracked file's content plus git history — independent of what the agent reads.
added a /privacy opt-out — which I wire-tested and found to be a data-retention setting, not a block on what's sent
"The whole repository — every tracked file's content plus git history — independent of what the agent reads."
"Opting out does not stop your repository from leaving the machine."
影響分析・編集コメントを表示
影響分析
この分析は、AI エージェントがユーザーのローカル環境からデータを収集する際の透明性と同意プロセスにおける重大な欠陥を浮き彫りにしました。特に、機密情報や全コードベースが意図せず外部サーバーへ送信されるリスクは、企業のコンプライアンスや開発者の信頼に直結する深刻な問題です。xAI は対応を発表していますが、根本的な設計思想(データ収集の最小化原則)の見直しを迫られる重要な転換点と言えます。
編集コメント
本記事は、AI ツールの利便性追求とデータプライバシー保護のバランスが崩れている現状を鋭く指摘しています。開発者コミュニティにおいては、ツール利用時の「暗黙的なデータ送信」に対する警戒感を高める重要な事例として捉えるべきです。
xAI Grok Build CLI が実際に xAI に送信しているデータの実態
——ワイヤーレベルでの分析(grok 0.2.93)
更新 — 2026-07-14: これは grok 0.2.93 の時点におけるオリジナルのワイヤーレベル分析です。公開以降、xAI はサーバー側でアップロード機能を無効化(disable_codebase_upload: true)しました。また、/privacy オプトアウトを追加しましたが、これをワイヤーレベルで検証した結果、これは「送信されるデータのブロック」ではなく、「データ保持期間の設定」であることが判明しています。さらに、イーロン・マスクは過去にアップロードされた全データの削除を公約しましたが、その完了はまだ確認されていません。
詳細な経緯と証拠については、以下のリンクをご覧ください:
https://cereblab.com · https://github.com/cereblab/grok-build-exfil-repro
*著者:@cereblab — 独立系 AI セーフティチェッカー。ご自身でも再現可能です:github.com/cereblab/grok-build-exfil-repro*
これは計測可能で再現性のある技術解析です。発見された事実は、キャプチャしたアーティファクト(エンドポイント、HTTP メソッド、ステータスコード、バイトサイズ、ホスト名)と再現可能なコマンドによって裏付けられています。ファイルとして保存されていないが実時間で観測された事象については、§7 で明示的にその旨を記しています。セクション 8 は証拠の付録であり、SHA-256 ハッシュ値や「証明できなかったこと」の一覧を含んでいます。
なお、すべてのキャプチャは著者自身のネットワークトラフィックに基づいています。使用したリポジトリには架空の「カナリア(検知用)シークレット」のみが含まれており、実際の認証情報は一切漏洩していません。
0. まとめ
xAI が公式に提供するコーディング CLI ツール「Grok Build (grok)」は、通常の一般ユーザーアカウントで動作させた場合、以下の 3 つの重要な挙動を示します。これらは正確に文書化する価値があります:
このツールは、読み込んだファイルの内容をそのまま xAI に送信します。.env シークレットファイルを含め、一切の削除や隠蔽なく転送されるのです。
その機密情報は 2 つの経路で外部へ漏れます。1 つ目はライブモデルの応答時(POST /v1/responses)です。もう 1 つはセッション状態のアーカイブをアップロードする際(HTTP 200 で成功した POST /v1/storage)。このエンドポイントは、バイナリが grok-code-session-traces という G バケットへデータを転送するために使用されています(詳細は§5 を参照)。
さらに、エージェントが実際に読み込んだかどうかに関わらず、リポジトリ全体をアップロードします。追跡対象となっているファイルの内容と Git の履歴すべてが含まれます。Grok はワークスペースをパッケージ化し、POST /v1/storage 経由で送信します。
この事実は実証済みです。実際のコードベースに対して「返信は OK にし、どのファイルも読まないで」というプロンプトを与えた際でも、Grok はリポジトリ全体を Git バンドルとしてアップロードしました(POST /v1/storage → 200)。キャプチャしたバンドルを git clone で復元すると、エージェントに開くなと指示されていたファイル src/_probe/never_read_canary.txt が、固有のマーカーを含めてそのまま回復します。さらに完全な Git 履歴も含まれています(付録の uploaded_repo.bundle を参照)。
スケーラビリティについても確認済みです。12 GB のリポジトリ(一度も読まれないランダムファイルで構成)に対して、/v1/storage エンドポイントでは 5.10 GiB が転送されました(途中までログが切り捨てられています)。すべての通信は HTTP 200 で成功しています。一方、モデルの応答チャネルで転送されたのはわずか 192 KB です。この約 27,800 倍という比率から、アップロードが「読み込んだ内容」ではなく「コードベース全体」に基づいていることが明確になります。
ストレージへのアップロードに失敗した事実は一度もありませんでした。HTTP ステータスが 200 でなかったのは、/v1/responses におけるモデル利用枠の制限(402 または 429)と、1 つの無関係な 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. テスト対象の特定(出所)
インストール:
curl -fsSL https://x.ai/cli/install.sh | bash # → ~/.grok/bin/grok
認証: 初回起動時にブラウザが開き、X / SuperGrok のログイン画面へ遷移します(API キーではなく、一般消費者アカウントでのログインです)。
バイナリの身元確認(再現手順):
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 | grep -E 'xai-data-collector|grok-code-session-traces|storage.googleapis')すると、以下のソースパスや定数が見つかります。
必ず JSON 形式で返してください。translation フィールドのみ。他のフィールドは一切追加しないこと — 余計なフィールドを書こうとして本文がトークン上限で打ち切られる事故を防ぐため:
{"translation": "リライト全文"}
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 月。
mitmproxyをインストールし、一度実行して CA 証明書を生成します。
brew install mitmproxy- 認証済み CA をログインキーチェーンに追加します(sudo は不要です。Grok はこの証明書に対してピン留めを行いません)。
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db \
~/.mitmproxy/mitmproxy-ca-cert.pem- プロキシを経由して Grok を実行します。
mitmdumpのアドオンが、リクエストごとにメソッド、ホスト、パス、レスポンスステータス、リクエストバイトサイズをログ出力し、xAI 関連のホストからのリクエストボディも保存します。
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 — ファイルの内容(シークレットファイルを含む)が送信され、受け入れられている(HTTP 200)
主張: Grok がファイルを読み込む際、その内容は xAI へ送信されます。具体的には、POST /v1/responses モデル・トーンのボディに直列化されてパッケージ化され、セッション状態のアーカイブとしてアップロードされます。そして、ファイルの内容が一切削除(redaction)されることなく、POST /v1/storage を介して受け入れられ(HTTP 200)ます。.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 を実行すると一致します。6 つのファイルマーカー(ソースコード、ロジック、README、ネストされた JS、API キー、DB パスワード)はすべて、復号化された /v1/responses のボディから回復可能です。このアーティファクトは、シークレットが/v1/responses エンドポイントへ送信されたことを証明するものです(生ファイルにはレスポンスステータスは含まれていないため、受け入れ(200)の主張は、直下の /v1/storage チャンネルに裏付けられています。これは wire_12gb.log 内でステータスマッピングされています)。
2 つ目のチャネルは、Google Cloud Storage に永続化されます。 同じコンテンツは、POST /v1/storage を介してアップロードされる session_state アーカイブにパッケージ化されています。ステージングされたアーティファクトが削除される前に解凍・確認することでこれを証明できます(付録:secrets_session_state.tar.gz)。
gzip -dc secrets_session_state.tar.gz | tar -xO | grep -ao 'CANARY7F3A9-[A-Z]*'
→ CANARY7F3A9-SECRET, CANARY7F3A9-DBPASS、その他すべてつまり、機密情報は処理中の一時データとして扱われるだけでなく、ストレージ宛てのアーカイブに書き込まれていることになります。
「秘密を読めと指示した」という批判への先回り対応
エージェントに開くことを禁止されたファイル(untouched_secret.txt)を用いた対照実験を行いました。プロンプトは「OK とだけ返信し、どのファイルも読まないでください」です。その結果、キャプチャされたレスポンス本文のどこにも、そのファイル固有のマーカーが出現することはありませんでした。つまり、この情報漏洩は Grok が実際に開いたファイルに限定される現象ですが、Grok はタスクに関連するあらゆるファイル(.env ファイルを含む)を広く読み込み、その内容に対して何らかの赤文字化(マスキング)も行っていません。問題の本質は「秘密ファイルがそのまま送信された」点であり、「ファイルを開いた行為」自体に欠陥があるわけではありません。
重要な範囲の再確認:この対照実験で示されたのは、未開封のファイルが /v1/responses 本文に含まれないという事実です。これはエージェントが読み込むファイルを扱う *Channel A* の話です。一方、§4 で言及した *Channel B*(リポジトリ全体の /v1/storage スナップショット)については、この実験では証明されません。そこでのボリューム証拠から推測される限り、一度も開かれていないファイルまで含まれてしまう可能性があります。私は /v1/storage のコードベースチャンクを解凍して特定のファイルを確認することはできませんでした。したがって、「未開封ファイルはアップロードされない」という主張が真となるのはモデルのターン(対話)チャンネルに限られ、コードベースのスナップショットには当てはまりません。
さらに 2 つの範囲に関する補足があります。(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 × 73 = 約 5.10 GiB)がゼロ失敗でアップロードされ、キャプチャはストリーミング途中まで記録されました。
ワイヤーキャプチャによるサイズスウィープの結果です。圧縮不可能なコンテンツのため tar アーカイブも縮小せず、各ステップごとに新規セッションを立ち上げています。12 GB の行のみがファイルとして保存(wire_12gb.log)され、それより小さいサイズの行はスウィープ中にライブで確認されましたがログは保存されていません(§7 参照):
| リポジトリサイズ | アップロード挙動(ワイヤー上で観測) | ステータス | 成果物 |
|---|---|---|---|
| 64 MB | single 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/(50 MB パート単位) | すべて 200 | 観測済み、保存なし
*注:直接 GCS への PUT 行は保持されていません(§7 参照)* |
| ~12 GB | POST /v1/storage を 75 MB チャンクで実行 (req≈75014840b);73 チャンク(約 5.1 GB)をキャプチャ、その後実行停止 | すべて 200、失敗なし | wire_12gb.log |
保存されたアーティファクト: wire_12gb.log(付録)。このファイルには、83 件の /v1/storage* に対する 200 OK レスポンスが含まれています。内訳は、82 件のコンテンツアップロード POST リクエスト(…/v1/storage)と、1 件の重複チェック用 /v1/storage/batch_exists です。
POST リクエストの詳細を見ると、73 件が約 75.0 MB のチャンクで構成されています。各ファイルのサイズは最小 75,014,811 バイト、最大 75,014,871 バイトで、合計すると 5,476,083,317 バイト(約 5.10 GiB / 5.48 GB)に達します。残りの9 件は小規模な POST リクエストです。
/v1/storage* 関連の全リクエストバイト数は 5,476,228,005 バイトでした。ゼロ件の失敗が発生しています。キャプチャはアップロードが進行中の時点で停止されました(最後の行も約 75 MB のチャンクで 200 OK を返しているため)。これは、5.1 GiB 以上が転送され、切り捨てられた時点でもまだ増加し続けていたことを示しています。つまり、12 GB がすべて完了したわけではありません。
再現テスト(3 つの 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です。この結論を支える 2 つの根拠があります。
- (a) 決定的なバイト数の乖離(負荷の偏り):同じくキャプチャされた 12 GB のセッション内(エージェントが一度も読まなかったランダムファイルで構成されるリポジトリ)において、2 つのチャンネルは劇的に異なる量のデータを転送していました。
Channel A の /v1/responses(モデルの応答)では、合計 5 リクエストにわたって 196,705 バイト(約 192 KB)が送られ、そのうち最大の単一応答は 60,394 バイトでした。
一方、Channel B の /v1/storage では 5,476,228,005 バイト(約 5.10 GiB)が送信されています。
これは約 27,800 倍の比率です。モデルが実際にファイルをインデックスした形跡はありません。192 KB のデータ量で 5 GiB もの内容を伝えることは物理的に不可能だからです。しかし、実際には /v1/storage を経由して 5.10 GiB が外部へ流出しました。さらに、スキャン全体を通じて /v1/storage の送信量はリポジトリの総サイズ(64 MB から 12 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)。マニフェストには、私が事前に仕掛けて Grok に開くよう指示もしていない src/_probe/never_read_canary.txt というファイルが、src/apns.ts や src/index.ts といった実際のソースコードと共にリストアップされています。また metadata.json では、各ファイルに gs://grok-code-session-traces/repo_changes_dedup/v2/supplemental/sha256_… という形式の宛先 fileId が付与されています。つまり、読み込まれていないファイルも含め、ツリー全体が列挙され、指定された GCS バケットへのアップロード用にコンテンツアドレス化されているのです。(なお、この実行ではコンテンツブロブのアップロード前にクォータ制限に抵触しましたが、前述の 12 GB の実行結果が独立して、実際にギガバイト規模のデータがネットワークを流れていることを証明しています。すべて 200 ファイル分です。)
- (d) 決定版:未読のファイル内容が、アップロードされた Git バンドルからそのまま復元されるケース(SuperGrok の単一実行)。アカウントをアップグレードしてクォータ制限を解除した後、実際の cereblab_api リポジトリで再実行しました。プロンプトは「返信は『OK』のみとし、ファイルの読み込みや開封は一切行わないこと」という明確な指示でした。その際、src/_probe/never_read_canary.txt に固有のマーカー「CANARY-XR47P2-NEVERREAD-UNIQUE」を仕込んでおきました。Grok はリポジトリ全体を git bundle として POST cli-chat-proxy.grok.com/v1/storage へ送信し、サーバーは 200(OK)を返しました(req=152102b)。添付されたアップロード済みバンドルファイル(uploaded_repo.bundle, SHA-256: 73b9c0af06311bae35c3ed03274d0eec2846e76762828d10b09757ca41bd6024)を git clone で復元すると、src/_probe/never_read_canary.txt には仕込んだマーカーがそのまま残っており、これはエージェントに開封禁止と指示されたファイルです。バンドルには完全な Git 履歴(コミット数 4、ファイル数 47)も含まれています。つまり、「未読のファイルを含むリポジトリ全体」が機械から送信され、サーバー側で正常に受け入れられたという、ファイルごとの内容に関する確実な証拠です。アップロード機構が git bundle であるため、「リポジトリ全体」という表現は文字通りの意味を持ちます(追跡対象となる全ファイルと履歴が含まれます)。この結果は、別の無関係なコードベースでも再現されました。cereblab_auth の Cloudflare Worker リポジトリで同様のテストを行ったところ、同じく git-bundle 形式でのアップロード(POST /v1/storage → 200, サイズ 31,743 バイト)が行われ、そこから復元された never-read マーカー「CANARY-AUTH-4T8K2-NEVERREAD」がそのまま確認できました(添付ファイル:uploaded_repo_auth.bundle, SHA-256: 0ee536538bcd1ee72a258f9977ab69f8a9b1ac240491b91a4e94335b4d83c768)。独立した 2 つのリポジトリで、同じ結果が得られました。
(プロンプトの補足:12 GB のセッションは対話形式だったため、そのプロンプトを逐語的に記録していません。しかし、Channel-A での総データ量が 192 KB に過ぎないという事実は、どのようなプロンプトであったにせよ、大量のファイル読み込みが行われなかったことを明確に示しています。また、ヘッドレス環境で「Reply exactly OK, do not read any files and confirmed an unread file is absent from Channel A」という明示的なプロンプトを用いた制御実験でも、未読ファイルが Channel-A に存在しないことが確認されています。)
以前の「1 つの欠落」は、証拠(d)によって解消されました。これは、特定の一度も読み込まれていないファイルの内容が、ネットワーク経由でキャプチャされた 200 ステータスの git-bundle アップロードから復元された SuperGrok の単一実行事例です。一方、12 GB の実行は、この処理が GB オーダーのボリュームにまで拡張可能であることを証明しています。
ストレージへのアップロード要求で失敗したものはありませんでした。82 回行われた /v1/storage 呼び出しのうち、すべてがステータスコード 200 を返しています。キャプチャ全体で見つかった非 200 の応答は、モデルエンドポイントとセッション管理の 1 つの呼び出しのみです(詳細は 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//replicas/update -> 404 ×1 (セッション管理の呼び出しであり、アップロードではありません)
そして最後に、標準出力(stdout)には以下のplain text が表示されました。
You've reached your free Grok Build usage limit for now. Get SuperGrok for much higher limits…
402 や 429 はモデル利用枠の制限を示すものであり、唯一の 404 エラーはストレージとは無関係です。特筆すべきは、モデルからの回答がレート制限(429)された後でも、ストレージへのアップロードは 200 OK を返し続けていた点です。具体的には、最初の 429 エラーが発生した時点以降に、/v1/storage 経由で 76 件の 200 ステータスが確認されています。つまり、コードベースのアップロードは、モデルが回答するかどうかとは独立して行われています。
「ローカルのディスクキャッシュとアップロードを混同しているのではないか」という指摘への先回りですが、この主張はあくまでネットワーク上でキャプチャされたファイル転送時の 200 ステータスに基づいています。具体的には、保存された wire_12gb.log に記録されている /v1/storage リクエストのボディサイズが 7.5MB から 75MB の範囲にあるファイルバイト列が、マシンから送信されている様子が確認されています。また、storage.googleapis.com 宛てに 3GB や 50MB の PUT リクエストが行われた事実もネットワーク上で捉えられていますが、そのログは保存されていません(§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 ファイル内のファイル ID が「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 が使われていますが、実際に指定されている宛先およびネットワーク上で確認されたのは GCS です。
第三者によるテレメトリ送信としては、Mixpanel への POST リクエスト(api.mixpanel.com/track および /engage)と、grok.com/_data/v1/events への POST リクエストが行われており、いずれもステータスコード 200 を返しています。
設定ドキュメントには明記されていません(範囲限定の主張):私が確認した CLI のインストールスクリプトやクイックスタート資料には、「grok-code-session-traces」への repo_state や session_state のアップロード、および ~/.grok/upload_queue によるステージングに関する記述は見当たりませんでした。これは xAI のドキュメント全体を網羅的に監査したものではないため、詳細は§7を参照してください。ただし、この機能は標準的なコンシューマーログインではデフォルトで有効になっています。
信頼性に関する注意(プライバシーとは別問題):~/.grok/upload_queue には、1 トーンあたり約 3 GB のスナップショットがステージングされます。負荷がかかる状況では数十 GB に膨れ上がり、ディスク容量を枯渇させる可能性があります。これはアップロードの成否に関わらず存在する実際のバグです。
6. コンセントとポリシー — 誠実に明記されていること
「クラウド AI ツールはコンテキストを送信するものだ。それは当然のことだ」という指摘には同意します。実際、コードを処理して実行するためには、クラウドベースのコーディングエージェントがサーバーへコードを送信する必要があります。
しかし、今回のケースで特筆すべき点は 3 つあります。(a) シークレットファイル(例:.env)が非公開のまま送信されること、(b) データが一時的な処理だけでなく、特定の GCS バケットに永続保存されること、そして (c) このアップロード機能が CLI の設定資料 (§7) に明記されておらず、デフォルトで有効になっていることです。
「利用規約(ToS)に記載されており、オプトイン方式だ」という主張については、xAI の消費者向けポリシーではモデル改善のためのデータ利用について広く開示しており、オプトアウト(利用停止)の仕組みが用意されています(grok.com → 設定 → データ → 「モデルを改善する」)。ただし、プライベートチャットは自動的にオプトアウトされ、オプトアウト機能は未来からの適用であり、過去のデータを遡って削除するものではありません。
しかし、「広範なトレーニング利用の開示」と「この特定のメカニズムの文書化」は別問題です。私が確認した CLI の資料 (§7 には、これは完全なドキュメント監査ではないと注記されています) では、repo_state/upload_queue/grok-code-session-traces というパイプラインに関する記述は見当たりませんでした。つまり、これらの資料に基づけば、ユーザーはこの特定の仕組みについて具体的に知らされていないことになります。
出典:xAI プライバシーポリシー (https://x.ai/legal/privacy-policy)、消費者利用規約 (https://x.ai/legal/terms-of-service)。
- 「モデルの改善」オプションをオンにしてもオフにしても、結果は変わりません。どちらの状態でも、リポジトリ全体が同じ方法でアップロードされます。デフォルト設定では、Grok は一度も読まれないリポジトリから 5.10 GiB をアップロードしました(§4)。その後、「モデルの改善」をオフにして再実行しましたが、Grok は依然としてリポジトリ全体を git bundle 形式でアップロードし続けました(§4(d)、POST /v1/storage → 200、git clone で復元された未読ファイルも含まれる)。また、サーバーから CLI へ返される /v1/settings の応答でも、「trace_upload_enabled」が true、「upload_enabled」が true、「session_registry_enabled」が true と表示され続けました(「max_upload_file_bytes」は 1073741824 で、ファイルごとの上限は 1 GiB です)。このオプトアウト機能はトレーニングの有無を制御するものであり、コードのアップロードや保存自体を止めるものではありません。どちらの場合でも、「grok-code-session-traces」へのコードベースのアップロードは継続されます。オプトアウトしても、リポジトリがユーザーの端末から外部へ送信されるのを防ぐことはできません。
7. 私たちが証明しなかったこと(知的誠実さについて)
- xAI がこのデータを使ってモデルをトレーニングしていることを証明したわけではありません。アップロードや保存が行われたからといって、それが即座にトレーニングに使われるとは限りません。これはポリシーとアカウントのグレードによって決定される事項であり、私たちが測定したのはあくまでデータの転送のみです。
- 直接 storage.googleapis.com/grok-code-session-traces への PUT リクエストが 3 GB で観測された事実はありますが、今回のセッションではログを保存できませんでした(スキャン中にログが上書きされ、モデルのクォータ制限により再キャプチャが現在ブロックされているため)。したがって、§4 で主張した「数ギガバイト」という根拠は、保存された wire_12gb.log(/v1/storage での 200 レスポンス、75 MB チャンクごとの転送)とバケット名を示すバイナリ文字列に基づいています。クォータがリセットされ次第、直接 PUT のキャプチャを再現することは可能です。
保存されたのは、12 GB のスウィープログのみです。64 MB、600 MB、3 GB のデータはライブで観測されましたが、キャプチャログは保存されていません(クォータのリセット後に再現可能です)。なお、12 GB のキャプチャ自体は途中(約 5.1 GB / 73 チャンク捕捉時点)で停止しているため、「数 GB のアップロードなら失敗なく成功する」ことは証明されますが、「12 GB リポジトリ全体をエンドツーエンドで完走させること」までは証明されていません。
「文書化されていない」という主張は、範囲を限定したものです。これは xAI のドキュメントやヘルプセンター記事、ポリシーを網羅的に検索した結果ではなく、CLI のインストールスクリプトとクイックスタートガイドに基づいています。実際には、私が確認しなかった箇所に記述がある可能性も否定できません。より正確に言えば、「CLI 自体のセットアップ資料には明示されていない」という表現が妥当です。
チャンネル B(リポジトリ全体)については、現在クローズされています(これが唯一残されていた隙間でした)。証拠§4(d)によると、SuperGrok アカウント上で単一のラン実行時に、Grok が git bundle 形式で全リポジトリを POST /v1/storage を経由してアップロードする様子をワイヤーキャプチャした結果、HTTP ステータス 200 が返されました。保存された uploaded_repo.bundle を git clone で復元すると、エージェントに開くなと指示されていたファイルである src/_probe/never_read_canary.txt が、その固有のマーカーを文字通り含んだ状態で回復しました。さらに完全な git ヒストリも含まれています。つまり、この「未読」ファイルの内容はハッシュ値だけでなく、実体としてマシンから流出し、サーバー側で正常に受け入れられたことが証明されます。また、wire_12gb.log の実行記録は、同じメカニズムが GB 単位の大容量にもスケールすることを裏付けています。
- ユニバーサル型と条件付き型の違い(一部回答済み):無料枠でも数 GB のアップロードは成功します。また、SuperGrok で「モデルの改善」オプションをオフにした場合、git-bundle 形式のコンテンツアップロードも成功します(/v1/settings 応答で trace_upload_enabled: true が返されました)。これらのテストではアップロードを無効化する設定は見つかりませんでしたが、すべてのアカウントや構成の組み合わせを網羅的に検証したわけではないため、「絶対にゲートされない」とは断言できません。
- 以前の主張の一部を取り下げます。当初「数 GB の巨大なデータブロックは失敗し、ローカルで削除されるだけで外部へ漏洩しない」という結論を出しましたが、これは PID スコープの nettop 測定(
secrets: grep -a CANARY
staged: gzip -dc | tar -xO | grep CANARY
整合性: すべてのキャプチャは、私が自分のマシン上で生成した独自トラフィックのみを対象としています。また、「シークレット」として検出されたのは偽のキャナリー文字列であり、実際の認証情報は一切漏洩していません。今回の発見は grok 0.2.93(2026 年 7 月時点)に限定されたものです。xAI は今後、動作仕様を変更する可能性があります。
原文を表示
What xAI Grok Build CLI actually sends to xAI - a wire-level analysis (grok 0.2.93)
Update — 2026-07-14: This is the original wire-level analysis (grok 0.2.93). Since publishing: xAI disabled the upload server-side (disable_codebase_upload: true); added a /privacy opt-out — which I wire-tested and found to be a data-retention setting, not a block on what's sent; and Elon Musk publicly committed to deleting all previously-uploaded data (not yet confirmed complete). Full, maintained story + evidence: https://cereblab.com · https://github.com/cereblab/grok-build-exfil-repro
*By @cereblab — Independent AI Safety Checker. Reproduce it yourself: github.com/cereblab/grok-build-exfil-repro*
*A measured, reproducible teardown. Findings are backed by captured artifacts (endpoint, HTTP method, status code, byte size, host) and repro commands; where an observation was seen live but not retained as a file, §7 says so explicitly. Section 8 is an evidence appendix with SHA-256s and a "what we did not prove" list. All captures are of my own traffic on my own machine, using a throwaway repository containing fake "canary" secrets — no real credentials were exposed.*
0. Summary
xAI's official Grok Build coding CLI (grok), on a normal consumer login, does three things worth documenting precisely:
- It transmits the contents of files it reads — including a .env secrets file — to xAI, verbatim and unredacted. The secret appears in two channels: the live model turn (POST /v1/responses) and a session_state archive uploaded and accepted (HTTP 200) via POST /v1/storage — the endpoint the binary routes to the grok-code-session-traces GCS bucket (see §5).
- It uploads the whole repository — every tracked file's content plus git history — independent of what the agent reads. Grok packages the workspace and uploads it via POST /v1/storage. Proven directly: on a real codebase, with the prompt "reply OK, do not read any files", Grok uploaded the entire repo as a git bundle (POST /v1/storage → 200); git clone-ing the captured bundle recovers a file the agent was told not to open — src/_probe/never_read_canary.txt — with its unique marker verbatim, plus the full git history (appendix uploaded_repo.bundle). And it scales: on a 12 GB repo of never-read random files, /v1/storage moved 5.10 GiB, all HTTP 200 (truncated mid-stream), while the model-turn channel moved just 192 KB — a ~27,800× ratio that pins the upload to the codebase, not to what was read. No storage upload failed; the only non-200s were a model-usage quota (402/429) on /v1/responses and one unrelated 404 — not a storage size cap.
- The storage destination is a Google Cloud Storage bucket, grok-code-session-traces (not AWS S3) — named verbatim in the binary and in a captured metadata.json (gs://grok-code-session-traces/…). I did not find this mechanism surfaced in the CLI's install/quickstart materials (not an exhaustive docs audit — §7), it is active by default, and disabling "Improve the model" does not turn it off (/v1/settings still returned trace_upload_enabled: true; §6).
None of this proves xAI *trains* on the data — that is a policy question addressed in §6. What is proven is transmission, acceptance, and storage.
1. Subject under test (provenance)
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)
Binary identity (repro: 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
The upload machinery is a first-party Rust crate. strings on the binary yields these source paths and constants (repro: 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. Method (reproducible)
Environment: macOS, Apple Silicon, grok 0.2.93, July 2026.
- brew install mitmproxy; run once to generate its CA at ~/.mitmproxy/.
- Trust the CA in the login 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>
- For staged-artifact inspection, race-copy ~/.grok/upload_queue/* during the run, then gzip -dc | tar -xO.
Canary repo: each file carries a unique marker so anything appearing in captured traffic is unambiguously traceable to a file. Secrets file secrets.env / .env:
API_KEY=CANARY7F3A9-SECRET-should-not-leave
DB_PASSWORD=CANARY7F3A9-DBPASS
3. Finding 1 — File contents, including a secrets file, are transmitted and accepted (200)
Claim: when Grok reads a file, its contents are transmitted to xAI — serialized into the POST /v1/responses model-turn body, and packaged into a session_state archive that is uploaded and accepted (HTTP 200) via POST /v1/storage — with no redaction of the file's contents. A .env is sent like any other file.
Wire artifact — a decrypted 48,070-byte POST cli-chat-proxy.grok.com/v1/responses request body (identifiable as a model turn by its embedded "messages":[…]"model":"grok-4.5" JSON). It contains the secrets file verbatim (appendix: secrets_responses_body.bin, secret_verbatim.txt):
…API_KEY=CANARY7F3A9-SECRET-should-not-leave\nDB_PASSWORD=CANARY7F3A9-DBPASS\n…"model":"grok-4.5"…
Repro: grep -a "CANARY7F3A9-DBPASS" secrets_responses_body.bin → matches. All six file markers (source, logic, README, nested JS, API key, DB password) are recoverable from the decrypted /v1/responses bodies. (This artifact proves the secret was transmitted to the /v1/responses endpoint; the raw body file does not carry the response status, so the acceptance (200) claim is anchored to the /v1/storage channel immediately below, which is status-mapped in wire_12gb.log.)
Second channel — persisted to Google Cloud Storage. The same content is packaged into a session_state archive uploaded via POST /v1/storage. Proven by decompressing the staged artifact before it drains (appendix: 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
So the secret is not only processed in-flight; it is written into an archive destined for storage.
Pre-empting "you told it to read the secrets." A control run with a file the agent was told not to open (untouched_secret.txt) and the prompt *"Reply exactly OK, do not read any files"* produced no occurrence of that file's marker in any captured body. The leak is therefore scoped to files Grok does read — but it reads liberally (any file relevant to the task, including a .env) and applied no redaction to that file's contents. The defect is that a secrets file was transmitted unredacted, not the act of reading. Important scope reconciliation: this control shows the unread file is absent from the /v1/responses bodies — that is *Channel A* (files the agent reads). It does not clear the separate whole-repo /v1/storage snapshot in §4 (*Channel B*), which — by the volume evidence there — does sweep in never-read files; I could not decompress the /v1/storage codebase chunk to check this specific file. So "unread file not uploaded" is true only for the model-turn channel, not for the codebase snapshot. (Two further scope notes: (i) in my runs the .env/secrets.env was git-tracked; I did not separately test whether a .gitignored file is still uploaded, so I make no gitignore claim — the mechanism is read-driven per the file_access_tracker crate, but that specific case is untested. (ii) The canary values sat in API_KEY=/DB_PASSWORD= keys inside a .env/secrets.env but were not real-format high-entropy tokens; I proved *this .env was transmitted unredacted*, not that no redactor exists for, say, an sk-…-shaped key.)
4. Finding 2 — The whole repo is uploaded at multi-GB scale; the only ceiling is a model quota, not storage size
Claim: Grok uploads a whole-repo snapshot with no storage size wall in the tested range. As the repo grows it switches upload strategy and keeps returning 200; on a 12 GB repo, 73 chunks of ~75.0 MB (5.10 GiB) uploaded with zero failures before the capture was truncated mid-stream.
Wire-captured size sweep (incompressible content so the tar cannot shrink; fresh session each step). Only the 12 GB row was retained as a file (wire_12gb.log); the smaller rows were observed live during the sweep but their logs were not saved (see §7):
Repo size
Upload behavior (observed on the wire)
Status
Artifact
64 MB
single POST /v1/storage, req=50548145b (48 MB)
200
observed, not retained
~600 MB
POST /v1/storage in ~7.5 MB chunks (dozens)
all 200
observed, not retained
~3 GB
POST /v1/storage/multipart/init → PUT storage.googleapis.com/grok-code-session-traces/multipart/<id> in 50 MB parts (direct-GCS PUT lines not preserved — §7)
all 200
observed, not retained
~12 GB
POST /v1/storage in 75 MB chunks (req≈75014840b); 73 chunks (~5.1 GB) captured before I stopped the run
all 200, 0 failures
wire_12gb.log
Preserved artifact: wire_12gb.log (appendix). It contains 83 /v1/storage* 200 responses: 82 content-upload POST …/v1/storage requests — of which 73 are chunks of ~75.0 MB each (byte sizes min 75,014,811 / max 75,014,871, totaling 5,476,083,317 B = 5.10 GiB / 5.48 GB) plus 9 smaller POSTs — and 1 /v1/storage/batch_exists dedup check. Total /v1/storage* request bytes: 5,476,228,005 B. Zero storage requests failed. The capture was stopped while uploads were still streaming (the last line is another ~75 MB chunk → 200), so this demonstrates ≥5.1 GiB uploaded, still climbing when truncated — *not* that the full 12 GB completed. Repro (three greps, so no count is ambiguous): grep 'cli-chat-proxy.grok.com/v1/storage' wire_12gb.log | grep -c '> 200' → 83 (all /v1/storage*); grep 'POST cli-chat-proxy.grok.com/v1/storage ' wire_12gb.log | grep -c '> 200' → 82 (content POSTs only); grep req=75014 wire_12gb.log | grep -c '> 200' → 73 (the ~75 MB chunks).
Scope — this is the *whole repo*, not just files the agent read. Channel A (§3, /v1/responses) carries files the agent opens. This §4 upload is a separate Channel B: a snapshot of the entire workspace. Two lines of evidence:
- (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:
Channel A /v1/responses (model turns): 196,705 B = 192 KB total, across 5 requests, largest single turn 60,394 B.
- Channel B /v1/storage: 5,476,228,005 B = 5.10 GiB.
- That is a ~27,800× ratio (5,476,083,317 ÷ 196,705). The model demonstrably never ingested the files (192 KB cannot carry 5 GiB of content), yet 5.10 GiB of them left via /v1/storage — and across the sweep the /v1/storage volume tracks total repo size (64 MB → 12 GB). GB-scale bytes leaving a never-read repo can only be a whole-repo snapshot.
- (b) The binary's own paths/strings corroborate the mechanism: after_codebase.tar.gz, xai-grok-shell/src/upload/{trace,turn}.rs, repo_state.upload, "collecting workspace files", "spawning background coordinator".
- (c) A staged codebase manifest enumerates a never-read file and content-addresses it to the GCS bucket. In a separate run on a real 298-file cereblab_api codebase, I race-copied the snapshot Grok staged for upload (appendix: staged_base_tree_manifest.json, staged_metadata.json). The manifest lists src/_probe/never_read_canary.txt — a file I planted and never asked Grok to open — alongside the real sources (src/apns.ts, src/index.ts, …); and metadata.json gives each file a destination fileId of the form gs://grok-code-session-traces/repo_changes_dedup/v2/supplemental/sha256_…. So the whole tree, unread files included, is enumerated and content-addressed for upload to the named GCS bucket. (That run was quota-blocked before the content blobs uploaded; the 12 GB run above independently proves GB-scale content actually leaves the wire, all 200.)
- (d) DEFINITIVE — a never-read file's content recovered verbatim from an uploaded git bundle (single SuperGrok run). With the account upgraded (quota lifted), I re-ran on the real cereblab_api repo with the explicit prompt Reply with exactly: OK. Do not read or open any files., having planted a unique marker CANARY-XR47P2-NEVERREAD-UNIQUE in src/_probe/never_read_canary.txt. Grok uploaded the entire repository as a git bundle via POST cli-chat-proxy.grok.com/v1/storage → 200 (req=152102b) (appendix uploaded_repo.bundle, SHA-256 73b9c0af06311bae35c3ed03274d0eec2846e76762828d10b09757ca41bd6024). Running git clone uploaded_repo.bundle reconstructs the repo, and src/_probe/never_read_canary.txt contains the marker verbatim — a file the agent was explicitly told not to open. The bundle also carries the full git history (4 commits, 47 files). This is the airtight per-file-content proof: the whole repo — unread files included — left the machine and was accepted (200). The upload mechanism is a git bundle, so "whole repo" is literal (every tracked file + history). Replicated on a second, unrelated codebase: the identical capture on the cereblab_auth Cloudflare-Worker repo produced a git-bundle upload (POST /v1/storage → 200, 31,743 B) from which git clone recovered its own never-read marker CANARY-AUTH-4T8K2-NEVERREAD verbatim (appendix uploaded_repo_auth.bundle, SHA-256 0ee536538bcd1ee72a258f9977ab69f8a9b1ac240491b91a4e94335b4d83c768). Two independent repos, same result.
*(Prompt note: the 12 GB session was interactive and I did not log its verbatim prompt, but the 192 KB Channel-A total is dispositive that no bulk read occurred whatever the prompt was; a separate headless control run used the explicit prompt Reply exactly OK, do not read any files and confirmed an unread file is absent from Channel A.)*
*(The earlier "one gap" is now closed by evidence (d): a single SuperGrok run where a specific never-read file's content is recovered from a wire-captured, 200-status git-bundle upload. The 12 GB run remains the proof that this scales to GB volumes.)*
No storage/upload request failed — every one of the 82 /v1/storage calls returned 200. The only non-200s in the entire capture were on the model endpoint plus one session-bookkeeping call (full set from wire_12gb.log; /v1/responses lines also in 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)
and finally, in plain text, on stdout:
You've reached your free Grok Build usage limit for now. Get SuperGrok for much higher limits…
The 402/429 are a model-usage quota; the lone 404 is unrelated to storage. Notably, storage uploads continued to return 200 *after* the model turn was rate-limited (76 /v1/storage 200s occur at or after the first 429) — the codebase upload is independent of whether the model answers.
Pre-empting "you're confusing a local disk cache with an upload." This claim rests strictly on wire-captured 200-status uploads of file bytes leaving the machine (/v1/storage request bodies of 7.5–75 MB in the preserved wire_12gb.log; the 3 GB 50 MB PUTs to storage.googleapis.com were also seen on the wire, but that log was not retained — §7). It does not rely on the ~/.grok/upload_queue draining — queue-drain is ambiguous (it empties on both success and drop) and is explicitly not used as evidence here. (An earlier draft that inferred upload from queue-drain was wrong and has been retracted; see §7.)
5. Finding 3 — Destination, telemetry, and what's not surfaced in the docs
- Storage destination is Google Cloud Storage, bucket grok-code-session-traces. This rests on the preserved binary strings grok-code-session-traces, storage.googleapis.com, and "Uploading bytes to GCS via proxy" (crate_strings.txt), and on a preserved staged metadata.json whose per-file fileIds are literally gs://grok-code-session-traces/repo_changes_dedup/v2/…/sha256_… (staged_metadata.json), corroborated by the direct storage.googleapis.com multipart PUTs observed at 3 GB (observed live; that log was not retained — see §7). It is not AWS S3 (the binary links aws-sdk-s3 for an alternate path and AWS STS/SSO for auth, but the destination named in the binary — and seen on the wire at 3 GB — is GCS).
- Third-party telemetry: POST api.mixpanel.com/track and /engage (Mixpanel), plus POST grok.com/_data/v1/events — all 200.
- Not surfaced in setup docs (scope-limited claim): I did not find the repo_state / session_state upload to grok-code-session-traces, or the ~/.grok/upload_queue staging, described in the CLI's install script or quickstart materials I reviewed (this is not an exhaustive audit of all xAI docs — see §7). The mechanism is active by default on the standard consumer login.
- Reliability note (separate from privacy): ~/.grok/upload_queue stages ~3 GB snapshots per turn and, under load, can grow to tens of GB and exhaust the disk. This is a real bug, independent of whether uploads succeed.
6. Consent and policy — stated honestly
- "Cloud AI tools send context; this is normal." True, and conceded: any cloud coding agent must send code to its server to act on it. The novel deltas here are (a) a secrets file (e.g. .env) is transmitted unredacted, (b) the content is persisted to a named GCS bucket, not just processed transiently, and (c) the upload mechanism is not surfaced in the CLI's setup materials (§7) and on by default.
- "It's in the ToS / opt-in." xAI's consumer policy broadly discloses data use for model improvement with an opt-out (grok.com → Settings → Data → "Improve the model"; Private Chat auto-opts-out; opt-out is prospective, not retroactive). But broad training disclosure ≠ documenting this specific mechanism. I did not find the repo_state/upload_queue/grok-code-session-traces pipeline described in the CLI materials I reviewed (§7 notes this is not an exhaustive docs audit), so — on those materials — a user is not informed of it specifically. Sources: xAI Privacy Policy (https://x.ai/legal/privacy-policy), Consumer ToS (https://x.ai/legal/terms-of-service).
- The "Improve the model" toggle makes no difference — ON or OFF, the whole repo is uploaded the same way. At default settings, Grok uploaded 5.10 GiB of a never-read repo (§4). Then I turned "Improve the model" off and re-ran: Grok still uploaded the entire repo as a git bundle (§4(d), POST /v1/storage → 200, the never-read file recovered by git clone), and the server's /v1/settings response to the CLI still returned "trace_upload_enabled": true, "upload_enabled": true, "session_registry_enabled": true (and "max_upload_file_bytes": 1073741824, a 1 GiB per-file cap). The opt-out governs training, not whether your code is uploaded/stored: the codebase upload to grok-code-session-traces continues either way. Opting out does not stop your repository from leaving the machine.
7. What we did NOT prove (intellectual honesty)
- We did not prove xAI trains on this data. Upload/storage ≠ training. That is governed by policy and account tier; we measured transmission only.
- The direct storage.googleapis.com/grok-code-session-traces PUT wire-lines were observed at 3 GB but not preserved in this session (the log was overwritten during the sweep, and re-capture is currently blocked by the model quota). The multi-GB claim in §4 therefore rests on the preserved wire_12gb.log (/v1/storage 200s at 75 MB chunks) plus the binary strings naming the bucket; the direct-PUT capture is reproducible once quota resets.
- Only the 12 GB sweep log was retained. The 64 MB / 600 MB / 3 GB rows were observed live but their capture logs were not saved (reproducible once quota resets). The 12 GB capture itself was stopped mid-stream (~5.1 GB / 73 chunks captured), so it proves multi-GB upload succeeds with no failures, not that an entire 12 GB repo run completes end-to-end.
- The "not documented" claim is scope-limited. I based it on the CLI's install script and quickstart, not an exhaustive search of all xAI documentation, help-center articles, or policies. It is possible the mechanism is described somewhere I did not check; the defensible statement is "not surfaced in the CLI's own setup materials."
- Channel B (whole-repo) — now CLOSED (was the one open gap). Evidence §4(d): on a SuperGrok account, a single run wire-captured Grok uploading the entire repository as a git bundle via POST /v1/storage → 200; git clone of the preserved uploaded_repo.bundle recovers src/_probe/never_read_canary.txt — a file the agent was told not to open — with its unique marker verbatim, plus the full git history. So the never-read file's content (not just its hash) demonstrably left the machine and was accepted. The wire_12gb.log run remains the proof that the same mechanism scales to GB volumes.
- Universal vs. conditional (partially answered): multi-GB upload succeeds on free-tier; the git-bundle content upload succeeds on SuperGrok with "Improve the model" turned OFF (/v1/settings returned trace_upload_enabled: true). I did not find a setting that disables the upload in these tests, but I did not exhaustively enumerate every account/config permutation, so I don't claim it can never be gated.
- One earlier claim was retracted: an initial "the multi-GB blobs fail and are deleted locally, not exfiltrated" conclusion — based on a PID-scoped nettop reading (<1 MB) — was wrong. PID/host-scoped egress misses (a) a separate upload coordinator process and (b) presigned PUTs that go directly to Google IPs, never touching the API host. The wire capture (this document) supersedes that inference.
8. Evidence appendix
All artifacts and SHA-256s (MANIFEST.sha256). Binary SHA-256: 2a97ba675bd992aa9b981e2e83776460d94f469b510c0b8efe28b50d236d767c.
Artifact
What it proves
secrets_responses_body.bin (48 KB)
.env contents verbatim in a POST /v1/responses body
secret_verbatim.txt
the two .env lines as extracted
secrets_session_state.tar.gz (16 KB)
same secret inside the archive uploaded via /v1/storage
wire_12gb.log
83 /v1/storage* → 200 on a 12 GB repo = 82 content POSTs (73 chunks ~75.0 MB = 5.10 GiB + 9 small) + 1 batch_exists; Channel-A /v1/responses = 196,705 B (192 KB) total (~27,800× less); 0 storage failures; truncated mid-stream
model_limit.txt
the /v1/responses failures (402×1, 429×3 — model quota)
crate_strings.txt
xai-data-collector paths + grok-code-session-traces + storage.googleapis.com
binary.sha256
binary provenance
gcs_puts.txt
placeholder (empty capture) — the 3 GB direct-GCS PUT lines were not retained; note inside explains, re-capture pending quota (§7)
uploaded_repo.bundle (152 KB, SHA-256 73b9c0af…)
the smoking gun — a git bundle wire-captured leaving via POST /v1/storage → 200; git clone recovers src/_probe/never_read_canary.txt (never-read) verbatim + full git history (real cereblab_api repo, SuperGrok, "Improve the model" OFF)
uploaded_repo_auth.bundle (SHA-256 0ee53653…)
replication on a 2nd unrelated repo (cereblab_auth Worker) — git bundle via POST /v1/storage → 200; git clone recovers its never-read canary CANARY-AUTH-4T8K2-NEVERREAD verbatim
staged_base_tree_manifest.json
real-code run: the codebase snapshot manifest enumerating src/_probe/never_read_canary.txt (never-read) + 30 real src/*.ts files
staged_metadata.json
real-code run: per-file destinations fileId: gs://grok-code-session-traces/repo_changes_dedup/v2/…/sha256_… (names the GCS bucket)
Repro (condensed):
brew install mitmproxy && mitmdump -q -p 8080 # generates ~/.mitmproxy CA
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db ~/.mitmproxy/mitmproxy-ca-cert.pem
# capture a run:
HTTPS_PROXY=http://127.0.0.1:8080 SSL_CERT_FILE=~/.mitmproxy/mitmproxy-ca-cert.pem grok -p "read every file" --cwd <repo>
# secrets: grep -a CANARY <saved /v1/responses body>
# staged: gzip -dc <staged session_state> | tar -xO | grep CANARY
Integrity: all captures were of my own traffic on my own machine; the "secrets" were fake canary strings; no real credentials were exposed. Findings are version-specific to grok 0.2.93 (July 2026); xAI may change behavior at any time.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み