Skip to content

The prompt said "do not open any files." The whole repo left anyway.

A researcher told xAI's coding agent to open nothing, and watched it ship the entire repo - git history, secrets and all - to a company bucket. Three rival agents, same test, came back clean. Then xAI shipped a fix that makes a promise the wire still doesn't keep.

The prompt said "do not open any files." The whole repo left anyway.

A coding agent's job is to touch your code. That is the deal, and it is a fair one. What Rex Factor looks at this week is narrower and sharper: what a coding agent sends home when you have explicitly told it to touch nothing at all. One researcher pointed a proxy at xAI's Grok Build, typed "do not open any files," and watched the entire repository leave anyway - git history, secrets, the lot. Then he ran the same test on three rival agents, and they came back clean. The receipts are all here, hashes included. So is the fix xAI shipped, and the reason it solves less than it looks. - Muximus


Start with the outcome, because the outcome is the whole story.

A researcher typed a nine-word instruction into xAI's official coding agent: "Reply with exactly: OK. Do not read or open any files." Grok Build replied OK. Then it packaged the entire repository - every tracked file, the complete git history - into a git bundle and shipped it to a Google Cloud Storage bucket owned by xAI. The upload returned HTTP 200. Accepted.

Here is how we know it was the whole repo and not just the files the agent looked at. The researcher had planted one: src/_probe/never_read_canary.txt, carrying a unique marker, in a file he told the agent not to open. He captured the upload on the wire, saved the bundle, and ran git clone on it. The canary came back verbatim. So did four commits of history.

"Improve the model" was switched off at the time.

That is the receipt. The rest of this piece is the work behind it - and it is worth walking, because the work is what separates this from the dozen breathless versions of the same headline you have already scrolled past.

By the numbers

The work was published as a public gist by a researcher writing as @cereblab, with a reproduction repository, a SHA-256 manifest of every captured artifact, and a section titled "What we did NOT prove." The method is boring in the way good method is boring: install the tool, route it through mitmproxy, log every request with its method, host, path, response status and byte size, and keep the bodies.

The subject was grok 0.2.93, macOS arm64, binary SHA-256 2a97ba67…d767c, installed the way anyone installs it - curl -fsSL https://x.ai/cli/install.sh | bash - on an ordinary consumer login, not an API key. The shipping product, on the default path. Not a lab build behind a flag.

One session, a 12 GB repository of files the agent never read:

ChannelWhat it isBytes moved
POST /v1/responsesthe model turn - what the agent actually ingests196,705 B (192 KB, 5 requests)
POST /v1/storagethe upload5,476,083,317 B (5.10 GiB, 73 chunks of ~75 MB)

Roughly 27,800 to 1. That ratio is the entire argument, so sit with it. 192 kilobytes cannot carry five gigabytes of source code. Whatever left through /v1/storage was not context the model needed to answer - the model got 192 KB. It was the codebase. And across a size sweep from 64 MB up to 12 GB, the volume on that channel tracked the size of the repository. That is what a snapshot does. It is not what a context window does.

Eighty-three storage requests came back 200. Zero failed. The only errors in the whole capture were a 402 and three 429s on the model endpoint - a usage quota - plus one stray 404. The detail that should stop you: the uploads kept succeeding after the model was rate-limited. The agent had stopped answering questions. The repository was still going out the door.

Now the caveat, stated as plainly as the finding, because a number without its limit is a claim and not a fact. The capture was stopped mid-stream; the last line in the log is one more 75 MB chunk returning 200. So what is proven is that multi-gigabyte upload succeeds with zero failures and was still climbing when the recording stopped - not that a full 12 GB repository completes end to end. The researcher says exactly this, in those words. Borrowing his discipline is the least we can do.

The 12 GB run was on the free tier. The git-bundle capture up top - the one where the clone recovered a file the agent was told to leave alone - was a paid SuperGrok account with the privacy toggle off. The behavior did not care which.

The .env file

A secrets file carrying two canary values - API_KEY=CANARY7F3A9-SECRET-should-not-leave and DB_PASSWORD=CANARY7F3A9-DBPASS - went across the wire verbatim and unredacted. It shows up in a decrypted 48,070-byte model-turn body. It shows up again inside a session_state archive uploaded to storage - meaning it was not merely handled in flight, it was written into an artifact bound for a bucket.

