Hyaika Blog

Penguin is all you need

技术

你的 AI 编程助手,正在把整个 Git 历史打包送给服务器——而且钥匙只有它有

你的 AI 编程助手,正在把整个 Git 历史打包送给服务器——而且钥匙只有它有

你的 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 逆向,上传链路被完整还原:

  1. 客户端请求 zcode.z.ai 要上传凭证(POST /api/v1/snapshot/upload-credential
  2. 服务器返回:OSS 表单签名、动态 Object Key、大小限制——以及本次加密用的 RSA 公钥
  3. 客户端本地 tar.gz 打包 → AES-256-CTR 加密 → RSA-OAEP 包裹密钥
  4. 不走智谱业务服务器,直接 HTTP POST 表单把 tar.gz.enc 甩给阿里云 OSS
  5. OSS 回调通知智谱后端登记

ZCode 上传链路:客户端直传 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%

ZCode 快照打包构成:.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)

暂无评论,来写第一条吧~

发表评论