← All posts
By Aseem Shrey

Your coding agent is uploading your repo. I checked.

Grok Build CLI uploads your repo and .env by default. I read the binary, caught it on the wire, and blocked the leak without breaking the tool.

A coding agent sends two requests to the same host. agentjail lets the POST /v1/responses model call through with a 200 and blocks the POST /v1/traces telemetry upload with a 403.
Same host, split by endpoint: the model API is allowed, the trace upload is denied.

A teardown of xAI’s Grok Build CLI made the rounds. The claim: it ships your files, .env included, off to xAI by default. Whole repo too. Even with the “improve the model” setting turned off.

My first reaction was “no way, that can’t be right.” My second reaction was the usual one. Let me check.

So I installed the exact version, read the binary, and then pointed my own tool at it to watch every packet it sends. This is what I found. Some of it lines up with the original teardown. Some of it doesn’t, and I’ll be honest about which.

Cereblab's tweet: Grok's CLI uploads your whole repo to xAI, and the opt-out doesn't stop it
cereblab caught this first (the original gist). I just took it apart a bit further, then built a way to actually block it.

tl;dr

  • Grok Build CLI (0.2.93) uploads session traces by default. The grok trace help literally has a --local flag described as “skip remote upload.” Read that again.
  • I watched a real session on the wire: 175 requests in one turn. Your .env secrets ride out inside the model request body (180 KB POSTs). Plus 12 trace uploads and 143 analytics events. In one turn.
  • The gist said the exfil goes to /v1/storage on api.x.ai and a GCS bucket. In 0.2.93 I saw something different, so I’m correcting that below.
  • Grok redacts secrets. But it’s a denylist of key names (apikey, accesstoken, …). Your STRIPE_SECRET_KEY and DATABASE_URL don’t match those names, so they go up in the clear.
  • You can’t just “block the network.” The agent needs its model API to work. The interesting part is blocking the exfil without breaking the tool. I did that. Two caveats at the end about how far along it is.
  • There’s a full screen recording of the run further down, if you’d rather watch it block the leak than read me describe it.

First, just read the binary