The scope, stated precisely, because precision is the point: what was proven is that this .env went up unredacted. The canaries were not real-format, high-entropy tokens, so the researcher does not claim no redactor exists for something shaped like an sk-… key. He also did not test whether a .gitignored file gets swept in, so he makes no claim there either. Two honest gaps, disclosed by the person who would have looked cleaner leaving them out. Remember that habit; it comes back.

Where it went, and the switch that didn't stop it

The destination is named in the binary itself. Strings pulled from the executable include grok-code-session-traces, storage.googleapis.com, and the log line "Uploading bytes to GCS via proxy" - sitting alongside the source paths of a first-party Rust crate called xai-data-collector, with modules named gcs.rs, storage_client.rs, queue.rs, and file_access_tracker.rs. A captured staging manifest hands each file a destination of the form gs://grok-code-session-traces/repo_changes_dedup/v2/…/sha256_….

This is not a stray telemetry accident. It is a component, with a name, that someone designed, built, and shipped.

And now the part that should rearrange how an operator reads a vendor's settings screen. Grok Build offers an "Improve the model" toggle. Any reasonable buyer reads that as the data-collection control - the box you tick to keep your code out of the vendor's hands. Turn it off, and the server still answers the client with:

trace_upload_enabled: true
upload_enabled: true
session_registry_enabled: true
max_upload_file_bytes: 1073741824

And the repository still uploads.

The setting governs training consent. It does not govern transmission. Two different things wearing one label - and the upload mechanism was not surfaced in the CLI's own install script or quickstart, which is the narrow, checkable version of "nobody was told."

There is a plain operational bug sitting next to the privacy one, and it earns a line on its own merits. The local staging directory, ~/.grok/upload_queue, stages snapshots of around 3 GB per turn, and under load it can grow to tens of gigabytes and exhaust the disk. A cost with no upside to the person paying it.

What went right - three times, on the neighbors

The researcher re-ran the git-bundle capture on a second, unrelated codebase, a Cloudflare Worker project. Same result: a bundle on the wire, a 200 from the server, its own never-read canary recovered from the clone. Self-replication. Rules out a one-repo fluke.

The stronger corroboration came from someone else entirely. A second practitioner, @wetlink, reproduced the finding on a different machine against an identically-hashed binary, and their local logs showed 339 repo_state.upload.enqueued events across four repositories - including one session where the repo_path was the user's entire home directory. They then did the genuinely useful thing and tested which kill-switches actually work, and published the answer: precedence runs env > config > remote, and [harness] disable_codebase_upload = true is a hard veto on the whole-repo channel that held even with uploads force-enabled through the environment. When their first write-up got a detail wrong - they had called some TOML keys inert, when their own test environment had variables overriding config - they posted the correction in public, above the original.

Here is what keeps this from being a story about xAI. The same researcher ran the same test on the competition: identical canary repos, same proxy, same two prompts, against Claude Code (2.1.204), Codex (on GPT-5.5), and Gemini (0.38.2). None of the three uploaded the repository. Claude Code sent only the model turn to api.anthropic.com, read files on demand as visible tool-calls, and on the real task summarized the project without ever reading the never-read canary - the marker never left the machine. Codex sent its model turn over a WebSocket and nothing more. Grok was the outlier. One agent of four shipped the whole tree.

That comparison is what turns one vendor's default into a finding worth your time. The takeaway is not "cloud coding agents send your code" - three of the four demonstrably did not. It is that one of them did, by default, through a channel that had nothing to do with the task in front of it. (Two caveats the researcher attaches, and so do we: Gemini was tested in API-key mode and its real-task run hit a rate limit before finishing, so only its idle capture is clean; and every run is one machine, one account per tool.)

The fix, and the same mistake inside it

xAI did respond - fast enough that an earlier draft of this column claiming otherwise was out of date within a day. The company now defaults the codebase upload off, and Grok Build has gained a /privacy opt-out command. Both are real. The second is a genuine improvement on the original state, whose entire problem was that no opt-out was surfaced at all. Credit where it is due.

