Il CLI ufficiale di xAI per Grok Build ha un problema. Anzi, tre. E questa volta non è teoria: è traffico di rete catturato, byte per byte, con SHA-256 e comandi per replicare tutto.
Questa è un’analisi tecnica fatta da @cereblab — checker AI Safety indipendente — su cosa trasmette esattamente il comando grok quando lo usi per scrivere codice. Tutti i test sono fatti su traffico personale, macchina personale, con repository fake contenenti ‘canary secrets’ — nessuna credenziale vera esposta. Ma i risultati sono… diciamo che vale la pena leggerli.
Cosa succede quando lanci grok nel tuo progetto
Tre cose, provate con traffico di rete catturato e file SHA-256 verificabili:
Uno — il CLI trasmette il contenuto dei file che legge, incluso il tuo .env con i secret, verso xAI. Verbatim. Senza redazione. Il secret compare in due canali: nella chiamata live al modello (POST /v1/responses) e in un archivio session_state caricato via POST /v1/storage — l’endpoint che punta al bucket GCS grok-code-session-traces. HTTP 200, accettato.
Due — carica l’intero repository. Ogni file tracciato, più tutta la storia git. Indipendentemente da cosa l’agente legge davvero. Grok impacchetta il workspace e lo carica via /v1/storage. Provato in modo definitivo: su una codebase reale, con il prompt esplicito ‘reply OK, do not read any files’, Grok ha caricato tutto il repo come git bundle (POST /v1/storage → 200). Clonando il bundle catturato si recupera un file che l’agente non doveva aprire — src/_probe/never_read_canary.txt — con il suo marker unico verbatim, più l’intera storia git. E scala: su un repo da 12 GB di file mai letti, /v1/storage ha mosso 5,10 GB (tutti HTTP 200, troncato a metà), mentre il canale del modello ha mosso solo 192 KB. Un rapporto di ~27.800×. L’upload è palesemente legato alla codebase, non a ciò che viene letto.
Tre — la destinazione è un bucket Google Cloud Storage chiamato letteralmente grok-code-session-traces. Non AWS S3. Il nome è hard-coded nel binario e compare in un metadata.json catturato (gs://grok-code-session-traces/...). Questo meccanismo non è descritto nei materiali di install/quickstart del CLI (non è un audit esaustivo della docs, ma nei materiali base non c’è), è attivo di default, e — dettaglio ghiotto — disabilitare ‘Improve the model’ nelle impostazioni non lo spegne. Endpoint /v1/settings restituisce ancora trace_upload_enabled: true.
Cosa non prova (onestà intellettuale)
Questo non prova che xAI allena sui dati. Quella è una questione di policy, non di wire. Quello che è provato è: trasmissione, accettazione, storage. Il dato lascia la tua macchina, arriva al server, viene accettato con 200, e finisce in un bucket GCS nominato esplicitamente.
Il test: come replicarlo
Ambiente: macOS Apple Silicon, grok 0.2.93, luglio 2026. Il metodo è pulito:
- Installa mitmproxy (
brew install mitmproxy), avvialo una volta per generare il certificato CA in~/.mitmproxy/ - Aggiungi il certificato al keychain login (niente sudo; Grok non fa certificate pinning contro di esso):
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db ~/.mitmproxy/mitmproxy-ca-cert.pem - Lancia Grok attraverso il proxy:
HTTPS_PROXY=http://127.0.0.1:8080 SSL_CERT_FILE=~/.mitmproxy/mitmproxy-ca-cert.pem grok -p '<prompt>' --cwd <repo> - Per ispezionare gli artifact staged, copia al volo
~/.grok/upload_queue/*durante l’esecuzione, poigzip -dc | tar -x0
Repository canary: ogni file porta un marker unico, quindi qualsiasi cosa compaia nel traffico catturato è tracciabile in modo univoco. File secrets .env:
API_KEY=CANARY7F3A9-SECRET-should-not-leave DB_PASSWORD=CANARY7F3A9-DBPASS
Finding 1: i secret escono, non redatti
Quando Grok legge un file, il contenuto viene trasmesso a xAI. Nel corpo della richiesta POST /v1/responses (quella del model turn), e nell’archivio session_state caricato via POST /v1/storage. Senza redazione. Un .env viene inviato come qualsiasi altro file.
Artifact wire: corpo di richiesta POST decriptato da 48.070 byte verso cli-chat-proxy.grok.com/v1/responses, identificabile come model turn dal JSON incorporato con 'messages':[...]'model':'grok-4.5'. Contiene il file secrets verbatim:
...API_KEY=CANARY7F3A9-SECRET-should-not-leave\nDB_PASSWORD=CANARY7F3A9-DBPASS\n...'model':'grok-4.5'
Replica: grep -a 'CANARY7F3A9-DBPASS' secrets_responses_body.bin → match. Tutti e sei i marker dei file (source, logic, README, nested JS, API key, DB password) sono recuperabili dai corpi /v1/responses decriptati.
Secondo canale — persistito su Google Cloud Storage. Lo stesso contenuto è impacchettato in un archivio session_state caricato via POST /v1/storage. Provato decomprimendo l’artifact staged prima che venga drenato:
gzip -dc secrets_session_state.tar.gz | tar -x0 | grep -ao 'CANARY7F3A9-[A-Z]*' → CANARY7F3A9-SECRET, CANARY7F3A9-DBPASS, + tutti gli altri
Quindi il secret non solo viene processato in-flight; viene scritto in un archivio destinato allo storage.
Pre-empting ‘gli hai detto di leggere i secrets’: un run di controllo con un file che l’agente non doveva aprire (untouched_secret.txt) e il prompt ‘Reply exactly OK, do not read any files’ non ha prodotto alcuna occorrenza del marker di quel file in nessun corpo catturato. Il leak è quindi limitato ai file che Grok legge davvero — ma li legge in modo liberale (qualsiasi file rilevante per il task, incluso un .env) e non applica redazione ai contenuti. Il difetto è che un file di secrets è stato trasmesso non redatto, non l’atto di leggerlo.
Finding 2: l’intero repo, scala multi-GB, zero fallimenti storage
Grok carica uno snapshot dell’intero repo senza limiti di dimensione storage nel range testato. Man mano che il repo cresce, cambia strategia di upload e continua a restituire 200. Su un repo da 12 GB, 73 chunk da ~75,0 MB (5,10 GiB) caricati con zero fallimenti prima che l’analista fermasse la cattura.
Sweep delle dimensioni catturate (contenuto incomprimibile quindi il tar non può ridursi; sessione fresca ad ogni step). Solo la riga 12 GB è stata conservata come file (wire_12gb.log); le righe più piccole sono state osservate live ma i log non sono stati salvati:
| Dimensione repo | Comportamento upload (osservato sul wire) | Status |
| 64 MB | singolo POST /v1/storage, req=50548145 byte (48 MB) | 200 |
| ~600 MB | POST /v1/storage in chunk da ~7,5 MB (dozzine) | tutti 200 |
| ~3 GB | POST /v1/storage/multipart/init → PUT storage.googleapis.com/grok-code-session-traces/multipart/ in parti da 50 MB | tutti 200 |
| ~12 GB | POST /v1/storage in chunk da 75 MB; 73 chunk (~5,1 GB) catturati prima dello stop | tutti 200, 0 fallimenti |
Artifact conservato: wire_12gb.log. Contiene 83 risposte 200 a /v1/storage*: 82 richieste POST di content-upload — di cui 73 sono chunk da ~75,0 MB ciascuno (dimensioni byte min 75.014.811 / max 75.014.871, totale 5.476.083.317 B = 5,10 GB / 5,48 GB) più 9 POST più piccoli — e 1 check di dedup /v1/storage/batch_exists. Byte totali richieste /v1/storage*: 5.476.228.005 B. Zero richieste storage fallite.
La cattura è stata fermata mentre gli upload stavano ancora streamando (l’ultima riga è un altro chunk ~75 MB → 200), quindi questo dimostra ≥5,1 GB caricati, ancora in salita quando troncato — non che i 12 GB completi siano stati completati.
Questo è l’intero repo, non solo i file letti
Due canali distinti:
Canale A (§3, /v1/responses) trasporta i file che l’agente apre. Canale B (questo §4) è uno snapshot separato dell’intero workspace. Due linee di evidenza:
(a) Lo split di byte decisivo. Nella stessa sessione catturata da 12 GB — un repo al 100% di file random mai letti dall’agente — i due canali hanno mosso volumi selvaggiamente diversi:
- Canale A
/v1/responses(model turn): 196.705 B = 192 KB totali, attraverso 5 richieste, singolo turn più grande 60.394 B - Canale B
/v1/storage: 5.476.228.005 B = 5,10 GB - Rapporto ~27.800× (5.476.083.317 ÷ 196.705)
Il modello palesemente non ha mai ingerito i file (192 KB non possono trasportare 5 GB di contenuto), eppure 5,10 GB di essi sono usciti via /v1/storage. E attraverso lo sweep il volume /v1/storage traccia la dimensione totale del repo (64 MB → 12 GB). Byte su scala GB che lasciano un repo mai letto possono essere solo uno snapshot whole-repo.
(b) I path/string del binario corroborano il meccanismo: after_codebase.tar.gz, xai-grok-shell/src/upload/{trace,turn}.rs, repo_state.upload, ‘collecting workspace files’, ‘spawning background coordinator’.
(c) Un manifest di codebase staged enumera un file mai letto e lo content-addressa al bucket GCS. In un run separato su una vera codebase cereblab_api di 298 file, è stata copiata al volo la snapshot che Grok ha staged per l’upload. Il manifest elenca src/_probe/never_read_canary.txt — un file piantato e mai chiesto a Grok di aprire — insieme ai source reali (src/apns.ts, src/index.ts, …); e metadata.json dà a ogni file un fileId destinazione della forma gs://grok-code-session-traces/repo_changes_dedup/v2/supplemental/sha256_.... Quindi l’intero tree, file non letti inclusi, è enumerato e content-addressed per l’upload al bucket GCS nominato.
(d) DEFINITIVO — contenuto di un file mai letto recuperato verbatim da un git bundle caricato. Con l’account aggiornato a SuperGrok (quota sollevata), ri-esecuzione sulla vera repo cereblab_api con il prompt esplicito ‘Reply with exactly: OK. Do not read or open any files.’, avendo piantato un marker unico CANARY-XR47P2-NEVERREAD-UNIQUE in src/_probe/never_read_canary.txt. Grok ha caricato l’intero repository come git bundle via POST cli-chat-proxy.grok.com/v1/storage → 200 (req=152102 byte). Eseguendo git clone uploaded_repo.bundle si ricostruisce il repo, e src/_probe/never_read_canary.txt contiene il marker verbatim — un file che l’agente è stato esplicitamente istruito a non aprire. Il bundle porta anche l’intera storia git (4 commit, 47 file).
Questa è la prova ermetica per-file-content: l’intero repo — file non letti inclusi — ha lasciato la macchina ed è stato accettato (200). Il meccanismo di upload è un git bundle, quindi ‘whole repo’ è letterale (ogni file tracciato + storia). Replicato su una seconda codebase non correlata: la cattura identica sulla repo Cloudflare-Worker cereblab_auth ha prodotto un upload git-bundle (POST /v1/storage → 200, 31.743 B) da cui git clone ha recuperato il proprio marker mai-letto CANARY-AUTH-4T8K2-NEVERREAD verbatim. Due repo indipendenti, stesso risultato.
Nessuna richiesta storage/upload è fallita — ognuna delle 82 chiamate /v1/storage ha restituito 200. Gli unici non-200 nell’intera cattura erano sull’endpoint del modello più una chiamata di session-bookkeeping:
- 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, non un upload)
I 402/429 sono una quota di model-usage; il singolo 404 non è correlato allo storage. Notevolmente, gli upload storage hanno continuato a restituire 200 dopo che il model turn è stato rate-limited (76 /v1/storage 200 avvengono al o dopo il primo 429) — l’upload della codebase è indipendente dal fatto che il modello risponda o no.
Finding 3: destinazione, telemetria, e cosa non è documentato
Destinazione storage: Google Cloud Storage, bucket grok-code-session-traces. Questo si basa sulle stringhe conservate del binario grok-code-session-traces, storage.googleapis.com, e ‘Uploading bytes to GCS via proxy’, e su un metadata.json staged conservato i cui fileId per-file sono letteralmente gs://grok-code-session-traces/repo_changes_dedup/v2/...sha256.... Non è AWS S3.
Telemetria third-party: POST api.mixpanel.com/track e /engage (Mixpanel), più POST grok.com/_data/v1/events — tutti 200.
Non documentato nei materiali di setup: l’upload repo_state / session_state a grok-code-session-traces, o lo staging ~/.grok/upload_queue, non sono descritti nello script di install del CLI o nei materiali quickstart revisionati (questo non è un audit esaustivo di tutta la docs xAI). Il meccanismo è attivo di default sul login consumer standard.
Consenso e policy — detto onestamente
‘Gli strumenti cloud AI inviano contesto; è normale’. Vero, e concesso: qualsiasi coding agent cloud deve inviare codice al proprio server per agirci sopra. I delta nuovi qui sono: (a) un file di secrets (es. .env) è trasmesso non redatto, (b) il contenuto è persistito a un bucket GCS nominato, non solo processato transiently, e (c) il meccanismo di upload non è evidenziato nei materiali di setup del CLI ed è on di default.
‘È nei ToS / opt-in’. La policy consumer di xAI divulga in modo ampio l’uso dei dati per il miglioramento del modello con un opt-out (grok.com → Settings → Data → ‘Improve the model’); Private Chat fa auto-opt-out; l’opt-out è prospettico, non retroattivo. Ma divulgazione ampia del training ≠ documentare questo specifico meccanismo. La pipeline repo_state / upload_queue / grok-code-session-traces non è stata trovata descritta nei materiali CLI revisionati.
Il toggle ‘Improve the model’ non fa differenza — ON o OFF, l’intero repo viene caricato allo stesso modo. Con impostazioni di default, Grok ha caricato 5,10 GiB di un repo mai letto. Poi ‘Improve the model’ è stato disattivato e ri-eseguito: Grok ha ancora caricato l’intero repo come git bundle, e la risposta /v1/settings del server al CLI restituiva ancora 'trace_upload_enabled': true, 'upload_enabled': true, 'session_registry_enabled': true. L’opt-out governa il training, non se il tuo codice viene caricato/salvato; l’upload della codebase a grok-code-session-traces continua in ogni caso. Fare opt-out non ferma il tuo repository dal lasciare la macchina.
Cosa NON è stato provato (onestà intellettuale)
Non è stato provato che xAI allena su questi dati. Upload/storage ≠ training. Quello è governato da policy e tier account; è stata misurata solo la trasmissione.
Le linee wire dirette storage.googleapis.com/grok-code-session-traces PUT sono state osservate a 3 GB ma non conservate in questa sessione (il log è stato sovrascritto durante lo sweep). La claim multi-GB in §4 poggia quindi sul wire_12gb.log conservato (/v1/storage 200 in chunk da 75 MB) più le stringhe del binario che nominano il bucket.
Solo il log dello sweep 12 GB è stato conservato. Le righe 64 MB / 600 MB / 3 GB sono state osservate live ma i loro log di cattura non sono stati salvati (riproducibili una volta che la quota si resetta). La cattura 12 GB stessa è stata fermata mid-stream (~5,1 GB / 73 chunk catturati), quindi prova che l’upload multi-GB ha successo senza fallimenti, non che un’intera run di repo da 12 GB si completa end-to-end.
La claim ‘non documentato’ è scope-limited. È basata sullo script di install del CLI e il quickstart, non una ricerca esaustiva di tutta la documentazione xAI, articoli help-center, o policy. È possibile che il meccanismo sia descritto da qualche parte non controllata; l’affermazione difendibile è ‘non evidenziato nei materiali di setup del CLI stesso’.
Come disabilitare gli upload (dai commenti GitHub)
Un utente (@tomholford) ha testato quali switch funzionano davvero e ha confezionato il risultato in una repo: github.com/wetlink/grok-build-privacy-hardening. Risultati dai test sui lever stessi:
[harness] disable_codebase_upload = true in ~/.grok/config.toml è un veto hard alla fine della pipeline di upload whole-repo — in un canary adversarial ha tenuto anche con upload force-enabled via env (repo_state.upload.skip, zero enqueue).
Il canale session-trace (quello che porta i contenuti .env letti) è env-priority. GROK_TELEMETRY_TRACE_UPLOAD=false + GROK_TELEMETRY_ENABLED=false funzionano. Le chiavi TOML dall’aspetto plausibile [telemetry] trace_upload = false e [features] telemetry = false vengono silenziosamente ignorate dal binario — scriverle crea solo falsa confidenza.
Caveat: i pin env coprono solo invocazioni che passano attraverso uno shell wrapper (chiamate dirette ~/.grok/bin/grok li bypassano), e auto_update = true significa che la semantica può cambiare sotto di te — la repo include uno script verify.sh canary da ri-eseguire dopo ogni update.
Un punto dati sullo stato server attuale: dal 13 luglio 2026 il loro probe mostra in_remote_trace_upload_enabled: false, cioè gli upload sono attualmente in pausa via flag remoto — ma il client mantiene la capability, quindi pin locali restano necessari.
Config completo suggerito:
# env: export GROK_TELEMETRY_TRACE_UPLOAD=0 export GROK_TELEMETRY_ENABLED=0 # o ~/.grok/config.toml: [features] telemetry = false [telemetry] trace_upload = false mixpanel_enabled = false [harness] disable_codebase_upload = true
Replica e integrità
Tutti gli artifact hanno SHA-256. Il binario testato: grok 0.2.93 (f00f96316d4b), SHA-256 2a97ba675bd992aa9b981e2e83776460d94f469b510c0b8efe28b50d236d767c. Tutte le catture erano del proprio traffico sulla propria macchina; i ‘secrets’ erano stringhe canary fake; nessuna credenziale reale è stata esposta. I finding sono version-specific a grok 0.2.93 (luglio 2026); xAI può cambiare comportamento in qualsiasi momento.
Gli artifact chiave: secrets_responses_body.bin (48 KB, contenuti .env verbatim in un corpo POST /v1/responses), wire_12gb.log (83 /v1/storage* → 200 su un repo 12 GB, 5,10 GiB caricati, 0 fallimenti storage), uploaded_repo.bundle (152 KB, SHA-256 73b9c0af…, la smoking gun — git bundle wire-captured via POST /v1/storage → 200; git clone recupera file mai letti verbatim + storia git completa).
Comando replica condensato:
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> grep -a CANARY <saved /v1/responses body>
Nei commenti GitHub: ‘This is extremely concerning’ (@tulior), ‘I looked into this too. Is the default to upload the whole codebase? — Yes’ (@Dhravya, ha fatto girare Claude Code per trovare claim verificabili ed è built nel binario per certo), ‘This entire thing has to be a mistake… right?’ (@DovieW), ‘I would love to see an analysis of Cursor’ (@luke-lewandowski).
Già che ci siamo: qualcuno potrebbe fare la stessa analisi su Cursor, Claude Code, e gli altri coding agent? Sarebbe interessante vedere se questo è unico di Grok o se è pattern di settore.
