---
最初只是在清理磁盘空间时进行的一次例行检查:
~/.zcode 占用了超过 700MB 的空间。经过一番间歇性的深入探查,我确认了一个相当离谱的事实:只要你保持登录状态,ZCode(智谱官方的 AI 编码桌面端应用)就会在后台默默打包你的整个工作区——包括完整的 .git 历史记录、LFS 资源缓存、reflogs(引用日志)以及全局应用配置——对其进行加密,并直接上传到阿里云 OSS。
更具讽刺意味的是:用于加密的 RSA 公钥是由服务器动态下发的,而私钥则完全存放在云端。 你无法解密那些躺在你自己磁盘上的几百兆密文,ZCode 客户端自己也同样无法解密。
以下是完整的调查记录、证据链,以及一招从根本上永久阻止它的防御方法。
更新,2026-09-19
在本文发布后引发广泛的社区关注之际,Z.ai 于 9 月 18 日 17:44 在其用户社区发布了一份官方声明(IT之家等科技媒体也在当晚进行了报道)。本节提供一个客观的交叉比对,下文的原始调查内容保持不变。
官方回应要点
Z.ai 于 9 月 18 日 17:44 发布了声明,公开报道可参见IT之家。其核心要点包括:
* 该问题源于“代码库索引(codebase indexing)”,用于本地索引、会话检查点恢复(checkpoint restore)以及 Repo Wiki;
* 在云端生成 Wiki 页面“可能”会触发代码库数据的上传;
* Wiki 生成完毕后,上传的数据会立即销毁,不会存储;
* 该功能在早期发布阶段默认开启,部分用户受到影响,目前该问题“已修复”;
* ZCode 即将开源,并接受第三方审查;所有用户将获得一次每周配额重置的补偿。
上传行为的发生已不再被智谱否认。目前存疑的是:实际上传的范围究竟有多大、用户开关是否真的有效,以及外部人员该如何验证所谓“立即销毁”的真实性。
逆向工程与本地证据 vs. 官方声明(旧版 3.12.3 vs. 3.14.0 vs. 官方声明)
为了避免在不同客户端版本之间产生混淆,以下是受影响的旧版本(3.12.3、被抓包版本)、逆向分析的 3.14.0 版本以及官方声明之间的直接对比:
| 维度 | 官方声明(9月18日 17:44) | 3.12.3 客户端审计(受影响版本) | 3.14.0 客户端审计(修复后版本) |
| --- | --- | --- | --- |
| 发送了什么 | 仓库数据(用于生成 Wiki) | 全工作区快照,约 87% 为 .git 数据 —— 包括对象、LFS、reflogs | 上传流水线代码已被物理移除;仅保留本地检查点 |
| 触发机制 | Wiki 页面生成“可能”会触发上传 | 上传 Sidecar 进程随登录常驻;
captureBeforePrompt(每次提示词前)和 repo-wiki-update 无条件触发 | 上传 Sidecar 已被拆除,不再触发云端打包 || 能否关闭 | 未提及是否存在停止上传的开关 | 禁用“优化体验(Optimize Experience)”和“仓库快照索引(Repo Snapshot Indexing)”后,仍会打包并尝试直接上传至 OSS | 代码流水线已被物理移除 |
| 云端处理方式 | 生成后立即销毁 | 外部无法验证(且从逻辑上与所谓的“检查点恢复”功能相矛盾) | 云端
upload-credential 接口已被移除(返回 404) || 数据保留 | 声称 Wiki 生成后不存储数据 | 一个包含 538 个文件的公开仓库快照已被服务器接受;其保留与解密权限未作说明 | 现有的云端快照是否已被物理抹除,外部无法核实 |
原文章容易引起误读的一点是:那个 313MB 的商业项目实际上停留在
pending 状态(failureCount: 564),并未成功上传。但另一个较小的公开仓库工作区成功上传了:包含 538 个文件,压缩加密后约 15KB,服务器状态显示已接收。所以,“到底有没有数据离开本机”——答案是肯定的,至少那一个成功了。其他用户也在 Windows 上复现了相同的目录结构和状态文件,包括多个没有失败记录、看似已被服务器接收的小型工作区:NodeSeek。另一个本地交叉验证参考:silencestar 博客。
我在 9 月 18 日归档了其隐私政策。该页面显示最后更新于 2026 年 6 月 15 日,其中仍然没有提及全工作区快照或云端同步。
个人几点澄清
1. 那个 313MB 的商业项目到底有没有被上传?:明确地说,那个 313MB 的商业仓库快照因为超出了大小限制,失败了 564 次,一直卡在本地的
pending 目录中 —— 它从未成功上传过。我家里运行着一台 OpenWrt 路由器,检查连接跟踪和流量记录后确认,那些加密的数据块从未离开过本地网络。2. 绝对不是什么“逆向工程大神” —— 归功于我那乞丐版的 MacBook Air:网上有些人开始称我为“逆向工程专家”,这完全没必要。整个起因极其滑稽:由于苹果的存储空间价格贵得像黄金,我这台基础配置的 256GB MacBook Air 永远处于磁盘空间饥饿状态。我在清理空间时,注意到
~/.zcode 神秘地吞噬了 700 多 MB。我的工程师直觉开始报警,于是我解包了 app.asar 看看这到底是怎么回事(如果是在我另一台配有 2TB NVMe 且运行 Arch Linux 的机器上,区区几百兆我是根本不会眨一下眼的)。话又说回来,在现代 AI 的时代,任何带着编码代理(Coding Agent)的人实际上都成了一个逆向工程师 —— 随便抓一个像样的 Agent 扔进货架,把这篇文章和源码喂给它,它就能以无可挑剔的清晰度剖析整个架构。这算不上什么独家黑魔法,不过是基本的工程好奇心和排查罢了。3. 极具讽刺意味的是:我自己其实也是 GLM Coding Max 的长期订阅者和支持者。最搞笑的是,就在 9 月 17 日晚上,我还兴致勃勃地在一个开发者群里向同行安利 ZCode。然而不到半天时间,当我亲自抓到这个默默全仓库打包的例行程序时,现实狠狠地抽了我一巴掌。
这种缓解措施现在还重要吗?
重要。尽管 3.14.0 版本删除了相关代码并且网关路由返回 404,但桌面客户端仍然可以接收热更新。下文的文文件系统锁作为安全防线依然有效。NodeSeek 的帖子中也有 Windows 对应的 ACL 锁定方法。
仍然未解的疑问
1. 如何从外部证明“立即销毁”?云端现有的加密快照是否已被物理清除,谁拥有私钥的解密权限?
2. 所谓的“检查点恢复”与“立即销毁”相互矛盾 —— 云端究竟保留了什么?
3. 开源版本是否会包含被抓包的历史上传 Sidecar 代码,还是只包含最新清理过的提交?
如果该代码库真的开源了,我会针对源码写一篇后续文章。
---
起点:一个卡在 Pending 中的 313MB 归档文件
~/.zcode 是 ZCode 的数据根目录。其大小分布大致如下:*
cli/:约 257MB(会话数据库、执行日志)*
computer-use/:约 130MB(捆绑的应用和运行时依赖)*
v2/checkpoints/:约 303MB(主要嫌疑对象)在
v2/checkpoints/ 内部,我发现了一个 313MB 的 .enc 文件以及一个状态元数据文件:
{
"workspacePath": "/Users/ferstar/myprojects/<a commercial project>",
"lastCompressedSize": {
"encryptedSizeBytes": 313070842,
"workspaceSizeBytes": 345549173
},
"kind": "baseline",
"failureCount": 564
}
事情的来龙去脉很清晰:
1. 客户端扫描了我活跃的商业项目,排除了
node_modules 等目录,将剩下的 345MB 打包成了一个标记为 baseline(全量快照)的 313MB 加密归档文件;2. 它记录了 564 次失败的上传尝试,并将其留在了本地的
pending/ 目录中,等待下一次重试。该仓库总大小为 10GB;减去依赖项后,剩下的 345MB 几乎全部是核心知识产权。
---
去向:从日志到 asar 逆向工程
日志中没有包含明确的上传 URL,因此我拆开了客户端的
app.asar。其逆向还原出的上传流程如下:
sequenceDiagram
participant C as ZCode 客户端
participant S as zcode.z.ai
participant O as 阿里云 OSS
C->>S: POST /api/v1/snapshot/upload-credential
S-->>C: snapshot_id + RSA 公钥 + 最大限制 + OSS 表单凭证 + 回调
C->>C: tar.gz 打包 → AES-256-CTR 加密 → RSA-OAEP 封装密钥
C->>O: PostObject 直接表单直传 tar.gz.enc
O->>S: 回调确认接收
整个流水线分两阶段运行:
1. 向协调服务器请求凭证:客户端调用
[https://zcode.z.ai](https://zcode.z.ai)(代码中的 VITE_ZCODE_ENDPOINT_ORIGIN)。服务器返回 OSS 表单签名(policy、x-oss-signature)、动态 Object Key、大小限制,以及本轮加密所需的 RSA 公钥;2. 直接向 OSS 发起表单 POST:在本地完成归档和流式加密后,客户端绕过 ZCode 自身的应用服务器,通过 HTTP POST 表单将 tar.gz.enc 直接上传到阿里云 OSS。随后 OSS 会回调智谱的后端以注册该快照。
检查活动套接字证实了这一点:运行中的 ZCode 进程与
zcode.z.ai 的 IP 端点以及两个阿里云 OSS 存储节点保持着持久的 HTTPS 连接。---
最具讽刺意味的部分:密钥属于服务器
其加密实现采用了教科书式的数字信封(Envelope Encryption)技术:
keyId: String(i.encryption.key_version),
keyWrapAlgorithm: "rsa-oaep-sha256",
publicKeySpkiPem: Ylt(i.encryption.public_key)
* 内容使用对称密钥通过 AES-256-CTR 进行加密;
* 对称密钥则使用服务器提供的公钥通过 RSA-OAEP-SHA256 进行包裹(封装)。
最致命的关键点在于那个公钥:它是服务器在凭证协商期间下发的,而对应的私钥永远不会触及你的电脑。 正如预期的那样,尝试用我系统上所有的本地私钥去解包这个信封密钥均告失败。
换句话说:你驱动器上的那个 313MB 密文,无论你还是客户端都无法打开。只有智谱的后端才持有能够解锁它的钥匙。
如果这个功能真的是为了用户侧的回滚或多端同步而构建的,解密密钥理应存放在本地(就像 Git 或时间机器 Time Machine 一样)。一个只有服务器才能使用的密钥,其目的只有一个:确保服务器能够在任何它想的时候读取你的代码。
---
打包了什么:近 90% 是 .git
尽管密文被锁死,但在打包过程中生成的清单(Manifest,即文件清单)却以明文形式保存在本地。对一个包含 42,411 个文件的快照进行拆解后发现:
| 内容 | 大小 | 占比 | 包含的信息 |
| --- | --- | --- | --- |
|
.git/lfs/ | 196.1 MB | 56.8% | LFS 缓存 —— 曾经下载过的所有二进制资产和大型媒体文件 ||
.git/objects/ | 102.2 MB | 29.6% | 完整的提交历史对象存储(提交、树、二进制对象) ||
.git/logs/ | 0.6 MB | 0.2% | reflogs —— 本地分支历史和未推送的操作轨迹 || 源码与文档 | 约 46.2 MB | 13.4% |
src/、配置文件、内部文档 |仅
.git 目录就占了 payload 的 86.6%。一旦上传,云端接收到的远不止你当前的工作树 —— 它获得了你自第一天起整个仓库的完整世系:
* 在后续提交中已被删除的历史 API 密钥和敏感配置;
* 未推送的本地分支名称(会暴露尚未发布的功能计划);
* 配置在
.git/config 中的内部 GitLab 主机名和仓库路径。此外,一个名为
repo_snapshot_extra_manifest 的额外清单会为你的全局 ZCode 配置文件(如 settings.behavior.json)生成哈希,并在每次快照时跨工作区进行捆绑上传。---
开关的真相:UI 开关根本无法阻止它
自然的反应是检查设置并将其关闭。我将 UI 选项与代码库进行了交叉比对:
| 开关 | 你期望它做什么 | 它实际做了什么 |
| --- | --- | --- |
| 优化体验(Optimize Experience)(
optimizeAgentExperienceEnabled) | 禁用遥测 / 数据收集 | 仅控制数据是否被授权用于模型训练。快照的捕获和上传仍在继续 || 仓库快照索引(Repo Snapshot Indexing)(
repoSnapshotIndexingEnabled) | 禁用快照功能 | 仅控制服务器是否对已上传的快照进行索引。本地打包和上传毫无阻碍地继续进行 |查看宿主汇编代码可以发现一清二楚:捕获/上传 Sidecar 是在启动时无条件实例化的。代码中没有任何对用户偏好设置的
if 条件门控;唯一的先决条件是 tokenProvider 能够返回一个有效的 JWT。底线:只要你保持登录状态,这个后台流水线就会永久处于活跃状态,任何 UI 设置都无法将其关闭。
捕获触发点有两个:
captureBeforePrompt(在每个提示词之前)以及在标记有 repo-wiki-update 的任务完成时。在会话日志中,单个活跃会话最多生成了 62 个捕获事件。---
隐私政策说了什么
查阅 ZCode 的隐私政策,其中明确规定它收集“对话期间提交的文本、文件和代码”——这对于向大模型提供上下文来说是标准做法。
然而,在整个政策、常见问题解答(FAQ)和更新日志中,没有一处提到会默默打包并上传整个工作区以及完整的 Git 历史记录。
最接近的表述是模板化的通用声明:“优化计划默认关闭,未经同意输入不会用于训练”。
---
防御:删除是在打地鼠,直接锁死目录
当我第一次发现这个 pending 中的包时,我直接删除了它。但在半小时内,它又重新捕获了 —— 一个崭新的 313MB 归档文件诞生了,重试计数器从 564 变为了 565。当上传程序发现文件消失时,它就会重新打包一个。手动删除纯属打地鼠。
最干净、最有效的解决方案是在文件系统级别设置不可变标志(Immutability Flag),从内核层拒绝写权限:
macOS
# 清空并锁定 checkpoints 目录
rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
chflags uchg ~/.zcode/v2/checkpoints
# 验证:应当输出 "Operation not permitted"
touch ~/.zcode/v2/checkpoints/test
Linux
# 清空并锁定 checkpoints 目录
rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
sudo chattr +i ~/.zcode/v2/checkpoints
# 验证:应当输出 "Operation not permitted"
touch ~/.zcode/v2/checkpoints/test
影响与回滚
* 结果:每当捕获逻辑试图进行磁盘 I/O 时,都会被内核拦截阻止。由于没有本地产物,上传流水线将无东西可发;
* 代价:“检查点回滚 / 时间线” UI 功能将无法使用(该功能本身就需要先上传你的代码)。正常的聊天、自动补全和工具执行均可正常工作。日志中被吞掉的 I/O 错误是无害的;
* 恢复方法:运行
chflags nouchg ~/.zcode/v2/checkpoints(macOS)或 sudo chattr -i ~/.zcode/v2/checkpoints(Linux)。---
结语
在使用 AI 工具时,模型推理不可避免地需要代码上下文 —— 这一点大家在开始使用时都是默认接受的。但这种行为在两个方面明显越过了红线:
首先是数据范围。推理发送的是任务相关的上下文;而快照却将整个仓库连同多年的 Git 提交历史一并窃取。
其次是架构姿态。如果这真的是为用户侧的恢复或同步而设计的,那么解密密钥理应属于用户。一个由服务器独占的加密密钥、隐私政策中的闭口不提、无法停止的后台上传,以及删除后顽固的重新打包 —— 这看起来不像是备份,倒更像是收集。
工具终究是工具,但用户必须划定自己的底线。如果软件不让你关闭它,那就用操作系统内核把它关进笼子里。
> 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.
Update, 2026-09-19
Following widespread community attention after this post, Z.ai released an official statement in their user community on Sep 18 at 17:44 (with tech media like IT Home picking up the story later that evening). This section provides an objective cross-check. The original investigation below remains unchanged.
What the company said
Z.ai posted their statement on Sep 18 at 17:44; public coverage can be found on IT Home. Key points:
- The issue came from "codebase indexing", used for local indexes, session checkpoint restore, and Repo Wiki;
- Generating Wiki pages in the cloud "may" trigger an upload of repository data;
- After the Wiki is generated, the uploaded data is destroyed immediately and is not stored;
- The feature was on by default in its early launch period; some users were affected; the issue "has been fixed";
- ZCode will be open-sourced soon, with third-party review; all users get one extra weekly quota reset.
The fact that uploads occurred is no longer contested by Zhipu. What remains in question is the actual upload scope, user toggles, and how anyone outside the company is supposed to verify "destroyed immediately".
Reverse Engineering & Local Evidence vs. Official Claims (Old Version 3.12.3 vs. 3.14.0 vs. Official Statement)
To prevent confusion between client versions, here is a direct comparison between the affected old version (3.12.3) when caught, the reverse-engineered 3.14.0 release, and the official statement:
| Dimension | Official Statement (Sep 18 17:44) | 3.12.3 Client Audit (Affected Version) | 3.14.0 Client Audit (Remediated Version) |
|---|---|---|---|
| What was sent | Repository data (for Wiki) | Full-workspace snapshots, ~87%
.git — objects, LFS, reflogs | Upload pipeline code physically stripped; only local checkpoints remain || Trigger mechanism | Wiki page generation "may" upload | Upload sidecar resident with login;
captureBeforePrompt (before every prompt) and repo-wiki-update trigger unconditionally | Upload sidecar dismantled; no longer triggers cloud packaging || Can you turn it off | No mention of a switch to stop uploads | Disabling "Optimize Experience" and "Repo Snapshot Indexing" still packaged and attempted direct OSS uploads | Code pipeline physically removed |
| Cloud-side handling | Destroyed immediately after generation | Not verifiable from outside (and logically contradicts the claimed "checkpoint restore" feature) | Cloud
upload-credential endpoint pulled (returns 404) || Retained data | Claims data is not stored post-Wiki | Snapshot of a 538-file public repo accepted by server; retention/decryption rights unaddressed | Whether existing cloud snapshots were physically wiped cannot be verified externally |
One thing the original post left easy to misread: the 313MB commercial project sat in
pending (failureCount: 564) and did not upload successfully. A separate, tiny public-repo workspace did: 538 files, about 15KB after compression and encryption, status accepted by the server. So "did anything actually leave the machine" — yes, at least that one.Someone else reproduced the same directory layout and state files on Windows, including multiple small workspaces with no failure record that look accepted: NodeSeek. Another local cross-check: silencestar.
I archived the privacy policy on Sep 18. The page still said it was last updated 2026-06-15, and still did not mention full-workspace snapshots or cloud sync.
A Few Personal Clarifications
1. Did the 313MB commercial project actually get uploaded?: To be 100% clear, that 313MB commercial repo snapshot failed 564 times because it exceeded size limits, remaining stuck in local
pending — it was never successfully uploaded. I run an OpenWrt router at home; checking connection tracking and traffic flow records confirmed those encrypted chunks never left the local network.2. Definitely not a "reverse engineering wizard" — credit goes to my base-spec MBA: Some people online started calling me a "reverse engineering expert," which is completely unnecessary. The entire trigger was laughably mundane: thanks to Apple's storage being priced like solid gold, my base-spec 256GB MacBook Air is perpetually starved for disk space. I was freeing up space when I noticed
~/.zcode mysteriously devouring over 700MB. My engineering spider-sense tingled, so I unpacked app.asar to see what the hell was going on (on my other machine with a 2TB NVMe running Arch Linux, I wouldn't have blinked twice at a measly few hundred megs). Besides, given the state of modern AI, anyone with a coding agent is effectively a reverse engineer now — pull any decent agent off the shelf, feed it this post and the source files, and it'll break down the entire architecture with flawless clarity. It’s hardly some exclusive black magic; it was just basic engineering curiosity and troubleshooting.3. The bitter irony: I was actually a long-term subscriber and supporter of GLM Coding Max myself. The funniest part is that on the evening of Sep 17, I was enthusiastically pitching ZCode to peers in a developer group chat. Less than half a day later, reality hit me right in the face when I caught this silent whole-repo packaging routine myself.
Does the mitigation still matter
Yes. Even though 3.14.0 removed the code and the gateway route returns 404, the desktop client can still receive hot updates. The filesystem lock below remains active as a tripwire. The NodeSeek post has the Windows ACL equivalent.
Still unanswered
1. How do you prove "destroyed immediately" from the outside? Have existing cloud-stored encrypted snapshots been physically purged, and who holds private key decryption rights?
2. The claimed "checkpoint restore" contradicts "destroyed immediately" — what exactly was retained in the cloud?
3. Will the open-source drop include the historical upload sidecar that was caught, or only the latest sanitized commit?
If the repo actually ships, I will write a follow-up against the source.
The Starting Point: A 313MB Archive Stuck in Pending
~/.zcode is the data root of ZCode. The size breakdown looked roughly like this:-
cli/: ~257MB (session databases, execution logs)-
computer-use/: ~130MB (bundled app and runtime dependencies)-
v2/checkpoints/: ~303MB (the main suspect)Inside
v2/checkpoints/, I found a 313MB .enc file alongside a state metadata file:
{
"workspacePath": "/Users/ferstar/myprojects/<a commercial project>",
"lastCompressedSize": {
"encryptedSizeBytes": 313070842,
"workspaceSizeBytes": 345549173
},
"kind": "baseline",
"failureCount": 564
}
The story was straightforward:
1. The client scanned my active commercial project, excluded
node_modules and a few others, and packaged the remaining 345MB into a 313MB encrypted archive labeled baseline (full snapshot);2. It recorded 564 failed upload attempts, leaving it sitting in the local
pending/ directory waiting for the next retry.The repository totaled 10GB; minus dependencies, the remaining 345MB was almost entirely core intellectual property.
Where It Goes: From Logs to asar Reverse Engineering
The logs contained no explicit upload URLs, so I cracked open the client's
app.asar. The reconstructed upload flow:
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 credentials + callback
C->>C: tar.gz pack → AES-256-CTR encrypt → RSA-OAEP wrap key
C->>O: PostObject direct upload of tar.gz.enc
O->>S: callback confirms receipt
The pipeline runs in two stages:
1. Request credentials from coordinator: The client calls
https://zcode.z.ai (VITE_ZCODE_ENDPOINT_ORIGIN in code). The server returns OSS form signatures (policy, x-oss-signature), a dynamic Object Key, size limits, and the RSA public key for this encryption round;2. Direct form POST to OSS: After archiving and streaming encryption locally, the client bypasses ZCode's own application servers and posts
tar.gz.enc directly to Aliyun OSS via an HTTP POST form. OSS then calls back to Zhipu's backend to register the snapshot.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 Most Ironic Part: The Key Belongs to the Server
The encryption implementation uses textbook envelope encryption:
keyId: String(i.encryption.key_version),
keyWrapAlgorithm: "rsa-oaep-sha256",
publicKeySpkiPem: Ylt(i.encryption.public_key)
- Content is encrypted using an ephemeral symmetric key via AES-256-CTR;
- The symmetric key is wrapped using RSA-OAEP-SHA256 with the public key supplied by the server.
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.
What Gets Packed: Nearly 90% Is .git
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:
| Content | Size | Proportion | Information Contained |
|---|---|---|---|
|
.git/lfs/ | 196.1 MB | 56.8% | LFS cache — all binary assets and large media ever downloaded ||
.git/objects/ | 102.2 MB | 29.6% | Complete commit history object store (commits, trees, blobs) ||
.git/logs/ | 0.6 MB | 0.2% | reflogs — local branch history and unpushed operational traces || Source code & docs | ~46.2 MB | 13.4% |
src/, config files, internal documentation |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:
- Historical API keys and sensitive configs that were deleted in later commits;
- Unpushed local branch names (which reveal unreleased feature plans);
- Internal GitLab hostnames and repository paths configured in
.git/config.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 Truth About the Switches: UI Toggles Don't Stop It
The natural reaction is checking settings to toggle it off. I cross-referenced the UI options with the codebase:
| Switch | What you expect it to do | What it actually does |
|---|---|---|
| Optimize Experience (
optimizeAgentExperienceEnabled) | Disables telemetry / data collection | Only controls whether data is authorized for model training. Snapshot capture and upload still run || Repo Snapshot Indexing (
repoSnapshotIndexingEnabled) | Disables the snapshot feature | Only controls whether the server indexes uploaded snapshots. Local packaging and upload continue uninterrupted |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.What the Privacy Policy Says
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".
Defense: Deleting Is Whack-a-Mole; Lock the Directory
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:
macOS
# Wipe 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
# Wipe 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 & Rollback
- Result: The capture logic gets blocked by the kernel whenever it attempts disk I/O. Without local artifacts, the upload pipeline has nothing to send;- Trade-off: The "checkpoint rollback / timeline" UI feature won't work (which always required uploading your code in the first place). Normal chat, autocomplete, and tool executions work without issue. Swallowed I/O errors in logs are harmless;
- To restore: Run
chflags nouchg ~/.zcode/v2/checkpoints (macOS) or sudo chattr -i ~/.zcode/v2/checkpoints (Linux).Closing Thoughts
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.
Source: https://blog.ferstar.org/en/posts/zcode-silent-workspace-snapshot-upload/