Then the researcher pointed the proxy at the fix. On grok 0.2.99 he ran the same task twice - opted in, then opted out - and put the two captures side by side.

Opted inOpted out
POST /v1/traces server response200 (stored)204 (discarded)
trace bytes sent by the client18,14518,664
model turn (/v1/responses, incl. contents of files read)~145 KB~145 KB
total bytes to xAI185,421185,578

Read the columns. Opting out does not send less. It sends slightly more. The session traces - including the verbatim contents of every file the agent reads - are POSTed to xAI in full either way. The one thing that changes is the server's answer: 200 when it stores them, 204 when it discards them.

That is a retention promise, not a transmission control. You still hand over everything. You are trusting the server on the far end to throw the opted-out copy away, and the only evidence you get that it did is a status code you would have to be running a proxy to see. The whole-repo upload machinery - repo_state.archive.build, the GCS multipart client, the grok-code-session-traces bucket - is still sitting in the 0.2.99 binary. Switched off by a server flag, not removed from the code.

So the fix reproduces the original error one floor up. "Improve the model" confused a training setting with a transmission control. /privacy opt-out confuses a retention setting with a transmission control. Same category mistake, fresh label. A promise you cannot observe is still just a promise.

One thing this column will not claim: that the exposé forced the change. The defensible statement is the timeline. The upload was captured while the report was being written; after publication, the server returned it disabled. "The behavior changed after we published," not "we made them change it." The difference matters, and Rex keeps to the side of it he can prove.

What nobody proved

The gist carries a section most write-ups of this kind never bother with, and it is the reason the whole thing holds up. In order: upload and storage do not prove xAI trained on the code - that is a policy question, and it was not measured. Only the 12 GB sweep log was retained; the smaller runs were seen live but their logs were not saved. The "not documented" claim covers the CLI's own install script and quickstart, not an audit of every xAI document. And an earlier conclusion of his - that the multi-gigabyte blobs failed and were deleted locally rather than exfiltrated - was wrong, and is publicly retracted; it had rested on a PID-scoped nettop reading that missed a separate upload coordinator process and presigned uploads going straight to Google's IPs. The wire capture supersedes it.

Two independent people investigated this. Both published their evidence with hashes. Both corrected themselves, in the open, when they got something wrong. That is what a finding you can trust looks like. Not the absence of errors - the errors caught and posted where anyone can see them. It is also, for the record, more than the vendor did.

What went wrong, and what it costs you

Strip the vendor's name off and this is a procurement finding, not a security thriller.

If a team pointed this CLI at a private codebase - a real one, with customer logic in it and a .env beside it - then a third party now holds an undisclosed copy of that source, its full history, and its secrets. Not "may have logged some telemetry." Holds a git bundle. The one question a CTO gets asked in that room is what did it send, and when - and the honest answer, for almost everyone, is that nobody at the company knows, because nobody was watching the wire. One researcher was. That is the only reason any of this is on the record. Whether it trips a disclosure obligation to your own customers is a question for counsel, and I am not counsel. But somebody in the building should be asking it this week.

Which points at the thing actually worth taking away, and it is not "be careful with AI."

A settings screen is not evidence. The toggle said one thing and the traffic said another; and when the vendor shipped a fix, the new toggle made a new promise the traffic still did not keep. Every control you cannot observe is a control you are accepting on faith - and this industry has been accepting a great many of them on faith, because checking looked like a research program. It is not. It is an afternoon with a free proxy. The instruction "do not read or open any files" was obeyed in the narrow sense, and the repository left anyway, through a channel with nothing to do with the agent's reasoning. That is the cleanest demonstration you will get that a rule written into a prompt cannot govern a component running outside it. What you cannot see, you cannot govern. So look: put a proxy in front of the agents you have already deployed, and find out what they send when nobody is asking them to send anything.

The reason this story exists at all is that a few people distrusted a settings screen enough to do exactly that - and then published everything, including the parts that made them look wrong, and including the three competing tools that came back clean.

Run it on your own stack before someone runs it on you.

Add VarOps on Google