No proxy yet. I just installed it (curl -fsSL https://x.ai/cli/install.sh | bash -s 0.2.93) and ran strings and --help on it. The binary tells on itself.

$ grok trace --help
Export or upload session trace data
Options:
      --local    Save locally only, skip remote upload

--local skips the remote upload. Which means the default is: upload. And the destination is hardcoded:

GROK_TELEMETRY_GCS_BUCKET
gs://grok-code-session-traces

A Google Cloud Storage bucket called grok-code-session-traces. Cute name.

Then I went digging through the upload module strings (crates/codegen/xai-grok-shell/src/upload/trace.rs). Here’s what it prepares to send, per session:

  • after_codebase.tar.gz : a tarball of your repo. The string right next to it says “Codebase upload completed successfully.”
  • turn_messages.json : every model turn, verbatim.
  • config_files.json : your tool config. And not just Grok’s own. The keys include claude_settings and claude_managed_settings. It uploads your Claude Code config too.
  • memory.tar.gz, session state, prompt images, permission decisions.

Okay so on paper it’s grabbing a lot. But strings in a binary are just intent. I wanted to see it actually happen.

The redaction thing (this is the sneaky part)

Grok does redact. There’s a [REDACTED_SECRET] path with a list of key names it scrubs:

apikey passwd pwd credential deploymentkey accesskeyid accesskey
authtoken accesstoken refreshtoken sessiontoken bearertoken mixpaneltoken

Look at that list carefully. It’s substring matching on names it thought of. Your .env has STRIPE_SECRET_KEY=sk_live_... and DATABASE_URL=postgres://admin:hunter2@.... Neither matches any of those names. So they don’t get redacted. They just leave.

Redaction that only covers the secrets the vendor imagined is not really redaction. It’s a vibe.

Now watch it on the wire

Here’s where I plugged in agentjail, a thing I’ve been building to put guardrails around coding agents. It can run any agent inside a transparent tunnel, terminate TLS with a CA it drops into the agent’s own trust store, and log every request. So I made a throwaway taskflow-api repo with a canary secret in .env, and ran:

agentjail run --tunnel -- grok --always-approve -p "list every env var and secret this project uses"

One turn. 175 requests hit the wire. Here’s the breakdown:

HostRequestsWhat it is
cli-chat-proxy.grok.com30model turns + trace uploads
grok.com143analytics events
mcp.posthog.com2an MCP server I had configured

Three things jumped out.

Your secrets leave in the model context. The /v1/responses POST bodies grew to 180 KB as Grok read the project and the .env. Its own reply listed JWT_SIGNING_SECRET, STRIPE_SECRET_KEY, SENDGRID_API_KEY, DATABASE_URL. So yes, it read .env, and that content went out in the request body.

The gist was close but not exact for 0.2.93. The original said /v1/storage on api.x.ai. I saw zero hits to /v1/storage. The model traffic actually goes through cli-chat-proxy.grok.com, and the trace telemetry goes to cli-chat-proxy.grok.com/v1/traces (12 uploads). The grok-code-session-traces GCS bucket is real (it’s in the binary), but it did not fire in my session. Maybe it’s gated by repo size, or an account setting. I’m only reporting what I actually captured. Not what I expected to capture.

It’s chatty as hell. grok.com/_data/v1/events took 143 analytics events in one short turn.

The hard part: you can’t just pull the plug

The obvious reaction is “sandbox it, block the network, done.” That breaks the tool. Grok needs cli-chat-proxy.grok.com to answer a single prompt. Block that host and you’ve got a very expensive cat.

And here’s the annoying bit: the model API and the trace telemetry are on the same host. Same cli-chat-proxy.grok.com. A normal firewall or host allowlist can’t tell them apart. Allow the host and the traces leak too. Deny it and Grok dies.

The only way to split them is to actually look inside the TLS and reason about the request path. Which is exactly what the tunnel does.

How to protect your coding agents

Okay so here’s the mental model, and it applies to any agent, not just Grok.

You have two leaks to worry about:

  1. What the agent can reach. It shouldn’t be able to read your ~/.ssh, your other repos, your ~/.aws. That’s the OS sandbox’s job (Landlock on Linux, Seatbelt on Mac).
  2. What the agent sends out. It can read your project (that’s its job), but it shouldn’t be able to ship your project’s secrets to a telemetry endpoint. That’s the network layer’s job.

I ran Grok through both at once. A policy that allows the model turn and denies the telemetry:

# allow the model API
id: grok-model-allow
match: { host: ["cli-chat-proxy.grok.com"], path: ["re:^/v1/responses"] }
action: allow
---
# block the trace upload on the SAME host
id: grok-traces-deny
match: { host: ["cli-chat-proxy.grok.com"], path: ["re:^/v1/traces"] }
action: deny
---
# block the analytics firehose
id: grok-analytics-deny
match: { host: ["grok.com"], path: ["re:^/_data/v1/events"] }
action: deny

(There’s more in the full pack: the GCS bucket, Mixpanel, and a rule that drops any request whose body carries a planted .env canary, so you catch the secret leaving without ever storing it.)

What happened when I ran it:

EndpointVerdictHTTP
/v1/responses (model)allow200
/v1/traces (telemetry)deny403
grok.com/_data/v1/events (analytics)deny403

Grok still worked. It answered my question about the billing route just fine. But 107 telemetry and analytics requests got a 403. Same host, kept the tool, dropped the surveillance.

I recorded the whole thing. Neofetch for the vibe, the .env sitting there full of secrets, then Grok running inside the tunnel while agentjail sorts its traffic in real time and finally refuses to read a secret file outside the project:

A fresh run, start to finish. Grok answers over the allowed model API (200) while every trace and analytics upload gets a 403, then the out-of-project secret read is denied. The repetitive analytics denials are folded up in the live log and tallied in the wire table.

And on the filesystem side, in the same run I asked it to read a secret file outside the project. Its own words:

Could not read company-vault/production-secrets.txt. Permission denied for cat, Python, and the file read tool, even though the process reports uid=0.

It tried four ways, as root inside its namespace, and got denied every time. It can read its own project. It cannot wander into your ~/.ssh.

Two caveats

The host-level blocking ships in agentjail today. The per-endpoint split and the secret-in-body catch need the transparent tunnel, which is still on a branch and experimental. So take this as “here’s what I built and what it caught,” not “brew install and you’re safe.”

And the tunnel reads /v1/traces from /v1/responses by terminating TLS with its own CA inside the agent’s sandbox (key in memory, never on disk). You’re decrypting your agent’s traffic instead of trusting the vendor’s word about it.

Things I took away from this

  • Your coding agent is a vendor binary phoning home on a channel you have to keep open. The vendor’s “we only use it to improve the model” toggle is not your guardrail. It’s their setting, and it defaults to send-everything.
  • The exfil isn’t one endpoint you can block. Model API, traces, and analytics are tangled across shared and separate hosts. You need to look at the request, not just the domain.
  • Name-based secret redaction misses anything the vendor didn’t name. Which is most of your secrets.
  • Own the boundary the agent talks through. Bring your own guardrails.

Thanks for reading :)

Original analysis that kicked this off: cereblab. My poking around and the defense: agentjail, against grok 0.2.93.