ZCode Uploads Your Entire Git History, and No Agent Permission Setting Can Stop It
The snapshot pipeline runs outside the agent's tool loop, which means the trust boundary most people audit is the wrong one
A developer cleaning up disk space found 700MB sitting in ~/.zcode and pulled the thread. Inside was a single 313MB encrypted file, built from a 345MB commercial project, with a metadata sidecar recording 564 failed upload attempts. The archive could not be opened. Not by the user, and not by the application that made it, because the RSA public key used to wrap the encryption key arrives from the server at upload time and the matching private key never touches the machine.
That is the part people are quoting. The part that should change how you audit tools is quieter and lives one layer down: the upload pipeline is not something the agent does. It is not a tool the model can call, and it does not appear anywhere in the agent's tool surface. It is a sidecar in the host process that starts unconditionally when the app launches. Which means every permission model, every tool-call classifier, every approval prompt you have been told makes agents safe would have sat there watching the tool loop and seen nothing at all.
What was actually found
The researcher, publishing as ferstar, documented the whole chain on September 18 using local forensics plus reverse engineering of the Electron app.asar bundle. ZCode is the desktop coding app from Z.ai, the company behind the GLM open-weight models.
The mechanism, as reconstructed: the client requests upload credentials from zcode.z.ai, which returns Aliyun OSS form signatures, a per-request object key, a size cap, and a fresh RSA public key for that round. The client tars the workspace, encrypts it with AES-256-CTR, wraps the symmetric key with RSA-OAEP-SHA256 using the server's public key, and POSTs the ciphertext directly to Aliyun OSS, which then calls back to Z.ai's backend to register the snapshot. Textbook envelope encryption, with one asymmetry that decides the whole story: ferstar tried to unwrap the envelope key with every private key on the local system and failed, as expected, because there was never a local key to try.
The packaging manifest stays on disk in plaintext, so we know what went in. For one 42,411-file snapshot: .git/lfs/ at 196.1MB, .git/objects/ at 102.2MB, .git/logs/ at 0.6MB, and source plus docs at roughly 46.2MB. The .git directory is 86.6% of the payload.
Sit with that ratio for a second. A git object store is not your working tree. It is every state the repository has ever been in. The API key you committed in March and removed in April is in there. So are the reflogs, which carry local branch names you never pushed, and .git/config, which carries internal hostnames and repository paths. What leaves the machine is not the files you had open when you asked a question. It is the institutional memory of the project.
Capture fires before every prompt and again on task completion. ferstar found up to 62 capture events in the logs of a single active session.
The two switches that look like off switches
The obvious response is to open settings, and ferstar did, cross-referencing each UI toggle against the code.
"Optimize Experience," internally optimizeAgentExperienceEnabled, governs whether your data is authorized for model training. Snapshot capture and upload keep running. "Repo Snapshot Indexing," internally repoSnapshotIndexingEnabled, governs whether the server indexes the snapshots it receives. Local packaging and upload keep running.
Per the writeup, the host assembly instantiates the capture sidecar with no conditional on user preferences at all. The only precondition is that the token provider can return a valid JWT. Logged in means capturing.
Deleting the pending archive does not help either. ferstar deleted one and had a fresh 313MB archive within half an hour, retry counter advancing from 564 to 565. The uploader treats a missing file as work to redo.
The structural point
Here is the finding that generalizes past this one app, and it comes from a second source rather than ferstar's own post, so hold it accordingly. The site Tokenstead, covering the same disclosure, cross-references a captured ZCode system prompt held in a public collection called OrcaPromptVault: roughly 131KB of instructions and a 31-tool surface. Tokenstead reports that the agent's complete tool surface contains no snapshot tool, no upload tool, and no telemetry tool, and that across those 131KB of instructions there is no mention of Aliyun, OSS, uploads, or privacy. Tokenstead's summary of what that implies is the sentence worth keeping: the exfiltration pipeline "is not an agent tool; it is a host-level sidecar instantiated outside the tool loop."
I have not verified the OrcaPromptVault capture myself, and Tokenstead is a site whose editorial position favors local and open tooling. Treat the prompt-capture details as secondary reporting. The architectural claim, though, is independently supported by ferstar's own finding that the sidecar instantiates at startup gated only on a JWT, with no preference checks anywhere in the path.
And that architecture defeats the entire safety stack the industry has spent a year building.
Think about what a tool-call gate actually does. It sits between the model deciding to call something and the runtime executing it. Claude Code, Codex and Cursor all ship a version of this, and as of September 17 LangChain ships an open one, writing that until now this classifier step had been "locked away in the closed source parts of the harness." That is real progress, and it protects against a real failure mode: the model, possibly steered by a prompt injection, tries to do something you did not sanction.
None of it applies here. The model never tried anything. The model does not know the pipeline exists. The upload happened before your first prompt was even sent, in a sibling code path, on a schedule the agent has no visibility into. You can set every permission to ask, deny every bash command, run in a sandbox, and the snapshot still goes out, because the thing you sandboxed was the agent and the thing uploading was the application.
Z.ai's privacy policy says the tool collects "text, files, and code submitted during conversations," which is the standard inference-context disclosure every AI coding tool makes and which most users have made peace with. ferstar reports finding no mention anywhere in the policy, FAQ, or changelog of packaging and uploading whole workspaces and git histories. Tokenstead adds that a Z.ai executive, asked publicly whether ZCode would contain "any sort of spyware," said the company would not implement "anything beyond what's listed" on the ZCode website, and that workspace snapshotting is not listed there. As of both writeups, Z.ai's official account had not responded.
Put this into practice
The lowest-friction version of this is two questions and about twenty minutes, and it works on any harness you run, including the ones you trust.
Look at the data directory before you look at the settings. Every one of these tools has a home under ~. Check its size first, because volume is the tell that costs you nothing to read. du -sh ~/.claude ~/.codex ~/.cursor ~/.zcode 2>/dev/null takes a second. Anything in the hundreds of megabytes that is not a model or a node_modules deserves an explanation. Then look for what a pending-upload queue looks like: directories named checkpoints, snapshots, pending, outbox, or files with .enc extensions and JSON siblings holding byte counts and failure counters. That metadata file is what turned this from a suspicion into a story.
Watch what it connects to while it is idle. Open the app, sign in, and touch nothing. On macOS, lsof -i -P | grep -i <appname>; on Linux, ss -tp | grep -i <appname>. A coding assistant at rest should be talking to its API endpoint. Persistent connections to object storage are a different thing, and object storage is where bulk data goes.
Do not trust a toggle you have not traced. The whole lesson of this disclosure is that two settings which read as off switches governed something adjacent to the behavior they appeared to control. If a setting matters to you, either find the code path or test the behavior directly. Flip it, then watch the directory.
If you need the lock, use the filesystem, not the app. ferstar's fix, which works because it denies the write at kernel level rather than asking the app nicely: rm -rf ~/.zcode/v2/checkpoints && mkdir -p ~/.zcode/v2/checkpoints && chflags uchg ~/.zcode/v2/checkpoints on macOS, or sudo chattr +i ~/.zcode/v2/checkpoints on Linux. Reverse with chflags nouchg or chattr -i. The cost is that the checkpoint and rewind feature stops working, which per ferstar always required the upload in the first place. Chat, completion and tool execution keep working. Swallowed I/O errors show up in the logs and do nothing.
The general form of that last move is worth internalizing even if you never touch ZCode. When an application will not let you turn something off, the operating system will.
What this piece does not establish
I have not run ZCode. Everything above rests on one researcher's forensics and reverse engineering plus one secondary writeup, and neither has been independently reproduced by a third party as far as I can tell. ferstar's post carries a note that it was AI-drafted and approved by the author, and the English version is translated.
Z.ai has not published a response. That means we do not know the intended purpose of the pipeline, whether the data is retained, for how long, or under what access controls, and I am not going to guess. A checkpoint and rewind feature is a real product need, and a plausible innocent reading is that this is a badly-designed implementation of one where the encryption scheme was chosen for server-side convenience rather than to read your code. ferstar takes the harder line, writing that "a key that only the server can use serves exactly one purpose: making sure the server can read your code whenever it wants." That is an inference about intent from an architecture, and it is a reasonable one, and it is still an inference.
The 131KB prompt capture and 31-tool count come from Tokenstead citing OrcaPromptVault, not from a source I read directly. If the architectural claim matters to your decision, verify it yourself before acting on it.
Finally, nothing here says the closed harnesses you already use are clean. It says the audit most people perform on them is aimed at the wrong layer. That applies to every harness in this category, including the ones with good reputations, and including the one you opened this morning.
What to do with this
The agent is the part everyone watches, because it is the part that talks. It is also, structurally, the part with the narrowest permissions and the most scrutiny. The application that hosts it has a filesystem, a network stack, a login token, and no classifier reading over its shoulder.
Run the two commands. Look at the directory, watch the sockets, and find out what your harness does in the ten seconds after you sign in and before you type anything. If the answer is boring, you have lost twenty minutes and gained a baseline you can diff against after the next update. If it is not boring, you will want to have found out on a Friday morning rather than from somebody else's blog post.
Sources: ferstar, "Inside ZCode: Silently Uploading Your Entire Git History to the Cloud" · Tokenstead's coverage of the same disclosure · LangChain, "Building a Harness with Jev" · Hacker News discussion
Medium metadata
Title: ZCode Uploads Your Entire Git History, and No Agent Permission Setting Can Stop It Subtitle: The snapshot pipeline runs outside the agent's tool loop, which means the trust boundary most people audit is the wrong one Tags: AI Agents, Security, Developer Tools, Privacy, Open Source Canonical: fervorai.dev