Inside ZCode: Je volledige Git-geschiedenis stilletjes naar de cloud uploaden

Wanneer je bent ingelogd, verpakt ZCode (de officiële AI-coding desktop-app van Zhipu) stilletjes je volledige workspace — inclusief de volledige .git-geschiedenis, LFS asset-cache, reflogs en globale app-configuraties — versleutelt deze en uploadt deze direct naar Aliyun OSS.

Nog ironischer is dat de RSA-publieke sleutel die wordt gebruikt voor de versleuteling on-the-fly door de server wordt geleverd, terwijl de privésleutel uitsluitend in de cloud staat. Je kunt het versleutelde bestand van honderden megabytes op je eigen schijf niet ontsleutelen, en de ZCode-client zelf kan dat ook niet.

Hieronder volgt het volledige verslag van het onderzoek, de bewijsketen en een eenvoudige oplossing om dit permanent te stoppen.

Het startpunt: een archief van 313 MB dat op 'pending' staat

De map ~/.zcode is de data-root van ZCode. De verdeling van de bestandsgrootte zag er ongeveer als volgt uit:

  • cli/: ~257 MB (sessie-databases, uitvoeringslogs)
  • computer-use/: ~130 MB (gebundelde app en runtime-dependencies)
  • v2/checkpoints/: ~303 MB (de hoofdverdachte)

Binnen v2/checkpoints/ vond ik een .enc-bestand van 313 MB samen met een metadata-bestand voor de status:

{
  "workspacePath": "/Users/ferstar/myprojects/<een commercieel project>",
  "lastCompressedSize": {
    "encryptedSizeBytes": 313070842,
    "workspaceSizeBytes": 345549173
  },
  "kind": "baseline",
  "failureCount": 564
}

Het verhaal was duidelijk: de client scande mijn actieve commerciële project, sloot node_modules en enkele andere mappen uit, en verpakte de resterende 345 MB in een versleuteld archief van 313 MB, aangeduid als baseline (volledige snapshot). Er waren 564 mislukte uploadpogingen geregistreerd, waardoor het bestand in de lokale pending/ map bleef staan in afwachting van de volgende poging.

Het volledige repository was 10 GB; minus de dependencies was de resterende 345 MB bijna volledig core intellectual property.

Waar het naartoe gaat: van logs naar asar reverse engineering

De logs bevatten geen expliciete upload-URL's, dus ik heb de app.asar van de client geopend. De gereconstrueerde upload-flow is als volgt:

  1. Client → Server (zcode.z.ai): POST /api/v1/snapshot/upload-credential
  2. Server → Client: Terugkoppeling van snapshotid + RSA publieke sleutel + maxsize + OSS form credentials + callback
  3. Client (intern): tar.gz pack → AES-256-CTR encryptie → RSA-OAEP wrap key
  4. Client → Aliyun OSS: Directe upload van tar.gz.enc via PostObject
  5. Aliyun OSS → Server: Callback bevestigt ontvangst

Het proces verloopt in twee fasen:

  • Inloggegevens aanvragen bij de coördinator: De client roept https://zcode.z.ai aan. De server stuurt OSS form-signatures (policy, x-oss-signature), een dynamische Object Key, groottebeperkingen en de RSA-publieke sleutel voor deze versleutelingsronde terug.
  • Directe form POST naar OSS: Na het lokaal archiveren en streamen van de encryptie, omzeilt de client de applicatieservers van ZCode en post tar.gz.enc direct naar Aliyun OSS via een HTTP POST-formulier. OSS meldt dit vervolgens terug aan de backend van Zhipu om de snapshot te registreren.

Controle van actieve sockets bevestigde dit: het draaiende ZCode-proces hield persistente HTTPS-verbindingen in stand met de IP-endpoints van zcode.z.ai en twee Aliyun OSS-storage nodes.

Het meest ironische deel: de sleutel is van de server

