AI & ML

xAI’s Grok Build CLI Quietly Ships Your Whole Repo to a Google Bucket, and the Opt-Out Is a Lie

// 8 min read
Bala Kumar Senior Software Engineer

I read the receipts. So should you, before you type grok -p "fix the bug" into your terminal.

A researcher who goes by cereblab on GitHub spent the last week running xAI's freshly-released Grok Build coding CLI through mitmproxy, planting canary strings in fake secrets files, and counting the bytes that left the machine. The teardown, published as a single long-form gist, is the kind of write-up that gets quietly taken down and then emailed around for years. It is also the clearest case study yet of an AI dev tool shipping telemetry nobody consented to, in a way the vendor's own opt-out does not actually disable.

The headline finding is worse than "shell history gets uploaded." The whole repo does, including files the agent was explicitly told not to open.

The setup, in one paragraph

Grok Build installs via curl -fsSL https://x.ai/cli/install.sh | bash, lands in ~/.grok/bin/grok, and authenticates against an X / SuperGrok consumer login. The researcher routed it through a local mitmproxy, dropped canary markers into fake API_KEY and DB_PASSWORD lines in a .env, planted a never-read file in the repo with its own unique tag, and watched every byte hit the wire. He did this twice on two unrelated real codebases, and once on a 12 GB repo of never-read random files.

The capture is reproducible. The artifact hashes are in the source.

Finding 1: your .env goes out unredacted

The first thing he found is what you would expect, and what the docs deny. When Grok reads a file, the contents land in a POST cli-chat-proxy.grok.com/v1/responses model-turn body. No redaction. A .env is sent like any other file. That part, on its own, is not news - every cloud coding agent has to send the code it acts on.

What makes it news is channel two. The same .env content is also packaged into a session_state archive and uploaded to a Google Cloud Storage bucket called grok-code-session-traces. Both channels returned HTTP 200. The canary values for the fake API_KEY and DB_PASSWORD were recoverable from both, byte-for-byte. The researcher says this himself:

"I proved this .env was transmitted unredacted, not that no redactor exists for, say, an sk-…-shaped key."

That is the responsible framing. What is not responsible is xAI shipping a CLI with no in-line redaction of a .env at all, by default, on a consumer login.

Finding 2: the whole repo gets uploaded, not just what the agent read

This is the one that matters. The researcher ran Grok against a 12 GB repository full of files the agent never touched, with the explicit prompt "Reply exactly OK, do not read any files." Two channels moved wildly different amounts of data, in the same captured session:

ChannelBytes outWhat it carries
/v1/responses (model turns)196,705 B (192 KB)Files Grok actually read
/v1/storage (codebase snapshot)5,476,083,317 B (5.10 GiB)The whole workspace

That is a ~27,800x ratio. The model demonstrably did not ingest the files (192 KB cannot carry 5 GiB of content), yet 5.10 GiB of them left via /v1/storage. Across 73 chunks of ~75 MB each, every single upload returned HTTP 200, zero failures. The capture was stopped mid-stream while uploads were still climbing, so the lower bound is 5.1 GiB.

The bytes-per-repo-size scaling makes the claim airtight:

Repo sizeUpload behaviorHTTP status
64 MBsingle POST /v1/storage, 48 MB body200
~600 MBPOST /v1/storage in ~7.5 MB chunksall 200
~3 GBPOST /v1/storage/multipart/init then 50 MB PUT parts directly to storage.googleapis.comall 200
~12 GBPOST /v1/storage in 75 MB chunks (73 captured)all 200

GB-scale bytes leaving a never-read repo can only mean a whole-repo snapshot.

Finding 3: the smoking gun - git clone recovers a file the agent was told not to open

The researcher then upgraded the account to SuperGrok, lifted the model quota, and re-ran on a real 298-file codebase with the explicit prompt "Reply with exactly: OK. Do not read or open any files." A file he had planted, src/_probe/never_read_canary.txt, contained the unique marker CANARY-XR47P2-NEVERREAD-UNIQUE.

Grok uploaded the entire repository as a git bundle via POST /v1/storageHTTP 200. Running git clone uploaded_repo.bundle recovered the repo, and src/_probe/never_read_canary.txt was there with its marker verbatim, along with the full git history.

