揭秘 ZCode:你的 Git 历史正被静默上传至云端
我不是英语母语者,本文由 AI 翻译。
事情源于清理磁盘时的一次例行检查:~/.zcode 竟然占了 700 多 MB。断断续续深挖之后,我确认了一个相当离谱的事实:
只要你登录着,ZCode(智谱官方 AI 编程桌面应用)就会悄悄把你的整个工作区打包——包括完整的 .git 历史、LFS 资源缓存、reflog 和全局应用配置——加密后直接上传到阿里云 OSS。
更讽刺的是:加密用的 RSA 公钥是服务端实时下发的,私钥只存在云端。你自己磁盘上那几百 MB 的密文,你解不开,ZCode 客户端自己也解不开。
下面是完整的调查记录、证据链,以及一条能永久关掉它的命令。
起点:一个卡在待上传状态的 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
}
事情很简单:
- 客户端扫描了我正在做的商业项目,排除
node_modules等目录后,把剩余的 345MB 打包成一个 313MB 的加密压缩包,标记为baseline(全量快照); - 它记录了 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 公钥 + max_size + OSS 表单凭证 + 回调
C->>C: tar.gz 打包 → AES-256-CTR 加密 → RSA-OAEP 封装密钥
C->>O: 直传 tar.gz.enc 到 PostObject
O->>S: 回调确认接收
整个管道分为两个阶段:
- 向协调器请求凭证:客户端调用
https://zcode.z.ai(代码中对应VITE_ZCODE_ENDPOINT_ORIGIN)。服务器返回 OSS 表单签名(policy、x-oss-signature)、动态 Object Key、大小限制,以及本轮加密使用的 RSA 公钥; - 直接向 OSS 发送表单 POST:在本地完成归档和流式加密后,客户端绕过 ZCode 自身的应用服务器,通过 HTTP POST 表单将
tar.gz.enc直接上传到阿里云 OSS。随后 OSS 回调智谱后端以注册该快照。
通过检查活跃套接字确认了这一点:正在运行的 ZCode 进程维持着到 zcode.z.ai IP 端点的持久 HTTPS 连接,以及到两个阿里云 OSS 存储节点的连接。
最具讽刺意味的部分:密钥属于服务器
#该加密实现采用的是教科书式的信封加密:
keyId: String(i.encryption.key_version),
keyWrapAlgorithm: "rsa-oaep-sha256",
publicKeySpkiPem: Ylt(i.encryption.public_key)
- 使用临时对称密钥,通过 AES-256-CTR 加密内容;
- 使用该轮次由服务器提供的公钥,通过 RSA-OAEP-SHA256 封装对称密钥。
关键在于该公钥:它在凭证协商过程中由服务器下发,对应的私钥从未出现在你的机器上。如预期般,使用我系统上所有本地私钥尝试解封信封密钥均告失败。
被打包的内容:近 90% 是 .git #
虽然密文已被锁定,但打包过程中生成的 Manifest(文件清单)是以明文形式保存在本地的。对包含 42,411 个文件的快照进行拆解分析:
| 内容 | 大小 | 占比 | 包含的信息 |
|---|---|---|---|
.git/lfs/ |
196.1 MB | 56.8% | LFS 缓存 —— 曾下载过的所有二进制资产和大型媒体文件 |
.git/objects/ |
102.2 MB | 29.6% | 完整的提交历史对象库(包括 commits、trees、blobs) |
.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)进行哈希处理,并将其与每个快照跨工作区捆绑在一起。
开关的真相:UI 切换无法阻止上传 #
直觉反应是去设置里把它关掉。我将 UI 选项与代码库进行了交叉核对:
| 开关 | 你以为的作用 | 实际作用 |
|---|---|---|
Optimize Experience(optimizeAgentExperienceEnabled) |
关闭遥测/数据收集 | 只控制数据是否被授权用于模型训练。快照的采集和上传照常运行 |
Repo Snapshot Indexing(repoSnapshotIndexingEnabled) |
关闭快照功能 | 只控制服务端是否对上传的快照建立索引。本地打包和上传不受任何影响 |
看宿主的汇编代码就一目了然:采集/上传的 sidecar 在启动时是无条件实例化的。没有任何根据用户偏好做拦截的 if 检查,唯一的前提是 tokenProvider 能返回有效的 JWT。
结论:只要你是登录状态,这条后台流水线就一直处于活跃状态,任何界面设置都关不掉。
触发采集的时机有两处:captureBeforePrompt(每次 prompt 之前)和任务完成时带 repo-wiki-update 标记的时刻。会话日志显示,一个活跃会话最多产生了 62 次采集事件。
隐私政策是怎么写的 #
翻看 ZCode 的隐私政策,它明确写明会收集“对话中提交的文本、文件和代码”——这属于给 LLM 提供上下文的常规操作。
但通篇政策、FAQ 和更新日志里,对“静默打包并上传整个工作区和完整 Git 历史”这件事只字未提。
最接近的表述只有一句模板式声明:“优化计划默认关闭,未经同意输入内容不会用于训练”。
防御:手动删除是打地鼠,直接锁目录 #
第一次发现待上传的压缩包时,我直接把它删了。结果半小时内它又重新采集了一次——一个全新的 313MB 压缩包,重试计数从 564 涨到了 565。上传器发现文件没了,就重新打一个。手动删除纯属打地鼠。
最干净且高效的方案是在文件系统层面设置不可变标志,从内核级别拒绝写入权限:
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
影响与回滚 #
- 结果:每当捕获逻辑尝试进行磁盘 I/O 时,都会被内核阻断。由于没有本地工件,上传流水线也就无内容可发送;
- 代价:“检查点回滚 / 时间线” UI 功能将失效(该功能本就依赖于上传代码)。正常的聊天、自动补全及工具执行均不受影响。日志中被吞掉的 I/O 错误无害;
- 恢复方法:运行
chflags nouchg ~/.zcode/v2/checkpoints(macOS)或sudo chattr -i ~/.zcode/v2/checkpoints(Linux)。
结语 #
在使用 AI 工具时,模型推理必然需要代码上下文——这一点大家起初都能接受。但上述行为在两方面明显越界:
首先是数据范围。推理发送的是任务相关的上下文;而快照机制外泄的却是整个仓库以及多年的 Git 提交历史。
其次是架构立场。若这是真正为用户端恢复或同步而设计,解密密钥理应归用户所有。服务器独占加密密钥、隐私政策零披露、后台上传无法阻止、删除后顽强重新打包——这看起来不像备份,更像是数据收集。
工具只是工具,但用户必须自行划定边界。如果软件不允许你关闭它,就用操作系统内核把它关进笼子。