De implementatie van de versleuteling maakt gebruik van standaard envelope encryption:

  • keyId: String(i.encryption.key_version)
  • keyWrapAlgorithm: "rsa-oaep-sha256"
  • publicKeySpkiPem: Ylt(i.encryption.public_key)

De inhoud wordt versleuteld met een tijdelijke symmetrische sleutel via AES-256-CTR. Deze symmetrische sleutel wordt vervolgens 'verpakt' (wrapped) met RSA-OAEP-SHA256, gebruikmakend van de publieke sleutel die door de server is geleverd.

Het cruciale punt is die publieke sleutel: deze wordt door de server overhandigd tijdens de onderhandeling over de inloggegevens, en de bijbehorende privésleutel komt nooit op jouw machine terecht. Het ontsleutelen van de envelope-sleutel met alle lokale privésleutels op mijn systeem mislukte, zoals verwacht.

Met andere woorden: dat versleutelde bestand van 313 MB op je schijf kan niet door jou of door de client worden geopend. Alleen de backend van Zhipu bezit de sleutel om het te ontsleutelen.

Als deze functie echt bedoeld was voor gebruikersfuncties zoals rollbacks of synchronisatie tussen apparaten, zouden de sleutels lokaal moeten staan (net als bij Git of Time Machine). Een sleutel die alleen de server kan gebruiken, dient precies één doel: ervoor zorgen dat de server je code kan lezen wanneer hij dat wil.

Wat er wordt verpakt: bijna 90% is .git

Hoewel het versleutelde bestand vergrendeld is, wordt het Manifest (de bestandsinventaris) dat tijdens het verpakken wordt gegenereerd lokaal in platte tekst opgeslagen. De analyse van een snapshot van 42.411 bestanden laat het volgende zien:

InhoudGrootteAandeelBevatte informatie
.git/lfs/196,1 MB56,8%LFS cache — alle binaire assets en grote media ooit gedownload
.git/objects/102,2 MB29,6%Volledige object store van commit-geschiedenis (commits, trees, blobs)
.git/logs/0,6 MB0,2%Reflogs — lokale branch-geschiedenis en niet-gepushte acties
Broncode & docs~46,2 MB13,4%src/, configuratiebestanden, interne documentatie

De .git-map alleen al is goed voor 86,6% van de payload. Zodra dit is geüpload, ontvangt de cloud veel meer dan alleen je huidige werkmap; het krijgt de volledige stamboom van je repository sinds dag één:

  • Historische API-sleutels en gevoelige configuraties die in latere commits zijn verwijderd.
  • Niet-gepushte lokale branch-namen (die plannen voor niet-uitgebrachte functies onthullen).
  • Interne GitLab-hostnamen en repository-paden geconfigureerd in .git/config.

Daarnaast maakt een extra manifest genaamd reposnapshotextra_manifest hashes van je globale ZCode-configuratiebestanden (zoals settings.behavior.json) en voegt deze bij elke snapshot toe over verschillende workspaces heen.

De waarheid over de schakelaars: UI-instellingen stoppen het niet

De natuurlijke reactie is om in de instellingen te kijken of je dit kunt uitschakelen. Ik heb de UI-opties vergeleken met de broncode:

SchakelaarVerwachte functieWerkelijke functie
Optimize Experience (optimizeAgentExperienceEnabled)Schakelt telemetrie / dataverzameling uitBeheert alleen of data geautoriseerd is voor modeltraining. Snapshots blijven worden gemaakt en geüpload.
Repo Snapshot Indexing (repoSnapshotIndexingEnabled)Schakelt de snapshot-functie uitBeheert alleen of de server geüploade snapshots indexeert. Lokaal verpakken en uploaden gaan ononderbroken door.

Kijkend naar de host-assemblycode wordt duidelijk dat de capture/upload sidecar onvoorwaardelijk bij het opstarten wordt geïnitieerd. Er zijn geen controles op gebruikersvoorkeuren; de enige vereiste is dat de tokenProvider een geldig JWT kan teruggeven.