He replicated it on a second, unrelated codebase (cereblab_auth, a Cloudflare Worker) and recovered its own never-read marker CANARY-AUTH-4T8K2-NEVERREAD the same way. Two independent repos, same result. SHA-256 hashes for both bundles (73b9c0af… and 0ee53653…) are in the appendix.

That is the airtight per-file-content proof. It is not a hash match. It is a git clone of a never-read file's bytes, recovered from the wire capture.

The opt-out is theatre

The thing that made me put my coffee down is finding 6. The researcher ran the same capture a third time, with xAI's "Improve the model" toggle turned off, and watched the same upload happen anyway.

/v1/settings → {"trace_upload_enabled": true,
                "upload_enabled": true,
                "session_registry_enabled": true,
                "max_upload_file_bytes": 1073741824}

The whole-repo git-bundle upload completed with POST /v1/storage → 200. The never-read file was recovered from the wire capture with the opt-out on. The toggle governs training, not whether your code is uploaded and stored. From the teardown:

"Opting out does not stop your repository from leaving the machine."

The mixpanel and grok.com/_data/v1/events telemetry also still fired. The /v1/storage traffic 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.

Where the bytes land, exactly

The destination is named in the binary itself and in a staged metadata.json the researcher preserved:

grok-code-session-traces
storage.googleapis.com
"Uploading bytes to GCS via proxy"
{
  "fileId": "gs://grok-code-session-traces/repo_changes_dedup/v2/supplemental/sha256_…"
}

That is a Google Cloud Storage bucket, not AWS S3, despite aws-sdk-s3 being linked in the binary for an alternate path. It is owned by xAI, not a third-party subprocess. The ~/.grok/upload_queue directory stages ~3 GB snapshots per turn and, under load, can grow to tens of GB and exhaust the disk - which is, incidentally, a real reliability bug independent of whether uploads succeed.

The repro, condensed

If you want to verify any of this yourself (and you should, before trusting me or the researcher):

brew install mitmproxy && mitmdump -q -p 8080
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db \
  ~/.mitmproxy/mitmproxy-ca-cert.pem

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 archive:
gzip -dc <staged session_state> | tar -xO | grep CANARY

The researcher notes that Grok does not certificate-pin against the mitmproxy CA on a consumer login, which is itself a finding.

What the docs say vs. what the wire says

xAI's consumer policy broadly discloses data use for model improvement with an opt-out. That is the legally necessary boilerplate. The teardown's read is fair:

"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's install script or quickstart materials I reviewed."

So the pipeline is:

  • active by default,
  • not described in the setup materials,
  • not disabled by the only opt-out the user can find,
  • and operates at multi-GB scale.

What I would do today, in priority order

  1. Do not run grok inside any repo you would not be comfortable giving a stranger. Treat the consumer CLI like an untrusted binary until xAI ships a real per-channel opt-out and documents the bucket by name.
  2. If you have run it, assume every tracked file in that directory left your machine. Rotate any secrets that were ever committed. The canary string is not a redactor, and git history is in the bundle.
  3. If you are an enterprise buyer: the "Improve the model" setting is not a data-residency control. Ask xAI, in writing, for the trace_upload_enabled flag's behavior on your tier, and for a setting that actually returns false on /v1/settings.
  4. Pin your trust boundary at the network layer. Run Grok in a sandbox where the only egress is the model endpoint you expect, and watch for cli-chat-proxy.grok.com/v1/storage and storage.googleapis.com/grok-code-session-traces on the wire.

The receipt that should not be necessary

The 12 GB capture alone is enough to show that this scales. The git-bundle capture alone is enough to show that never-read files leave verbatim. The opt-out capture alone is enough to show the toggle does not work. All three together are the wire-level proof that the only sane default for this CLI, today, is: do not point it at anything you would not hand over.

xAI will probably fix this within a week, or change the bucket name and call it a different product. Either way, the receipts are SHA-pinned, the repro is on a laptop you can run yourself, and the answer to "should I trust Grok Build with my codebase right now" is no, and the answer to "did the opt-out actually opt me out" is also no.

That is two no's from one binary. Both of them earned.

Source: Wire-level analysis of xAI's Grok Build CLI by cereblab, July 2026. All findings, numbers, and SHA-256s in this post are from that document; I added the framing, not the evidence.