一次常规的磁盘清理,牵出了 ZCode 后台数据管道的问题。ZCode 是智谱推出的官方 AI 编码桌面应用;有技术用户在其本地数据目录中发现,只要保持登录,客户端会在后台扫描整个工作区,把完整的 .git 历史、LFS 资产缓存、reflogs 以及全局应用配置一并打包、加密,并直接上传到 Aliyun OSS。更令人在意的是,加密所用 RSA 公钥由服务器在请求时下发,本地磁盘上留下的数百 MB 密文既无法由用户解密,也无法由 ZCode 客户端自身解密。
线索起点:pending 目录中的 313MB 加密包
~/.zcode 是 ZCode 的数据根目录。用户清理磁盘时,各部分体积大致为:会话数据库与执行日志约 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
}
元数据指向一个商业项目路径。结合日志可还原出基本过程:客户端扫描该项目,排除了 node_modules 和少量其他目录;上传失败被记录 564 次,加密包一直留在本地 pending/ 目录等待重试。该仓库总量约 10GB,而被打包的 345MB 几乎全部是核心代码资产。
从日志到逆向:上传被重建为两段式流程
进一步解开客户端的 app.asar 后,上传链路被还原为如下流程:
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
整个管道分为两个阶段:
- 向协调方请求凭证:客户端请求编码为 VITE_ZCODE_ENDPOINT_ORIGIN 的 https://zcode.z.ai,服务器返回 OSS 表单签名(policy、x-oss-signature)、动态对象键、大小限制,以及本轮加密使用的 RSA 公钥;
- 直接表单 POST 到 OSS:客户端在本地完成归档与加密后,绕过 ZCode 自身应用服务器,通过 HTTP POST 表单把 tar.gz.enc 直接发送到 Aliyun OSS;OSS 随后回调智谱后端,确认快照已经收到。
活动 socket 也印证了这条路径:运行中的 ZCode 进程维持着到 zcode.z.ai IP 端点的持续 HTTPS 连接,同时还连接两个 Aliyun OSS 存储节点。
关键点:私钥只在服务器侧
逆向结果显示,加密实现采用了“信封加密”结构:
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 的做法。私钥完全由服务器持有,意味着服务器在需要时可以读取这部分代码数据。
被打包的内容:.git 占有效载荷 86.6%
加密文本虽无法读取,但打包过程中生成的文件清单 manifest 以纯文本留在本地。对一个包含 42,411 个文件的快照进行拆解后,内容占比如下:
| 内容 | 体积 | 占比 | 包含信息 |
|---|---|---|---|
| .git/lfs/ | 196.1 MB | 56.8% | 所有已下载的二进制资产和大型媒体 |
| .git/objects/ | 102.2 MB | 29.6% | 完整提交历史对象存储(commit、tree、blob) |
| .git/logs/ | 0.6 MB | 0.2% | reflogs、本地分支历史与未推送操作痕迹 |
| 源代码和文档 | 约 46.2 MB | 13.4% | src/、配置文件、内部文档 |
仅 .git 目录就占有效载荷的 86.6%。上传完成后,云端拿到的并不只是当前工作树,而是从项目第一天起的完整仓库谱系,包括后续提交中已删除的历史 API 密钥和敏感配置、未公开的本地分支名称,以及 .git/config 中记录的内部 GitLab 主机名和仓库路径。
另一个名为 repo_snapshot_extra_manifest 的附加表达式,会对全局 ZCode 配置文件(如 settings.behavior.json)做哈希,并随每次快照与工作区内容一并打包。
界面开关不能真正关闭上传
发现问题后的自然动作是进入设置关闭相关选项。但把界面开关与代码库交叉比对后,结果并不符合用户预期:
| 开关 | 用户预期 | 实际作用 |
|---|---|---|
| 优化体验(optimizeAgentExperience) | 禁用远程测量/数据收集 | 仅控制数据是否被授权用于模型训练;截图与上传仍在运行 |
| 备份快照索引 | 禁用快照功能 | 仅控制服务器是否索引已上传快照;本地打包与上传仍持续进行 |
主机组件代码显示,capture/upload 侧车在启动时被无条件实例化;代码中不存在根据用户偏好整体关闭该管道的检查。唯一前提是 token provider 能返回有效 JWT。
结论很直接:只要处于登录状态,这条后台管道就持续活跃,界面设置无法将其关闭。
触发点主要有两类:captureBeforePrompt(每次提示词之前)以及任务完成时以 repo-wiki-update 标记触发。会话日志显示,单个活跃会话最多产生 62 次 capture 事件。
隐私政策如何表述
查看 ZCode 隐私政策,其明确写到会收集“对话期间提交的文本、文件和代码”,这属于向 LLM 提供上下文时的常见表述。但在整份政策、FAQ 与更新日志中,均未明确提到会静默打包并上传整个工作区和完整 Git 历史。
最接近的表述是通用模板话术:“优化计划默认关闭,未经同意输入不会用于训练”。这与实际发现的整仓快照上传并不是同一层面的说明。
防御:删除只是打地鼠,应锁死目录
最初发现 pending 加密包时,直接删除似乎是最快办法。但半小时内,客户端重新捕获了一份新的 313MB 归档,重试计数从 564 增加到 565。上传侧发现文件消失后,只会重新打包一份。手工删除因此只是打地鼠。
更彻底的办法是在文件系统层面设置不可变标志,在内核级拒绝写入:
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
影响与恢复
- 结果:capture 逻辑一旦尝试磁盘 I/O,就会被内核拦截;本地没有产物,上传管道也就没有可发送内容;
- 代价:“检查点回滚 / 时间线”界面功能将不可用,而该功能本来就以上传代码为前提;普通聊天、自动补全和工具执行不受影响;日志中被吞掉的 I/O 错误没有实质影响;
- 恢复方法:macOS 执行
chflags nouchg ~/.zcode/v2/checkpoints;Linux 执行sudo chattr -i ~/.zcode/v2/checkpoints。
结语
使用 AI 工具时,模型推理不可避免需要代码上下文,这一点用户在使用前通常可以接受。但本次发现的争议集中在两处。
第一是数据范围。推理过程发送的是与任务相关的上下文;而快照式采集上传的是整个仓库,以及多年积累的 Git 提交历史。
第二是架构姿态。若设计初衷真是面向用户的恢复或同步,解密密钥应归属用户。私钥完全由服务器持有、隐私政策零披露、后台上传无法停止、删除后仍顽固重打包,这些特征组合起来,更接近“采集”,而非用户可控的备份。
工具终归是工具,边界需要用户自己划定。当软件不提供真正关闭入口时,操作系统内核仍可把这条管道锁进笼子。





