zgba 站群
Inside ZCode: Silently uploading your Git history to the cloud

Inside ZCode: Silently uploading your Git history to the cloud

I am not a native English speaker; this article was translated by AI.

It started with a routine check while freeing up disk space: ~/.zcode was taking up over 700MB. After digging into it intermittently, I confirmed something pretty wild:

Whenever you are logged in, ZCode (Zhipu’s official AI coding desktop app) silently packages your entire workspace — complete .git history, LFS asset cache, reflogs, and global app configs — encrypts it, and uploads it directly to Aliyun OSS.

Even more ironic: the RSA public key used for encryption is delivered on the fly by the server, while the private key lives exclusively in the cloud. You cannot decrypt that multi-hundred-megabyte ciphertext sitting right on your own disk, and neither can the ZCode client itself.

Here is the complete record of the investigation, the evidence chain, and a one-liner defense that permanently shuts it down.

~/.zcode is the data root of ZCode. The size breakdown looked roughly like this:

Inside v2/checkpoints/, I found a 313MB .enc file alongside a state metadata file:

The story was straightforward:

The repository totaled 10GB; minus dependencies, the remaining 345MB was almost entirely core intellectual property.

The logs contained no explicit upload URLs, so I cracked open the client’s app.asar. The reconstructed upload flow:

The pipeline runs in two stages:

Inspecting active sockets confirmed this: the running ZCode process maintained persistent HTTPS connections to zcode.z.ai IP endpoints plus two Aliyun OSS storage nodes.

The encryption implementation uses textbook envelope encryption:

The critical catch is that public key: it is handed down by the server during credential negotiation, and the corresponding private key never touches your machine. Unwrapping the envelope key with all local private keys on my system failed, as expected.

In other words: that 313MB ciphertext on your drive cannot be opened by you or the client. Only Zhipu’s backend holds the key to unlock it.

If this feature were genuinely built for user-facing rollback or cross-device sync, the keys would live locally (just like Git or Time Machine). A key that only the server can use serves exactly one purpose: making sure the server can read your code whenever it wants.

Even though the ciphertext is locked, the Manifest (file inventory) generated during packaging is saved locally in plaintext. Breaking down a snapshot of 42,411 files:

The .git directory alone accounts for 86.6% of the payload.

Once uploaded, the cloud receives far more than your current working tree — it gets the entire lineage of your repository since day one:

Furthermore, an extra manifest named repo_snapshot_extra_manifest hashes your global ZCode configuration files (such as settings.behavior.json) and bundles them across workspaces with every snapshot.

The natural reaction is checking settings to toggle it off. I cross-referenced the UI options with the codebase:

Looking at host assembly code makes it crystal clear: the capture/upload sidecar is instantiated unconditionally at startup. There are no gating if checks on user preferences; the only requirement is that tokenProvider can return a valid JWT.

Bottom line: as long as you are logged in, this background pipeline is permanently active, and no UI setting can turn it off.

Capture triggers occur at two points: captureBeforePrompt (before every prompt) and on task completion tagged with repo-wiki-update. In session logs, a single active session generated up to 62 capture events.

Checking ZCode’s privacy policy, it explicitly states that it collects “text, files, and code submitted during conversations” — standard practice for feeding context to LLMs.

However, across the entire policy, FAQs, and changelogs, there is not a single mention of silently packaging and uploading entire workspaces and full Git histories.

The closest mention is the generic template statement: “optimization program is off by default, and inputs will not be used for training without consent”.

When I first found the pending package, I simply deleted it. Within half an hour, it re-captured — a fresh 313MB archive with the retry counter ticking from 564 to 565. When the uploader sees the file is gone, it just packs a new one. Manual deletion is whack-a-mole.

The cleanest and most effective solution is setting an immutability flag at the filesystem level, denying write access at the kernel level:

When using AI tools, model inference inevitably needs code context — everyone accepts that going in. But this behavior clearly crosses the line in two ways:

First, data scope. Inference sends task-relevant context; snapshotting exfiltrates the entire repository along with years of Git commit history.

Second, architectural posture. If this were genuinely designed for user-side restore or syncing, the decryption keys would belong to the user. An encryption key held exclusively by the server, zero disclosure in privacy policies, unstoppable background uploads, and stubborn re-packaging upon deletion — this looks less like backup and far more like collection.

Tools are tools, but users must draw their own boundaries. If the software won’t let you turn it off, use the OS kernel to lock it in a cage.

View original article