你的 AI 编程助手,正在把整个 Git 历史打包送给服务器——而且钥匙只有它有
目录
- 一个 313MB 的加密文件,躺在你的硬盘上
- 86.6% 是 .git:它打包的不是代码,是底裤
- 那把钥匙:只有服务器能解开
- 两个开关,一个都管不住它
- 隐私政策:只字未提
- 现场验证:我亲手把「删除」和「锁死」都跑了一遍
- 同样的工具,两种姿态
先说结论:如果你在登录状态下用过 ZCode(智谱官方的 AI 编程桌面端),你的整个工作区——包括完整的 .git 历史、LFS 大文件缓存、reflog、全局配置——都曾被静默打包加密,上传到阿里云 OSS。而解开那份密文的钥匙,只有智谱的服务器有。
这不是我瞎猜。9 月 18 日,一位叫 ferstar 的开发者把完整的排查过程、证据链和防御方案都写了出来。他本来只是清理磁盘时看了一眼 ~/.zcode 为什么占了 700 多 MB。
一个 313MB 的加密文件,躺在你的硬盘上
~/.zcode 是 ZCode 的本地数据根目录。ferstar 看了一眼体积分布:
cli/:约 257MB(本地会话数据库、执行日志)computer-use/:约 130MB(应用本体和运行依赖)v2/checkpoints/:约 303MB —— 主角
点进 v2/checkpoints/,里面躺着一个 313MB 的 .enc 加密文件,旁边是一份状态文件:
{
"workspacePath": "/Users/ferstar/myprojects/<一个商业项目>",
"lastCompressedSize": {
"encryptedSizeBytes": 313070842,
"workspaceSizeBytes": 345549173
},
"kind": "baseline",
"failureCount": 564
}
意思很直白:客户端扫描了他本地打开的商业项目,排除 node_modules 等少量目录后,把剩下的 345MB 内容打包加密成 313MB 压缩包,标记为 baseline(全量快照)。然后尝试上传,失败了 564 次——所以一直卡在本地 pending/ 目录里等着重试。
整个项目 10GB,排除依赖后剩下的 345MB 几乎全是核心资产。
顺着日志和客户端 app.asar 逆向,上传链路被完整还原:
- 客户端请求
zcode.z.ai要上传凭证(POST /api/v1/snapshot/upload-credential) - 服务器返回:OSS 表单签名、动态 Object Key、大小限制——以及本次加密用的 RSA 公钥
- 客户端本地
tar.gz打包 → AES-256-CTR 加密 → RSA-OAEP 包裹密钥 - 不走智谱业务服务器,直接 HTTP POST 表单把
tar.gz.enc甩给阿里云 OSS - OSS 回调通知智谱后端登记
抓包还发现:ZCode 进程常驻的 HTTPS 连接,刚好就是 zcode.z.ai 的解析 IP 外加两个阿里云 OSS 节点。不是一次性的,是常驻的。
86.6% 是 .git:它打包的不是代码,是底裤
密文解不开,但快照生成时留下的 Manifest(文件清单)是明文写在本地的。这份 42,411 个文件的清单,把 ZCode 到底打包了什么看得清清楚楚:
| 内容 | 体积 | 占比 | 包含的信息 |
|---|---|---|---|
.git/lfs/ |
196.1 MB | 56.8% | LFS 缓存:项目历史下拉过的所有大文件和二进制资产 |
.git/objects/ |
102.2 MB | 29.6% | 完整 Git 历史对象库(Commit、Tree、Blob) |
.git/logs/ |
0.6 MB | 0.2% | reflog:本地所有分支操作和未推送记录 |
| 其余源码与文档 | ~46.2 MB | 13.4% | src/、各类配置文件和业务代码 |
.git 一个目录占了整整 86.6%。
这意味着只要这个包传上去,云端拿到的绝不只是你当前工作区的代码,而是这个仓库自打创建以来的全部历史底裤:
- 早就在后来的提交里「删掉」的敏感配置和历史 key——删掉只是工作区看不见,
.git里全在; - 本地还没推远端的分支名——直接暴露业务未公开的研发动向;
.git/config里配的内部自建 GitLab 域名和仓库路径。
另外还有一个 repo_snapshot_extra_manifest,会把 ZCode 全局配置文件(比如 settings.behavior.json)算完哈希,跨工作区打包,随每次快照一起传。
那把钥匙:只有服务器能解开
这是整件事里最讽刺的部分。
客户端用的加密是标准的信封加密:内容用随机对称密钥走 AES-256-CTR,对称密钥再用 RSA-OAEP-SHA256 包裹。包裹用的公钥,是服务器在下发凭证时动态给的——私钥从头到尾只在云端。
ferstar 拿本机所有私钥去解 envelope 里的密钥,毫无悬念全部失败。
翻译一下:你硬盘上那份 313MB 的密文,你打不开,ZCode 客户端自己也打不开,全天下只有智谱后端的私钥能解。
如果这功能真是为了给用户做断点恢复或跨设备同步,密钥理应绑在本地——就像 Git 或 Time Machine 那样。一把只有服务端能解开的钥匙,目的只有一个:确保服务端单方面能看。
两个开关,一个都管不住它
很多人第一反应是去设置里找开关关掉。ferstar 把设置项和代码逻辑逐个对了一遍:
| 开关 | 你以为它管 | 实际管 |
|---|---|---|
优化体验(optimizeAgentExperienceEnabled) |
数据采集 / 遥测上传 | 只管要不要拿你的数据去训练模型。关了照样抓快照上传 |
仓库快照索引(repoSnapshotIndexingEnabled) |
快照功能本身 | 只管服务端拿了快照后要不要建索引。关了本地打包上传一点不落 |
代码里更直白:负责快照捕获和上传的 sidecar 在启动时无条件实例化,没有任何针对用户配置的 if 判断,唯一要求是 tokenProvider 能拿到登录后的 JWT。
一句话:只要你登录了账号,这个上传机制就是常开的,UI 里没有任何开关能关掉它。
抓取触发点有两个:captureBeforePrompt(每次发 Prompt 前)、任务结束时标记 repo-wiki-update。翻看日志,单个活跃会话里最多能产生 62 次快照捕获。
隐私政策:只字未提
ZCode 的隐私政策里写了会收集「对话中提交的文本、文件和代码」——这是 AI 助手调模型推理的常规操作,各家都一样。
但通篇只字未提会把整个工作区连同完整 Git 历史静默打包上传。官方文档、FAQ、更新日志里也完全没有任何说明。
能对得上的只有一句万能套话:「优化计划默认关闭,不主动加入不会将输入用于训练」。
现场验证:我亲手把「删除」和「锁死」都跑了一遍
这篇文章的关键在于:「删掉就没事了」是错的,「关了开关就没事了」也是错的。 我在自己的服务器上把这两件事各验证了一遍。
验证一:删除 ≠ 消失
我建了一个临时 git 仓库,提交了一个带「密钥」的文件:
02cba66 init: config with secrets
然后「修复」——把它改成从环境变量读取:
b5a3638 fix: remove hardcoded secrets
现在打开工作区里的 config.py,密钥确实不在了。但历史里原封不动:
$ git grep -n "demo-token" $(git rev-list --all) -- config.py
e0be6c7e9bc3ece6d1a324474af56eb7a0939ad4:config.py:1:API_KEY="***"
$ git show HEAD~1:config.py
API_KEY="***"
DB_PASSWORD="***"
工作区里干干净净(grep -c "demo-token" config.py 返回 0),但 git show HEAD~1:config.py 照样能打出完整内容。这就是为什么 ZCode 打包 .git 是「底裤级」泄露:你以为删了,其实只是工作区看不见,.git/objects 里每一个已删除的提交、每一份被覆盖的密钥都还活着。上传快照 = 把整个仓库的「考古层」一起端走。
(测试在隔离的临时目录里进行,写入的是占位符,不是真实密钥。)
验证二:锁死目录,内核帮你挡住
ferstar 给的防御方案是文件系统层加不可变锁。Linux 上是 chattr +i。我实装了一遍:
$ chattr +i ~/.zcode/v2/checkpoints
$ lsattr -d ~/.zcode/v2/checkpoints
----i---------e------- ~/.zcode/v2/checkpoints
然后模拟 ZCode 再次写入快照:
$ touch ~/.zcode/v2/checkpoints/new-snapshot.enc
touch: setting times of ...: No such file or directory
exit=1
写入直接被内核拒绝。删除也试了:
$ rm -f ~/.zcode/v2/checkpoints/snapshot.enc
rm: cannot remove ...: Operation not permitted
exit=1
chattr -i 解锁后一切恢复正常。结论:快照逻辑在尝试写目录时被系统内核拦截,没有产物,后续向 OSS 的直传自然无从谈起。 代价是「检查点回滚 / 时间线」功能不可用(那本来就是要拿全量代码上云换的),日常补全、对话、工具调用完全不受影响。
文章里还写了个细节:删掉 pending 包后半小时内又重新抓了一次,失败计数从 564 蹦到 565——上传器发现文件没了,直接重新打包。手动删除就是打地鼠。只有文件系统层的锁,能让上传器连「重新打包」都做不到。
同样的工具,两种姿态
六月底我写过一篇 Claude Code 的事:一个国际社区的开发者发现,Claude Code 会把本地会话隐写入一个「告状」网络,一批人因此被封号。那是发送端的问题——工具往外传了不该传的东西。
ZCode 是另一层的问题:接收端。它把「加密」做成了单向门——公钥由服务器下发,私钥只存云端。你不是「没锁门」,你是被发了一把你永远打不开的锁,而钥匙在房东手里。
一个是「传了不该传的」,一个是「让你自己都解不开地传」。方向相反,但指向同一个问题:用 AI 编程工具,你交出去的边界,到底由谁说了算?
模型推理必然要吃代码上下文——「我发给模型的代码它能看到」,这个大家换工具的时候心里都有数。但「我的整个 Git 历史被静默打包,连我自己都解不开地送走」,是另一条线。数据范围不同,架构姿态不同,隐私政策零披露,UI 开关关不掉,删了还自动重传——这摆明了不是备份,更像采集。
工具没有原罪,但红线应该由使用者自己划。既然软件里关不掉,那就用操作系统的锁把它关进笼子里。
反正,钥匙在谁手里,谁才算真的拥有数据。
评论(0)
暂无评论,来写第一条吧~