ZCode 静默上传全量 Git - 官方回应了,我有几个疑问
ZCode 回应了静默上传全量 Git 历史一事,我逐条提出疑问;文末附 ferstar 原文,从卡在 pending 的 313MB 加密包出发,还原 ZCode 静默打包上传整个工作区(含完整 .git 历史)的链路,以及用文件系统不可变锁防御的办法。
先交代背景。ZCode 是智谱的 AI 编程桌面客户端。今天,博主 ferstar 发文扒出,只要登录账号,它就会在后台把整个工作区连同完整的 .git 历史打包加密,传到阿里云 OSS,UI 里没有任何开关关得掉。文章发出来后,ZCode 官方给了一份回应。我先放这份回应和我的几点疑问,ferstar 的原文完整附在后面。
官方回应来了,我有几个疑问
原文发出之后,ZCode 官方在社区里发了一份说明。照录原文。
各位 ZCode 用户:
今日社区中的相关讨论我们高度重视,第一时间完成自查,先向受影响的用户道歉。现就有关情况说明如下:
此次问题源于 ZCode 的“代码库索引”功能。该功能旨在帮助用户在本地生成仓库索引,以支持包括历史版本在内的会话检查点恢复、历史版本回退及 Repo Wiki 等功能。
Repo Wiki 功能在生成 Wiki 页面时可能会触发仓库数据上传。Wiki 页面在云端生成后,相关上传数据会立即销毁,不会保存。由于该功能在上线初期默认开启,部分用户因此受到影响,对此我们深感抱歉,目前相关问题已经修复。
我们深知任何关于数据的问题都会直接影响用户对产品的信任,我们将于近期开源 ZCode 代码库,在一个更加开放的生态中把产品做的更好,并邀请第三方评估人员对系统运行情况进行审查,并持续公开产品审查进展,以完全透明的方式建立大家的信任。
对于此次问题给大家带来的困扰,我们深表歉意。我们将为全体 ZCode 用户额外提供一次周额度重置,额度将于今日发放。
再次感谢大家的关注与监督。@所有人
我把这份说明读了三遍,越读越觉得有几处和下面这篇原文里的证据链对不上。
一、“代码库索引”是个好词,但索引不该索引到把 .git 全打包。
官方说问题源于“代码库索引”功能,是为了支持检查点恢复、历史回退和 Repo Wiki。可原文的 Manifest 写得清清楚楚。一个 345MB 的 baseline 全量快照,.git 一个目录就占了 86.6%,LFS 缓存、完整对象库、reflog 全在里面。本地建索引要的是可检索的结构化数据,不是把仓库从创建到今天的每一版历史原样端走。“索引”这两个字,背不动这 300 多兆。
二、“云端生成后立即销毁,不会保存”,这句话没法自证。
原文的加密方案是服务端动态下发 RSA 公钥、私钥只存云端的信封加密。也就是说,这份数据到了云端是能解的,而且是只有他们能解。“立即销毁”是一句承诺,不是一个你我或任何第三方能验证的技术事实。没有审计日志,没有第三方凭证,也拿不出可复现的销毁证明。这边是一整套打包上传的证据链,那边回一句“已经销毁了”,这笔账怎么算都是亏的。
三、该回答的问题,一个都没答。
- 为什么上传范围包含完整
.git历史和reflog,而不是当前工作区?做 Repo Wiki,用得到五年前已删分支的记录吗? - 为什么快照捕获和上传在代码里是无条件实例化的,UI 里没有任何开关关得掉?真是为用户好的功能,为什么不给用户一个拒绝的按钮?
- 为什么解密私钥只放服务端?号称给用户做断点恢复,结果用户自己解不开自己的备份,那这恢复的是谁的数据?
- 为什么原文里那个 pending 包失败重试 564 次、手动删掉半小时内又自动重传?这到底是“可能触发上传”,还是“执着地要传”?
说明里只承认了“Repo Wiki 生成页面时可能触发上传”,对“整个工作区连同 .git 静默打包”这个核心事实,只字未提。
四、补偿方式很有互联网味。
数据问题,赔的是一次“周额度重置”。翻译一下,你的代码历史被打包上云这件事,我们赔你几百万 token。把隐私折算成额度,是这届 AI 产品最熟练的公关动作。
五、开源和第三方审查,欢迎,但顺序反了。
“近期开源代码库、邀请第三方评估审查、公开审查进展”,这些当然是好事,也正是原文作者一直在呼吁的。但看时间线,数据先传完,文章再发出,社区炸了之后,才想起透明。**先上车后补票,补得再诚恳,也还是补票。**不过话说回来,真开源、真送审,总比死不承认强,这一条我给半分。
最后,那句“第一时间完成自查”也值得玩味。这个上传机制早就存在,pending 包失败了 564 次、作者手动删了还自动重传,**它一直在那里,只是没人看。**所谓“第一时间”,恐怕是“被公开的第一时间”。
工具可以犯错,公司也可以道歉。但数据这件事,道歉之后的动作比道歉本身更重要。我会盯着两件事,一是代码库是不是真开源了,二是那句“立即销毁”有没有一天能被第三方验证。
以下为转载原文。
转载本文转载自 ferstar 的博客,原文《扒一扒 ZCode 静默上传全量 Git 历史的骚操作》,版权归原作者所有。
本来只是清理磁盘时顺手看一眼 ~/.zcode 为什么占了 700 多 MB。断断续续排查下来,确认了一件挺离谱的事:
只要你在登录状态,ZCode(智谱官方的 AI 编程桌面端)就会在后台静默把整个工作区——包括完整的 .git 历史、LFS 大文件缓存、reflog 以及全局应用配置——打包加密,直接上传到阿里云 OSS。
更讽刺的是:加密用的 RSA 公钥是服务端动态下发的,私钥只存在云端。 你本地生成的那份几百兆的密文,连你自己和客户端本体都解不开。
这篇文章把排查过程、证据链和最终的一键防御方案完整记录一下。
起点:一个卡在 pending 里的 313MB 压缩包
~/.zcode 是 ZCode 的本地数据根目录。当时扫出来的目录体积分布大概这样:
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 几乎全是核心资产。
它要传到哪:从日志到 asar 逆向
日志里没有直接打出具体的上传地址,于是顺手把客户端的 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表单凭证 + callback
C->>C: tar.gz 打包 → AES-256-CTR 加密 → RSA-OAEP 包裹密钥
C->>O: PostObject 表单直传 tar.gz.enc
O->>S: callback 回调确认接收
整个流程分两步:
- 先找协调服务器要凭证:客户端请求
https://zcode.z.ai(代码里的VITE_ZCODE_ENDPOINT_ORIGIN),服务端返回 OSS 表单签名(policy、x-oss-signature)、动态分配的 Object Key、大小限制,以及本次加密要用的公钥; - 表单直传 OSS:客户端在本地打包并流式加密后,根本不经过智谱自己的业务服务器,直接走 HTTP POST 表单把
tar.gz.enc甩给阿里云 OSS。传完之后由 OSS 服务端触发 callback 通知智谱后端登记。
抓包看了下 ZCode 运行时的连接,进程常驻的 HTTPS 连接刚好就是 zcode.z.ai 的解析 IP 外加两个阿里云 OSS 节点。
最讽刺的部分:这把钥匙是服务端的
客户端用的加密是标准的信封加密(Envelope Encryption):
keyId: String(i.encryption.key_version),
keyWrapAlgorithm: "rsa-oaep-sha256",
publicKeySpkiPem: Ylt(i.encryption.public_key)
- 文件内容用随机生成的对称密钥走 AES-256-CTR 加密;
- 对称密钥用 RSA-OAEP-SHA256 包裹,公钥就是上面从服务端动态拿到的。
关键就在这把 RSA 公钥:它是服务端在下发上传凭证时临时给的,私钥从头到尾只在云端。 我拿本机的所有私钥去解 envelope 里的密钥,毫无悬念全都失败。
这意味着:你硬盘上那份 313MB 的密文,你打不开,ZCode 客户端自己也打不开,全天下只有智谱后端的私钥能解。
如果这玩意真是为了给用户做断点恢复或者跨设备同步,密钥理应绑在本地(像 Git 或者 Time Machine 一样)。一把只有服务端能解开的钥匙,目的只有一个:确保服务端单方面能看。
快照里到底装了啥:近九成是 .git
密文虽然解不开,但快照生成时留下的 Manifest(文件清单)是明明白白写在本地的。拉出这份包含 42,411 个文件的清单统计了一下:
| 内容 | 体积 | 占比 | 包含的信息 |
|---|---|---|---|
.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/config里配的内部自建 GitLab 域名和仓库路径。
另外代码里还带了一个 repo_snapshot_extra_manifest,会把你的 ZCode 全局配置文件(比如 settings.behavior.json)算完哈希,跨工作区打包,随每次快照一起传上云端。
开关的真相:你关掉的开关根本管不着它
很多人第一反应是去设置里找开关关掉。我把设置项和代码逻辑逐个对了一遍:
| 开关 | 你以为它管 | 实际管 |
|---|---|---|
优化体验 (optimizeAgentExperienceEnabled) | 数据采集 / 遥测上传 | 只管要不要拿你的数据去训练模型。关了照样抓快照上传 |
仓库快照索引 (repoSnapshotIndexingEnabled) | 快照功能本身 | 只管服务端拿了快照后要不要建索引。关了本地打包上传一点不落 |
看客户端组装代码更直白:负责快照捕获和上传的 sidecar 是在启动时无条件实例化的。代码里根本没有任何针对用户配置的 if 判断,唯一的要求就是 tokenProvider 能拿到登录后的 JWT。
一句话:只要你登录了账号,这个上传机制就是常开的,而且 UI 里没有任何开关能把它关掉。
抓取触发点有两个:一个是 captureBeforePrompt(每次发 Prompt 前),另一个是任务结束时标记 repo-wiki-update。翻看日志,单个活跃会话里最多能产生 62 次快照捕获。
隐私政策怎么说的
翻了下 ZCode 的隐私政策,里面明确写了会收集“对话中提交的文本、文件和代码”——这属于 AI 助手调模型推理的常规操作,各家都一样。
但通篇只字未提会把整个工作区连同完整 Git 历史静默打包上传。官方文档、FAQ 和更新日志里对此也完全没有任何说明。
能对得上的只有一句万能套话:“优化计划默认关闭,不主动加入不会将输入用于训练”。
防御:删是打地鼠,直接锁目录
刚发现这个 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
影响与恢复
- 阻断效果:快照逻辑在尝试写目录时直接被系统内核拦截,没有产物,后续向 OSS 的直传自然无从谈起;
- 功能影响:ZCode 的“检查点回滚 / 时间线”功能不可用(本来也是拿全量代码上云换的)。日常的代码补全、对话、工具调用完全正常,日志里被吞掉的 IO 报错不影响使用;
- 想恢复:执行
chflags nouchg ~/.zcode/v2/checkpoints(macOS)或sudo chattr -i ~/.zcode/v2/checkpoints(Linux)即可。
写在最后
用 AI 工具,模型推理必然要吃代码上下文,这个在用的时候大家心里都有数。但这事越线的地方很明确:
一是数据范围。推理给的是当前任务相关的上下文,而快照是把整个仓库连同几年的 Git 提交历史全部打包端走。
二是架构姿态。如果真是给用户做断点恢复或跨端同步,解密密钥理应在用户本地。一把只有服务端能解的钥匙、零披露的隐私政策、关不掉的默认行为、删了还自动重传的执拗——这摆明了不是备份,更像采集。
工具没有原罪,但红线应该由使用者自己划。既然软件里关不掉,那就用操作系统的锁把它关进笼子里。
评论