· Greywatch
What the Grok CLI Actually Uploads
A teardown of grok 0.2.99: when it uploads your files and what you can do about it.
You've probably read the recent drama circulating on X and Hacker News about the new Grok Build CLI uploading users' entire folders or repositories to Google Cloud Storage, including untracked files and secrets.
We wanted to know exactly what the collection looks like and when it triggers. So we tore it down: string analysis of the grok 0.2.99 binary on macOS, the bundled docs, and the upload decisions the CLI logs to ~/.grok/logs/unified.jsonl.
Inside the binary is a subsystem the code calls prompt tracing. When it's on, every prompt turn ships an upload to the GCS bucket grok-code-session-traces, via signed URLs from code.grok.com. Each turn includes:
- A snapshot of your repo before and after the turn
- The full conversation content of the turn
- Your effective settings, including
~/.grok/config.toml,.claude/settings.json, and resolved permission rules - Session state, memory archives, the unified log, and any images you pasted
The repo archive is git-aware. Commits already on your upstream are referenced by hash. What it actually uploads is your local commits, your staged and unstaged patches, and your untracked files.
Some things get filtered: whatever is in .gitignore, a hardcoded list of build directories (node_modules, target, .venv, and friends), and a hardcoded list of filename patterns baked into the binary. Here is what we were able see in it's exclusion list:
# build and editor noise
*.egg-info *.pyc *.pyo *.o *.dylib *.class *.jar
.DS_Store Thumbs.db *.swp *.swo *~ *.iml
# the "secret" patterns
.env.** *.pem *.p12 *.pfx *.keystore *.tfstate
id_rsa* id_ecdsa* id_ed25519* id_dsa*
.npmrc .netrc .htpasswd service-account*.json
While this covers many common files, a static list is not a comprehensive method of avoiding accidental sensitive uploads. In our initial inspection we did not see any local content analysis in the path so filename seems like the only metric used to filter individual files.
Grok also does not filter based solely on git repo to enable collection. The collector tries three ways to find out what to package and upload: first it walks up from your working directory to a .git root. If that fails it infers a root from the files the agent has touched that session. Finally, if there is no root determined by the current touched files, the harness falls back to walking the working directory. If Grok is launched from a hard-coded list of well-known directories, such as Desktop or Documents, the harness will try to guide you out of that folder, but a user can decline this and still end up unintentionally uploading personal files.
The harness also has a method intended to avoid uploading private repositories, however we discovered some concerning lapses in its detection method. Before the harness uploads a project target, the collector classifies it as public, private, or unknown. It does this by taking your origin remote and asking GitHub, at https://api.github.com/repos/{owner}/{repo}, whether the repo is private. Then it decides:
publicgets uploaded.unknowngets uploaded.privategets skipped, unless your account is "eligible" (an enterprise agreement), in which case it uploads too.
The fail open nature of unknown means that any project that is not tracked by git, or is on a non-GitHub host (such as Gitea, GitLab, or Bitbucket), may be classified as uploadable.
Mitigation
At the time of writing, it looks like Grok has this turned off globally. Every session on our machines logged uploads_enabled: false with upload_reason: "feature_off", and the decision came from the server, even when we had "Improve the Model" enabled in our Grok account. Given the viral thread, our guess is xAI flipped the kill switch after the reports landed.
You can disable this locally with the following grok config:
# ~/.grok/config.toml
[telemetry]
trace_upload = falseYou can confirm the current decision on your own machine:
grep trace.upload.decision ~/.grok/logs/unified.jsonl | tail -1That fixes Grok, today, for this feature. It doesn't fix the pattern.
Agent harnesses are network-connected programs with full access to your working tree. What they phone home is a vendor decision, and you shouldn't need a binary teardown to find out what that decision was. We think the only durable fix lives on your side of the wire: assume any harness will eventually phone home, and watch it yourself.
This is what we built watch mode for:
greywatch -- grokNothing is blocked and nothing breaks, but every file read, every command, and every network connection shows up on a live dashboard for you to review.
Once you know what a tool needs, flip from watching to enforcing. greywall is the same binary in deny-by-default mode. It ensures that only your working directory is accessible to the passed in harness and network requests are blocked unless you allow them.
Or stay permissive and carve out what the harness should never call out to:
{
"network": {
"rules": [{ "destination": "storage.googleapis.com", "action": "deny" }]
}
}