ZCode silently uploads full Git history to Aliyun OSS

ZCode silently uploads full Git history to the cloud

Takeaway: ZCode (Zhipu’s official AI coding desktop) automatically packages your entire workspace—including the complete .git history, LFS cache, reflogs, and global app configs—encrypts it with a server‑supplied RSA public key, and uploads the ciphertext directly to Aliyun OSS whenever you are logged in. The upload pipeline runs unconditionally, cannot be turned off through UI toggles, and the decryption key resides only on Zhipu’s backend, meaning you cannot decrypt the data locally.


Starting point: a 313 MB encrypted archive stuck in pending

  • The ~/.zcode directory (ZCode’s data root) occupied > 700 MB.
  • Inside ~/.zcode/v2/checkpoints/ a 313 MB file baseline.enc was found alongside metadata indicating it contained a 345 MB workspace snapshot and had failed 564 upload attempts.
  • The snapshot represented a commercial project whose full repository size was 10 GB; after excluding node_modules the packaged payload was 345 MB, i.e., almost entirely intellectual property.

Upload flow: client‑side packaging → direct OSS POST

The reverse‑engineered app.asar reveals a two‑stage pipeline:

  1. Credential request – the client POSTs to https://zcode.z.ai/api/v1/snapshot/upload-credential. The server returns:
    • a snapshot ID,
    • an RSA public key (used for envelope encryption),
    • a maximum size limit,
    • Aliyun OSS form credentials (policy, x-oss-signature), and
    • a dynamic object key.
  2. Direct upload – the client creates a tar.gz archive, encrypts it with AES‑256‑CTR, wraps the symmetric key with RSA‑OAEP‑SHA256 using the supplied public key, and POSTs the resulting *.enc file straight to Aliyun OSS. OSS then calls back to Zhipu’s backend to confirm receipt.

Network traces show persistent HTTPS connections to both zcode.z.ai and two Aliyun OSS nodes.

Encryption key belongs exclusively to the server

The implementation follows textbook envelope encryption:

keyId: "<version>",
keyWrapAlgorithm: "rsa-oaep-sha256",
publicKeySpkiPem: "<server‑provided PEM>"
  • Content is encrypted locally with an ephemeral AES‑256‑CTR key.
  • The AES key is wrapped with the RSA public key supplied by the server during the credential request.
  • The corresponding private key never appears on the client machine; attempts to unwrap the key locally fail.

Consequences:

  • The 313 MB ciphertext stored on disk cannot be decrypted by the user or the ZCode client.
  • Only Zhipu’s backend can decrypt the snapshot, effectively giving the service unrestricted read access to the entire repository history.

What is actually uploaded: 86 % .git

A manifest generated during packaging (saved in plaintext) lists 42 411 files. Size breakdown:

Path Size % of payload Notable content
.git/lfs/ 196.1 MB 56.8 % All large binary assets ever downloaded
.git/objects/ 102.2 MB 29.6 % Complete commit‑object store (commits, trees, blobs)
.git/logs/ 0.6 MB 0.2 % Reflogs, local branch history
Source code & docs ~46.2 MB 13.4 % src/, config files, documentation

Thus 86.6 % of the uploaded payload is raw Git data, exposing:

  • Historical API keys and secrets that were later removed.
  • Unpushed branch names revealing unreleased features.
  • .git/config entries containing internal hostnames and repository URLs.

An additional manifest (repo_snapshot_extra_manifest) hashes global ZCode configuration files and bundles them with every snapshot.

UI toggles do not stop the capture

Two settings appear in the UI:

Setting Intended effect Actual effect
Optimize Experience (optimizeAgentExperienceEnabled) Suppress telemetry / model‑training data Only disables sending data for model training; snapshot capture still runs
Repo Snapshot Indexing (repoSnapshotIndexingEnabled) Disable snapshot feature Only stops server‑side indexing of already‑uploaded snapshots; local packaging and upload continue

Code inspection shows the snapshot side‑car is instantiated unconditionally at startup. The only gating condition is a valid JWT token from tokenProvider. Capture triggers fire before every LLM prompt (captureBeforePrompt) and on task completion tagged repo-wiki-update. A single session can generate dozens of capture events.

Privacy policy omission

ZCode’s privacy policy states that it collects “text, files, and code submitted during conversations,” which is standard for LLM context. However, the policy, FAQs, and changelogs contain no mention of automatic full‑workspace packaging or Git‑history exfiltration. The only related clause is a generic note that “optimization program is off by default, and inputs will not be used for training without consent.”

Defensive measures: lock the checkpoints directory

Deleting the pending archive only triggers a fresh capture (the retry counter increments). The reliable mitigation is to make the ~/.zcode/v2/checkpoints directory immutable, preventing the client from writing new archives.

macOS

rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
chflags uchg ~/.zcode/v2/checkpoints   # set immutable flag
# Verify: touch should fail with "Operation not permitted"

Linux

rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
sudo chattr +i ~/.zcode/v2/checkpoints   # set immutable attribute
# Verify: touch should fail with "Operation not permitted"

Impact: The capture pipeline aborts when it cannot write the archive, so no data is uploaded. The trade‑off is loss of ZCode’s “checkpoint rollback / timeline” UI feature; normal chat, autocomplete, and tool execution remain functional. To restore, remove the immutable flag (chflags nouchg on macOS, chattr -i on Linux).


Community reaction and official response

  • Hacker News comments highlighted concerns about other AI agents reading dotfiles, the futility of sandboxing, and distrust of Chinese AI vendors.
  • Zhipu’s statement (screen‑shot linked in the comments) claimed the behavior stemmed from an early‑launch “codebase indexing” feature intended for local repo indexing and temporary Wiki generation. The statement asserted that uploaded data is destroyed immediately after Wiki generation and that the issue has been fixed, with an apology and a one‑week quota reset for users.
  • Critics note the discrepancy between the statement (temporary, destroyed data) and the technical evidence (persistent encrypted uploads to OSS with server‑held keys).

Bottom line

ZCode’s background process captures the entirety of a logged‑in user’s workspace—including full Git history—and uploads it encrypted to a third‑party cloud without any user‑controllable opt‑out. The encryption key resides only on Zhipu’s servers, meaning the service can decrypt the data at will. Users who cannot accept this level of data collection should lock the ZCode checkpoints directory (or uninstall the app) to prevent further exfiltration.

Sources

Related