ZCode's Silent Full Git Upload - The Official Response, and My Questions
ZCode has responded to the silent upload of entire Git histories, and I raise my questions point by point; ferstar's original piece follows, reconstructing from a 313MB encrypted bundle stuck in pending how ZCode silently packages and uploads your whole workspace (complete .git history included), plus a one-command defense using a file system immutable lock.
Some background first. ZCode is Zhipu’s AI coding desktop client. Today, the blogger ferstar published a piece showing that as long as you are logged in, it silently packages your entire workspace, complete .git history included, encrypts it, and uploads it to Alibaba Cloud OSS, with no switch in the UI able to turn it off. After the piece went out, ZCode published an official response. I am putting that response and my questions first, with ferstar’s original article in full below.
Follow-up: The official response is here, and I have a few questions
After the original piece went out, ZCode published a statement in the community. Quoted in full:
Dear ZCode users,
We take today’s community discussion very seriously and completed a self-review at the first opportunity. First of all, we apologize to the affected users. The relevant circumstances are explained below:
This issue originated from ZCode’s “codebase indexing” feature. That feature is intended to help users generate a repository index locally, so as to support session checkpoint recovery including historical versions, historical version rollback, and Repo Wiki, among other things.
The Repo Wiki feature may trigger repository data upload when generating a Wiki page. After the Wiki page is generated in the cloud, the related uploaded data is destroyed immediately and is not retained. Because this feature was enabled by default in its early rollout, some users were affected. We are deeply sorry for this, and the issue has now been fixed.
We are fully aware that any issue concerning data directly affects users’ trust in the product. We will open-source the ZCode codebase in the near future, make the product better within a more open ecosystem, and invite third-party evaluators to review how the system operates, while continuously disclosing the progress of that review, in order to build everyone’s trust in a fully transparent way.
We sincerely apologize for the trouble this issue has caused everyone. We will provide all ZCode users with one additional weekly quota reset, which will be granted today.
Thank you once again for your attention and oversight. @everyone
I read this statement three times, and the more I read it, the more several parts fail to line up with the evidence chain in the original piece below.
One: “codebase indexing” is a nice phrase, but indexing should not index the whole .git and carry it away.
The statement says the issue arose from the “codebase indexing” feature, built to support checkpoint recovery, history rollback, and Repo Wiki. But the Manifest above is clear: in a 345MB baseline full snapshot, the .git directory alone accounts for 86.6% — LFS cache, the complete object store, reflog, all of it. A local index wants searchable structured data, not the repository’s every revision from creation to today, carried off verbatim. The word “index” cannot shoulder those 300-odd megabytes.
Two: “destroyed immediately after cloud generation, not retained” — this cannot prove itself.
The encryption scheme above is envelope encryption where the server dynamically issues the RSA public key and the private key lives only in the cloud. That means the data is decryptable once it reaches the cloud, and only by them. “Destroyed immediately” is a promise, not a technical fact you or I or any third party can verify: no audit log, no third-party attestation, no reproducible proof of destruction. On one side is a whole evidence chain of packaging and upload; on the other, the reply that “it has been destroyed.” However you do the math, it comes out short.
Three: the questions that actually matter are all unanswered.
- Why does the upload scope include the full
.githistory andreflograther than the current working tree? Does building Repo Wiki need the records of branches deleted five years ago? - Why are snapshot capture and upload instantiated unconditionally in the code, with no switch in the UI able to turn them off? If it is truly a feature for the user’s benefit, why not give the user a button to refuse?
- Why is the decryption private key kept only on the server? A feature advertised as checkpoint recovery for the user, yet the user cannot decrypt their own backup — whose data is it recovering?
- Why did the pending package in the article fail and retry 564 times, and re-upload automatically within half an hour of being manually deleted? Is this “may trigger upload,” or “determined to upload”?
The statement only admits that “the Repo Wiki feature may trigger repository data upload when generating a page.” On the core fact — the entire workspace, .git included, silently packaged — it says not a word.
Four: the compensation is very much of the internet.
For a data problem, what is offered is one “weekly quota reset.” Translation: for the whole code history being packaged and sent to the cloud, we compensate you with a few million tokens. Converting privacy into quota is the PR move this generation of AI products has mastered best.
Five: open-sourcing and third-party review are welcome, but the order is backwards.
“Open-source the codebase soon, invite third-party evaluators, disclose review progress” — all of this is good, and exactly what the original author had been calling for. But look at the timeline: the data was already uploaded, the article was already published, the community had already blown up, and only then did transparency come to mind. Get on the bus first, buy the ticket later — however earnest the ticket, it is still a late ticket. That said, actually open-sourcing and actually submitting to review beats stubborn denial; this one gets half marks from me.
Finally, that line about “completed a self-review at the first opportunity” also rewards a closer look. This upload mechanism had existed for a long time; the pending package failed 564 times, and the author deleted it only to have it re-upload automatically — it was there the whole time, just unseen. The so-called “first opportunity” was, I suspect, “the first moment it went public.”
Tools can err, and companies can apologize. But with data, what follows the apology matters more than the apology itself. I will be watching two things: whether the codebase really gets open-sourced, and whether that “destroyed immediately” ever becomes verifiable by a third party.
Below is the original repost.
RepostThis article is reposted from ferstar's blog, original piece "Digging Into ZCode's Shady Silent Upload of Your Entire Git History". Copyright belongs to the original author.
It started as a casual glance at why ~/.zcode was taking up more than 700 MB while I was cleaning up disk space. After poking at it on and off, I confirmed something pretty outrageous:
As long as you are signed in, ZCode (the official AI coding desktop client from Zhipu) silently packages and encrypts your entire workspace in the background — including the complete .git history, LFS large-file cache, reflog, and global app configuration — and uploads it straight to Alibaba Cloud OSS.
Even more ironic: the RSA public key used for encryption is handed out dynamically by the server, and the private key lives only in the cloud. That several-hundred-megabyte ciphertext sitting on your local disk cannot be decrypted by you, nor by the ZCode client itself.
This article documents the whole investigation, the chain of evidence, and the final one-command defense.
Starting Point: A 313MB Archive Stuck in pending
~/.zcode is ZCode’s local data root. At the time, the directory size breakdown looked roughly like this:
cli/: about 257MB (local session database, execution logs)computer-use/: about 130MB (the app itself and its runtime dependencies)v2/checkpoints/: about 303MB (the star of this story)
Inside v2/checkpoints/ sat a 313MB .enc encrypted file, along with a status file:
{
"workspacePath": "/Users/ferstar/myprojects/<some commercial project>",
"lastCompressedSize": {
"encryptedSizeBytes": 313070842,
"workspaceSizeBytes": 345549173
},
"kind": "baseline",
"failureCount": 564
}
The meaning is plain:
- The client scanned the commercial project I had open locally; after excluding a handful of directories such as
node_modules, it packaged and encrypted the remaining 345MB into a 313MB archive taggedbaseline(a full snapshot); - The status showed it had failed to upload 564 times, so it kept sitting in the local
pending/directory waiting to retry.
The project is 10GB in total; with dependencies excluded, the remaining 345MB is almost entirely core assets.
Where It Goes: From Logs to asar Reverse Engineering
The logs never printed the actual upload address, so I cracked open the client’s app.asar to read the code. The full upload chain reconstructs as follows:
sequenceDiagram
participant C as ZCode Client
participant S as zcode.z.ai
participant O as Aliyun OSS
C->>S: POST /api/v1/snapshot/upload-credential
S-->>C: snapshot_id + RSA public key + max_size + OSS form credential + callback
C->>C: pack tar.gz → AES-256-CTR encrypt → wrap key with RSA-OAEP
C->>O: PostObject form direct upload tar.gz.enc
O->>S: callback confirms receipt
The whole flow has two steps:
- First, ask the coordinator server for credentials: the client requests
https://zcode.z.ai(theVITE_ZCODE_ENDPOINT_ORIGINin the code), and the server returns the OSS form signature (policy,x-oss-signature), a dynamically assigned Object Key, a size limit, and the public key to use for this encryption; - Then upload directly to OSS via a form: after packaging and streaming-encrypting locally, the client bypasses Zhipu’s own business servers entirely and throws
tar.gz.encstraight at Alibaba Cloud OSS with an HTTP POST form. Once it finishes, the OSS server fires a callback to notify the Zhipu backend to register it.
I sniffed ZCode’s runtime connections, and the persistent HTTPS connections of the process were exactly the resolved IP of zcode.z.ai plus two Alibaba Cloud OSS nodes.
The Most Ironic Part: The Key Belongs to the Server
The client uses standard envelope encryption:
keyId: String(i.encryption.key_version),
keyWrapAlgorithm: "rsa-oaep-sha256",
publicKeySpkiPem: Ylt(i.encryption.public_key)
- The file contents are encrypted with a randomly generated symmetric key using AES-256-CTR;
- The symmetric key is wrapped with RSA-OAEP-SHA256, using the public key dynamically obtained from the server above.
The crux is this RSA public key: it is handed out temporarily by the server when issuing the upload credentials, and the private key only ever lives in the cloud. I tried every private key on my machine to unwrap the key in the envelope, and without suspense they all failed.
This means: that 313MB ciphertext on your disk cannot be opened by you, nor by the ZCode client itself — only the private key on Zhipu’s backend can decrypt it.
If this were really for user-facing checkpoint recovery or cross-device sync, the key ought to be bound locally (like Git or Time Machine). A key only the server can unlock serves exactly one purpose: to guarantee the server alone can see it.
What’s Actually in the Snapshot: Nearly 90% Is .git
The ciphertext may be unbreakable, but the manifest left behind when the snapshot was generated is written plainly on local disk. I pulled the manifest with its 42,411 files and tallied it up:
| Content | Size | Share | Information included |
|---|---|---|---|
.git/lfs/ | 196.1 MB | 56.8% | LFS cache — every large file and binary asset ever pulled down across the project’s history |
.git/objects/ | 102.2 MB | 29.6% | The complete Git history object database (Commit, Tree, Blob) |
.git/logs/ | 0.6 MB | 0.2% | reflog trail — every local branch operation and unpushed record |
| Everything else: source and docs | ~46.2 MB | 13.4% | src/, various config files, and business code |
The single .git directory accounts for a full 86.6%.
In other words, once this archive goes up, what the cloud gets is by no means just the code in your current workspace, but the entire historical underwear drawer of this repository since the day it was created:
- sensitive configs and historical keys that were overwritten and deleted long ago;
- branch names not yet pushed to the remote (directly exposing unreleased R&D directions of the business);
- the internal self-hosted GitLab domain and repository paths configured in
.git/config.
On top of that, the code carries a repo_snapshot_extra_manifest that hashes your global ZCode configuration files (such as settings.behavior.json), packages them across workspaces, and sends them up to the cloud with every snapshot.
The Truth About the Switches: The One You Turn Off Doesn’t Control It
Most people’s first instinct is to go into settings and flip a switch. I went through the settings items and the code logic one by one:
| Switch | What you think it controls | What it actually controls |
|---|---|---|
Optimize Experience (optimizeAgentExperienceEnabled) | Data collection / telemetry upload | Only whether your data is used to train models. Turn it off and snapshots are still captured and uploaded |
Repository Snapshot Indexing (repoSnapshotIndexingEnabled) | The snapshot feature itself | Only whether the server indexes the snapshot after receiving it. Turn it off and the local packaging and upload proceeds without missing a beat |
The client’s assembly code is even more blunt: the sidecar responsible for snapshot capture and upload is unconditionally instantiated at startup. There is no if check against user configuration anywhere in the code; the only requirement is that tokenProvider can obtain the JWT after login.
In one sentence: as long as you are signed into an account, this upload mechanism is permanently on, and there is no switch in the UI that can turn it off.
There are two capture triggers: one is captureBeforePrompt (before every prompt is sent), and the other is a repo-wiki-update mark at the end of a task. Going through the logs, a single active session can produce as many as 62 snapshot captures.
What the Privacy Policy Says
I looked through ZCode’s privacy policy, and it explicitly states it collects “text, files, and code submitted in conversations” — that is the standard operation for an AI assistant invoking model inference, and every vendor does the same.
But nowhere does it mention silently packaging and uploading your entire workspace together with its complete Git history. The official docs, FAQ, and changelog say nothing about it either.
The only thing that lines up is one catch-all line: “Optimization programs are off by default; your input will not be used for training unless you opt in.”
Defense: Deleting Is Whack-a-Mole, Just Lock the Directory
When I first found the pending archive I casually deleted it, and within half an hour it captured again — a fresh 313MB archive, with the failure count jumping from 564 to 565. The uploader noticed the local file was gone and simply repackaged one. Relying on manual deletion is whack-a-mole.
The most direct and effective fix is to add an immutable lock at the file system layer, forbidding writes to this directory at the kernel level:
macOS
# Clear and lock the checkpoints directory
rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
chflags uchg ~/.zcode/v2/checkpoints
# Verify: should output Operation not permitted
touch ~/.zcode/v2/checkpoints/test
Linux
# Clear and lock the checkpoints directory
rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
sudo chattr +i ~/.zcode/v2/checkpoints
# Verify: should output Operation not permitted
touch ~/.zcode/v2/checkpoints/test
Impact and Recovery
- Blocking effect: when the snapshot logic tries to write to the directory it is intercepted directly by the system kernel; with no artifact produced, the subsequent direct upload to OSS naturally has nothing to send;
- Functional impact: ZCode’s “checkpoint rollback / timeline” feature becomes unavailable (it was bought with full-code uploads to the cloud anyway). Everyday code completion, chat, and tool calls work perfectly fine; the swallowed IO errors in the logs do not affect usage;
- To revert: run
chflags nouchg ~/.zcode/v2/checkpoints(macOS) orsudo chattr -i ~/.zcode/v2/checkpoints(Linux).
Final Thoughts
When you use AI tools, model inference inevitably needs to consume code context — everyone understands that going in. But what crosses the line here is clear:
First, the scope of data. Inference is given the context relevant to the current task, whereas the snapshot packages up the entire repository along with years of Git commit history and carries it all away.
Second, the architectural posture. If this were truly for user-facing checkpoint recovery or cross-device sync, the decryption key ought to be local to the user. A key only the server can unlock, a zero-disclosure privacy policy, default behavior you cannot turn off, and the stubbornness to re-upload after deletion — this is plainly not backup, but collection.
Tools are not inherently guilty, but the line should be drawn by the user. Since it cannot be turned off inside the software, use the operating system’s lock to shut it in a cage.
Comments