ZCode's Silent .git Upload: Why AI Coding Harnesses Are Now a Data-Perimeter Problem
On September 18, 2026, a developer going by ferstar published a reverse-engineering walkthrough of ZCode, Z.ai's first-party AI coding desktop app, and the finding is the kind that should end up in every platform team's AI-tooling policy document: whenever the app is logged in, it silently packages the user's entire workspace — complete .git history, LFS asset cache, reflogs, and global app configs — encrypts the archive, and uploads it directly to Aliyun OSS, Alibaba Cloud's object storage. The researcher's own capture was a 313 MB encrypted archive built from a 345 MB commercial workspace spanning 42,411 files, with 564 failed upload attempts logged in the retry state while they investigated.
ZCode launched on July 2, 2026 as Z.ai's answer to Claude Code, Cursor, and Copilot — a harness for its GLM-5.2 model, pitched on first-party integration. The company behind it went public on the Hong Kong Stock Exchange in January 2026 under code 2513. Three months after launch, the harness is accused of doing something no inference-context disclosure covers: full-repository collection, encrypted so that only the vendor can read it.
This article is not a hot-take about a specific vendor. It is a case study in a structural problem your team already has: AI coding harnesses are unmanaged, credentialed exfiltration endpoints running on developer machines with full filesystem access — and most orgs have zero detection or containment posture for them. The ZCode finding is simply the most documented example yet.
The Finding in Numbers
ferstar's investigation started with a disk-space check: ~/.zcode had grown past 700 MB. Inside v2/checkpoints/ sat a 313 MB .enc file and a plaintext state file that reads like a confession:
{
"workspacePath": "/Users/ferstar/myprojects/<a commercial project>",
"lastCompressedSize": {
"encryptedSizeBytes": 313070842,
"workspaceSizeBytes": 345549173
},
"kind": "baseline",
"failureCount": 564
}
The packaging manifest — stored locally in plaintext, even though the payload is encrypted — shows what actually leaves the machine. The working tree is a minority of the payload:
| Content | Size | Share | What it contains |
|---|---|---|---|
.git/lfs/ |
196.1 MB | 56.8% | LFS object cache — every binary asset and large media file ever pulled |
.git/objects/ |
102.2 MB | 29.6% | The complete commit history: every commit, tree, and blob ever made locally |
| Source code & docs | ~46.2 MB | 13.4% | The current working tree minus node_modules-style exclusions |
.git/logs/ |
0.6 MB | 0.2% | Reflogs — local branch history and unpushed operational traces |
The .git directory alone is 86.6% of the payload. A second manifest, repo_snapshot_extra_manifest, additionally hashes global ZCode configuration files and bundles them across workspaces with every snapshot — so the archive also links the workspace to the developer's other projects.
Why "it just reads your code" understates this by an order of magnitude
Inference context is what you consent to when you use any AI coding tool: the files relevant to your prompt. A git object store is categorically different — it is the complete lineage of the repository since day one.
- Deleted secrets live in
.git/objects/forever. Every API key that was ever committed and later "removed" in a follow-up commit is still a readable blob in the object store. Rotating the secret in the working tree does nothing; history-based scanners (trufflehog, gitleaks) exist precisely because of this. - Unpushed branch names are product roadmaps. Local-only branches like
feature/audit-mode-ddos-v2orhotfix/whale-customer-outagereveal unreleased plans and which customers have problems. .git/configleaks infrastructure topology. Remote URLs, internal GitLab hostnames, repo paths — and any URL-embedded credentials if developers ever cloned with a token in the URL.- Reflogs (
.git/logs/) leak operational history — rebases, resets, amends — that was never intended to leave the workstation.
ferstar's repo totaled 10 GB; excluding dependencies left 345 MB that was, in their words, almost entirely core intellectual property. Multiply that by every developer with a commercial workspace open in the tool.
The Reconstructed Upload Pipeline
With no upload URLs in the logs, ferstar cracked open the Electron client's app.asar and reconstructed the full flow. The client does not ship files through Z.ai's own application servers — it posts the encrypted blob directly to Aliyun OSS using server-issued form credentials:
ZCode client (host-level sidecar) zcode.z.ai (coordinator) Aliyun OSS
───────────────────────────── ────────────────────────── ─────────────
1. captureBeforePrompt() ── tar.gz
entire workspace
(86.6% is .git/)
│
│ 2. POST /api/v1/snapshot/upload-credential
├────────────────────────────────────►
│ returns: snapshot_id, per-round RSA
│ public key, max_size, OSS form
│ signature (policy, x-oss-signature),
│ dynamic object key, callback token
│ ◄────────────────────────────────────
│
3. AES-256-CTR encrypt archive
with ephemeral symmetric key
4. RSA-OAEP-SHA256 wrap the key
(server-supplied public key —
private key NEVER on client)
│
│ 5. PostObject: tar.gz.enc (direct HTTPS form POST)
├───────────────────────────────────────────────────────────► OSS storage node
│ │
│ 6. OSS calls back → snapshot registered with Z.ai backend │
└──────────────────────────────────────────────────────────────┘
Runtime evidence: persistent HTTPS connections to zcode.z.ai
plus two Aliyun OSS nodes while the client is logged in.
Capture fires on two triggers — captureBeforePrompt (before every prompt) and on task completion tagged repo-wiki-update. A single active session in ferstar's logs produced 62 capture events. The state file's kind: "baseline" indicates the first snapshot is a full capture, with the retry counter ticking upward whenever the upload endpoint is unreachable.
The Part That Should End the "It's a Backup Feature" Debate
The encryption implementation is textbook envelope encryption — content encrypted with an ephemeral symmetric key (AES-256-CTR), and the symmetric key wrapped with RSA-OAEP-SHA256. The code references a server-provided publicKeySpkiPem and keyWrapAlgorithm: "rsa-oaep-sha256".
Server-held keys: the architecture is the confession
The RSA public key is delivered by the server during upload-credential negotiation, and the corresponding private key exists only in Z.ai's cloud. ferstar attempted to unwrap the archive with every private key on the local system and failed, as expected. The 313 MB ciphertext on your own disk cannot be decrypted by you, and it cannot be decrypted by the ZCode client itself.
The test that matters: any feature genuinely built for user-facing rollback or cross-device sync keeps decryption keys local — that is exactly how git bundles, Time Machine, restic, and every legitimate backup tool work. An archive only the vendor can open serves one purpose: ensuring the vendor can read your code whenever it wants. As ferstar put it: "a key that only the server can use serves exactly one purpose."
The Settings Toggles Do Not Stop It
The natural response — open settings, find the off switch — fails. ferstar cross-referenced the UI options against the decompiled code:
| UI toggle | What you expect it to do | What it actually controls |
|---|---|---|
Optimize ExperienceoptimizeAgentExperienceEnabled |
Disable telemetry / data collection | Only whether data is authorized for model training. Snapshot capture and upload continue. |
Repo Snapshot IndexingrepoSnapshotIndexingEnabled |
Disable the snapshot feature | Only whether the server indexes uploaded snapshots. Local packaging and upload continue. |
The host assembly instantiates the capture sidecar unconditionally at startup — there is no gating on user preferences at all; the only requirement is that the token provider can produce a valid JWT. Translation: logged in = pipeline active, and there is no setting that changes that.
Why the agent never asks permission — and never mentions it
This is the detail that separates the ZCode case from ordinary "agents upload code fragments during tool calls" whataboutism. A public corpus of captured AI harness prompts (OrcaPromptVault) holds ZCode's complete system prompt — 131 KB of instructions and a 31-tool surface. It contains zero snapshot, upload, or telemetry tools, and no mention of Aliyun, OSS, or uploads anywhere.
The exfiltration pipeline is not an agent tool. It is a host-level sidecar instantiated outside the tool loop, so it never appears in the agent's permission surface — which is exactly why no permission prompt fires and why the agent itself is unaware of it. Your developers can be scrupulous about approving tool calls and it changes nothing: the collection path bypasses the consent layer entirely.
The user-facing tip of this pipeline is the "checkpoint / rewind" feature (the leaked prompt template Workspace rewind applied. rewindId, checkpointId, strategy, restoredFiles appears five times) — the feature whose local artifacts the filesystem lock below disables.
What the Privacy Policy Says (and Doesn't)
ZCode's privacy policy discloses that it collects "text, files, and code submitted during conversations" — the standard inference-context disclosure every AI coding tool makes. Across the policy, FAQ, and changelogs, ferstar found no mention of packaging and uploading entire workspaces and full git histories. The closest statement is a template line about the optimization program being off by default and inputs not being used for training without consent — which is technically consistent with the toggle behavior above, and entirely silent about collection.
Tokenstead adds context that makes this worse rather than better: Z.ai positioned ZCode against Anthropic's Claude Code in July 2026, and per their reporting, a Z.ai executive asked on X whether ZCode would include "any sort of spyware" answered that the company would not implement anything beyond what's listed on the ZCode website. Workspace snapshotting is not listed on the ZCode website. As of publication, Z.ai's official channels had not responded to ferstar's post; the most visible reply from an account affiliated with the ZCode team — "hey I am sorry to let you find it" — reads as confirmation of the mechanism, not a rebuttal.
Why Your DLP Never Saw This (and Won't See the Next One)
Here is the part platform teams should internalize regardless of what they think of Z.ai specifically. The detection failure mode is architectural, not a tooling gap:
Traditional egress inspection What actually happens
──────────────────────────────── ───────────────────────────────────
Dev workstation Dev workstation
│ │
│ HTTPS (TLS-inspected at proxy) │ 1. tar.gz + AES-256-CTR encrypt
▼ │ BEFORE the socket is opened
[Corporate proxy / DLP] ▼
│ content inspection [Corporate proxy / DLP]
│ on plaintext body │ sees only ciphertext form-POST
▼ ▼
Allow or block Content inspection: nothing to match on
(payload is vendor-encrypted blob)
- Content inspection is dead on arrival. The payload is encrypted client-side with a vendor-held key. TLS interception at your proxy reveals an opaque
tar.gz.encform POST. Regexes for secrets, customer names, or PII match nothing. Your DLP investment contributed zero signal here. - Volume and destination become the only signals. A 313 MB POST to an object-storage endpoint outside your approved SaaS list is anomalous for a developer workstation. If you are not baselining per-host egress volume, you will not catch the next harness that does this — from any vendor, in any jurisdiction.
- The consent layer was bypassed by design. App-level permission systems (tool approvals, "Optimize Experience" toggles) are rendered moot when collection runs at the host layer below the agent. Any audit that stops at the app's own settings UI is auditing the wrong layer.
- Community verification is still mixed. In the HN thread, at least one long-time user reported finding no
~/.zcode/v2/checkpoints/directory or upload logs on their machine — so the behavior may be version-dependent or partially rolled out. That cuts both ways: it means you cannot assume your fleet is clean just because one workstation looks quiet, and treat any single-machine check as a sample, not an audit.
The Detection Playbook
Whether or not ZCode is in your environment, run these checks across your fleet for every AI harness — the same pattern applies to any Electron-based agent with filesystem access.
1. Find the on-disk artifacts
# Inventory every AI harness's data root and flag large encrypted blobs
for d in ~/.zcode ~/.cursor ~/.claude ~/.codeium ~/.windsurf ~/.continue; do
[ -d "$d" ] && du -sh "$d" && find "$d" -size +50M -type f -exec ls -lh {} \;
done
# Any *.enc / *.tar.gz.enc in a harness data root is a red flag worth explaining
find ~ -maxdepth 4 -name '*.enc' -size +10M 2>/dev/null
# Look for the retry-counter state files — the smoking gun in the ZCode case
find ~/.zcode -name '*.json' -path '*checkpoint*' -exec grep -l failureCount {} \; 2>/dev/null
2. Watch what the harness processes touch and where they connect
# Live egress sockets for every node/electron process on a workstation
sudo ss -tnp | grep -E 'node|electron|zcode|cursor'
# Auditd: record all reads/writes under a harness data root (add -p rwa)
sudo auditctl -w /home/dev/.zcode -p rwa -k ai_harness_watch
sudo ausearch -k ai_harness_watch --format json | \
jq -r '.[] | select(.record_type in ("PATH","SYSCALL")) | .data' | head -50
# Which destinations does the harness actually resolve? Watch DNS for a day
sudo tcpdump -i any -nn -l port 53 2>/dev/null | \
grep -iE 'z\.ai|aliyun|oss|amazonaws|storage' | sort -u
On Linux fleet workstations, auditd rules deployed via your configuration management of choice (Ansible, Puppet, or an eBPF-based agent) are the durable version of the manual check above. The key principle: watch the harness's data root and any large outbound POSTs, not the agent's tool-call logs — the tool loop is the layer vendors show you, and per this incident, it is not where the interesting traffic originates.
Containment: The Filesystem Lock That Actually Works
ferstar's forensics showed that deleting the pending archive is whack-a-mole — within thirty minutes the client re-packaged a fresh 313 MB snapshot and the retry counter ticked from 564 to 565. The fix that holds is kernel-level: make the checkpoints directory unwritable, so the capture sidecar fails at disk I/O and the upload pipeline has nothing to send. These commands are ferstar's published mitigation (reproduced from the primary source; run them on a machine you administer):
# Linux: wipe and lock the checkpoints directory rm -rf ~/.zcode/v2/checkpoints mkdir -p ~/.zcode/v2/checkpoints sudo chattr +i ~/.zcode/v2/checkpoints # Verify — should fail with "Operation not permitted" touch ~/.zcode/v2/checkpoints/test # macOS: same effect with file flags rm -rf ~/.zcode/v2/checkpoints mkdir -p ~/.zcode/v2/checkpoints chflags uchg ~/.zcode/v2/checkpoints # Verify touch ~/.zcode/v2/checkpoints/test # Rollback (restores checkpoint/rewind UI at the cost of re-enabling capture): sudo chattr -i ~/.zcode/v2/checkpoints # Linux chflags nouchg ~/.zcode/v2/checkpoints # macOS
The trade-off, stated honestly: the checkpoint/rewind UI stops working — a feature that (per the encryption analysis above) always required uploading your code to function. Chat, autocomplete, and tool calls work normally; the swallowed I/O errors in the logs are harmless. If your developers depend on rewind-style features, the filesystem lock is not a compromise you can sell them — the right answer is a different harness.
Assume Breach: What to Do About Repos That Were Exposed
If a commercial workspace was ever open in a logged-in ZCode session, assume the full git lineage — including deleted secrets and unpushed branches — left the building. Treat it as a credential and IP exposure event, not a settings problem:
# 1. Inventory what is actually in your history — most teams have never done this git log --all --full-history --oneline -- '*.env' '*secret*' '*credential*' '*token*' # 2. Scan every blob, not just the working tree, for verified live secrets trufflehog git file://./ --only-verified # (or: gitleaks git --redact -v .) # 3. Scrub a confirmed secret from history — a force-push rewrite, so coordinate first pip install git-filter-repo echo 'AKIAIOSFODNN7EXAMPLE==>***REMOVED***' > replacements.txt git filter-repo --replace-text replacements.txt # 4. Rotate anything that ever touched a commit — assume the object store leaked it # (secrets managers make this a metadata change; embedded keys make it a release)
The rotation step is the one teams skip and regret. Once a secret exists in .git/objects/, "we deleted it in a follow-up commit" is not a remediation — the blob is still there, and per this incident the entire object store may already be on someone else's infrastructure.
The Architectural Answer: Harnesses Belong Inside the Perimeter You Already Run
Point mitigations lose to architecture. The durable fix is to stop treating AI harnesses as chat apps and start treating them as untrusted, credentialed workloads:
Run AI harnesses inside dev containers, cloud workspaces, or ephemeral VMs — the same isolation you already apply to untrusted code. A harness with read access to a clone that contains only the branch being worked, with history-scrubbed mirrors for sensitive repos, turns an 86.6%-is-.git payload into a much smaller blast radius. Shallow clones (git clone --depth 1) and git clone --no-checkout + sparse-checkout deny the object store to the harness entirely.
Developer workstations need internet access; harness processes don't need object storage. Per-process allowlists (via proxy authentication tags, eBPF-based policy like Cilium/Tetragon on fleet-managed Linux, or MDM-driven firewalls) can deny AI-harness binaries access to storage endpoints while leaving browsers alone. Volume anomaly baselining — who POSTs >100 MB where — catches the vendors that rename their endpoints.
Before any AI harness is approved, require a vendor-signed network specification: every endpoint contacted, what is uploaded, encryption key custody, and a switch that demonstrably stops collection (verified by your own traffic capture, not the vendor's UI). "We collect text, files, and code submitted during conversations" is an inference disclosure, not a data-flow disclosure. Vendors that can't produce the spec haven't measured it — or have something to hide.
The community's reflex in the HN thread — "never use a harness that isn't open source" — is directionally right even if imperfect: open code doesn't guarantee good behavior, but it removes the app.asar-reverse-engineering requirement to find out. For teams standardizing on closed harnesses, the burden of proof shifts to you: intercept the traffic, because nobody else will.
Verdict and Who Should Skip
Skip ZCode — entirely, for now, not just "toggle the settings" — if you do commercial or regulated work on machines where it is installed. The disclosed data scope and the implemented data scope are different by a factor of the entire repository history; the encryption keys are structured so only the vendor can read what it collects; and the only working kill switch is an OS filesystem flag. Until Z.ai ships a disclosure change, a capture-off switch verified by independent capture, or client-held keys, treat every logged-in session on your fleet as a potential full-repo export.
The bigger takeaway: the AI coding tool you standardized on this quarter has filesystem access, a JWT, and an update channel — and your DLP sees its traffic as opaque ciphertext. The ZCode incident will not be the last of its kind; it is merely the first with a 42,411-file manifest attached. The teams that benefit from this news are the ones that add "AI harness egress audit" to their quarterly security review this week, before the next vendor makes the same discovery necessary.
Verification status: this article is based on ferstar's published forensics (Sep 18, 2026) and Tokenstead's corroborating analysis; the mechanism has not been independently confirmed by Platform Monkey, and at least one HN commenter reported not reproducing the artifacts on their own install. Z.ai had not issued an official response at publication time. We will update this page if the vendor responds with a fix, a disclosure change, or a rebuttal.