Kortom: zolang je bent ingelogd, is deze achtergrondpipeline permanent actief en kan geen enkele UI-instelling dit stopzetten. De triggers voor capture vinden plaats op twee momenten: captureBeforePrompt (voor elke prompt) en bij voltooiing van taken getagd met repo-wiki-update. In sessielogs genereerde één actieve sessie tot wel 62 capture-events.

Wat het privacybeleid zegt

In het privacybeleid van ZCode staat expliciet dat het "tekst, bestanden en code verzamelt die tijdens conversaties worden ingediend" — een standaardpraktijk om context aan LLM's te voeden.

Echter, in het gehele beleid, de FAQ's en changelogs wordt er geen woord gerept over het stilletjes verpakken en uploaden van volledige workspaces en volledige Git-geschiedenissen. De enige vermelding die in de buurt komt, is de generieke standaardzin: "het optimalisatieprogramma staat standaard uit, en inputs zullen zonder toestemming niet worden gebruikt voor training".

Verdediging: verwijderen is dweilen met de kraan open; vergrendel de map

Toen ik het tijdelijke pakket voor het eerst vond, verwijderde ik het simpelweg. Binnen een half uur was het opnieuw gemaakt — een vers archief van 313 MB waarbij de teller van mislukte pogingen versprong van 564 naar 565. Wanneer de uploader ziet dat het bestand weg is, pakt hij gewoon een nieuwe. Handmatig verwijderen is een zinloze strijd.

De schoonste en meest effectieve oplossing is het instellen van een onveranderlijkheidsvlag (immutability flag) op bestandsniveau, waardoor schrijftoegang op kernelniveau wordt ontzegd.

macOS

# Wis en vergrendel de checkpoints directory
rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
chflags uchg ~/.zcode/v2/checkpoints

# Verificatie: zou "Operation not permitted" moeten geven
touch ~/.zcode/v2/checkpoints/test

Linux

# Wis en vergrendel de checkpoints directory
rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
sudo chattr +i ~/.zcode/v2/checkpoints

# Verificatie: zou "Operation not permitted" moeten geven
touch ~/.zcode/v2/checkpoints/test

Impact & Rollback

  • Resultaat: De capture-logica wordt geblokkeerd door de kernel zodra er een poging tot disk I/O wordt gedaan. Zonder lokale artefacten heeft de uploadpipeline niets om te verzenden.
  • Trade-off: De UI-functie voor "checkpoint rollback / timeline" zal niet meer werken (die vereiste sowieso dat je code geüpload werd). Normale chat, autocomplete en tool-uitvoeringen werken zonder problemen. I/O-fouten in de logs zijn ongevaarlijk.
  • Herstellen: Voer chflags nouchg ~/.zcode/v2/checkpoints uit (macOS) of sudo chattr -i ~/.zcode/v2/checkpoints (Linux).

Slotbeschouwingen

Bij het gebruik van AI-tools is model-inferentie onvermijdelijk afhankelijk van code-context — dat accepteert iedereen. Maar dit gedrag overschrijdt duidelijk de grens op twee manieren:

Ten eerste, de scope van de data. Inferentie verzendt taakrelevante context; snapshotting exfiltreert de volledige repository inclusief jaren aan Git-commitgeschiedenis.

Ten tweede, de architecturale houding. Als dit echt was ontworpen voor herstel of synchronisatie door de gebruiker, zouden de decryptiesleutels bij de gebruiker liggen. Een encryptiesleutel die uitsluitend door de server wordt beheerd, het ontbreken van vermeldingen in privacybeleid, onstopbare achtergrond-uploads en hardnekkig opnieuw verpakken na verwijdering — dit ziet er minder uit als een backup en veel meer als dataverzameling.

Tools zijn tools, maar gebruikers moeten hun eigen grenzen trekken. Als de software je niet toestaat het uit te schakelen, gebruik dan de OS-kernel om het in een kooi te zetten.