Skip to content
Elliot's Harness Lab
Go back

全网都在问 ZCode 把代码传去哪了,智谱在忙着发布新模型

Vibe Coding

听说智谱今天又发新模型,可喜可贺啊。。。

智谱当天的官方公众号发布了 GLM-5.3-FlashX,最高 200 tokens/s,背后还有 10 万张国产芯片。智能、价格、速度,全面竞争力又上了一个台阶。

仿模型发布海报风格的反讽头图:Git 98.9% BackupX Silent Snapshot,代码历史被吸入云端文件堆

同一天,开发者社区里到处都在转另一件事:ZCode 为什么会在后台把整个项目连同 Git 历史一起打包?这些东西准备传到哪里?用户有没有真正同意过?

V2EX 上有人把标题写得很损:

ZCode 会静默上传 Git 全历史,弥补了中国没有 a\的遗憾。

V2EX 用户用反讽标题讨论 ZCode 静默上传 Git 全历史

这当然是一句玩笑。

但这个时间点确实颇有黑色幽默:开发者在看自己的几百兆代码快照,官方在讲每秒能吐多少 token。一个在讨论速度,一个在讨论信任。

为什么我今天忍不住要写这篇文章呢?

事情的起因是这样的。

昨天我干活的时候,我的 Mac 突然滚烫。我以为 Codex 又抽风了,于是让它查了一下。结果它委屈巴巴地告诉我:

不是它。

而是 ZCode。

ZCode 正在偷偷上传我的源码。

Codex 排查 Mac 异常发热后发现 ZCode 正在打包项目并尝试上传的真实事件截图

我第一反应也是不信,于是继续往下查。

ZCode 还在运行,版本是 3.12.3。它的快照目录已经占了 894 MB;当天刚生成的一份加密包接近 748 MiB,对应一个我正在工作的本地项目。继续检查 Manifest 后,我发现这份快照有 98.9% 都来自 .git。

不是 98.9% 的当前源码。

是 98.9% 的 Git 历史。

这就不是网上传言了。这是此刻躺在我自己硬盘上的文件。

先把边界说清楚:本文基于我自己电脑上仍在运行的 ZCode 3.12.3 做只读取证,没有触发新的备份或上传,没有读取项目源码正文,也没有尝试访问云端对象。公开资料部分参考了 Ferstar 的逆向分析,但本文的核心数据来自我本机当前版本。

先用大白话讲:.git 为什么比当前代码更敏感

不写代码的人可能不熟悉 .git。

你可以把它理解成一个项目的时间机器、回收站和研发档案馆。

平时打开项目,只能看到当前版本的代码;.git 里面却可能保存着:

所以,把当前任务相关的十几个文件交给 AI,和把完整 .git 打包,是两件完全不同的事。

前者像是把正在修改的几页合同交给助手;后者更像是把公司的档案室、废纸篓、历史修订记录和未公开草稿一起装箱。

这也是这件事真正严重的地方:ZCode 打包的不只是“我现在让 AI 看什么”,而是“这个仓库在我电脑上曾经拥有什么”。

我在自己电脑上确认了什么

这次检查确认了六件事:

  1. ZCode 3.12.3 确实存在常驻的 Repo Snapshot Sidecar。 登录 token 可用时,它会在本地工作区执行快照捕获。
  2. .git 不是误打包,而是代码明确要求优先纳入。 普通大文件、二进制、疑似密钥文件会被排除;.git 内部文件反而直接放行。
  3. 我的一份快照有 98.9% 来自 .git。 758.0 MiB 的工作区清单里,Git 元数据占了约 749.8 MiB。
  4. 客户端具备完整上传链路。 它会向 zcode.z.ai 请求上传凭证,再把加密包通过 multipart POST 直传凭证指定的 OSS Host。
  5. 当前 748 MiB 大包还在 pending,不能说这一次已经传成功。 但多个工作区存在历史 lastAcceptedManifestHash,而当前代码只有在 OSS 上传成功后才写入这个字段。
  6. 界面里的相关开关并不关闭快照捕获。 当前“优化体验”“仓库快照索引”和 Memory 全部关闭,新的 baseline 包仍然被生成。

这不是“AI 为了回答问题读取了几段代码”。它处理的是一个更大的数据单元:工作区快照、完整 Git 元数据,以及部分工作区外的 ZCode 全局配置。

我电脑上实际留下了什么

当前应用版本:

ZCode 3.12.3
Build 3.12.3.7463
Bundle ID: dev.zcode.app

ZCode 的快照目录已经占用:

~/.zcode/v2/checkpoints    894 MB

其中,当前打开的“项目 A”对应一份 baseline 加密包:

项目实际值
压缩前清单体积794,869,376 bytes,约 758.0 MiB
加密包体积784,276,729 bytes,约 747.9 MiB
文件数量7,946
快照类型baseline
当前状态activeUpload + pendingUpload
当前单包上传尝试attemptCount: 1

此外,“项目 B”还留着一份约 143 MiB 的历史 pending 加密包。

这些并不是缓存缩略图或索引数据库。文件名和 envelope 清楚写着:

repo_snapshot_manifest/v2
repo_snapshot_encrypted_artifact/v2
tar.gz.enc
baseline

最离谱的地方:98.9% 都是 .git

我没有读取任何源码内容,只对 Manifest 中的路径和 sizeBytes 做了统计:

内容文件数体积占总快照字节
全部文件7,946758.0 MiB100%
.git6,663749.8 MiB98.91%
.git/objects6,475738.3 MiB97.39%
非 .git 文件1,2838.3 MiB1.09%

清单还包含:

这意味着它拿到的不只是你屏幕上正在编辑的代码,而是本地 Git 仓库保存的历史对象、分支引用、reflog 和远程仓库配置。

代码里对 .git 的态度更加直接。

ZCode 普通文件的过滤规则会排除:

但是,过滤函数先判断路径是否属于 .git。只要命中根 .git 或 .git 内部目录,就直接返回 include: true,后面的大文件、二进制和 secret-like 检查全部跳过。

用人话说就是:

你的大文件我嫌占空间,疑似密钥我也会躲开;但只要这些内容已经进入 Git 对象库,不管多大、不管是不是二进制,我都要。

这也是为什么一份当前源码只有几 MiB 的项目,最终能生成接近 750 MiB 的快照。

参考文章里的另一个样本包含 196.1 MB .git/lfs。我的仓库没有 LFS 对象,但从当前过滤逻辑看,只要它位于 .git 内部,同样会被无条件纳入。

它不只打包项目,还打包 ZCode 全局配置

本次快照还有一份 repo_snapshot_extra_manifest,实际包含:

文件来源体积
settings.behavior.jsonZCode 全局行为设置761 bytes
skills.json用户 Skills 元数据32,741 bytes

当前版本客户端还实现了更多收集器。如果对应配置存在,它可以把这些内容作为工作区外的额外输入加入快照:

这部分每个工作区最多不是只有一份哈希。代码会把内容序列化成 JSON,再作为 extraFiles 一起进入归档与加密流程;文本内容上限是单项 20 MiB。

因此,这不只是“代码备份”。它可能同时描述你的 Agent 工作方式、工具接入、规则、Skills 和全局指令。

上传链路到底怎么走

ZCode 3.12.3 的客户端代码里,整条链路可以还原成:

本地工作区
  ↓ captureBeforePrompt / terminal capture
扫描 tracked、untracked 与完整 .git 元数据
  ↓
生成 Manifest 和 extra Manifest
  ↓
tar.gz 全量或增量归档
  ↓
AES-256-CTR 加密文件内容
  ↓
RSA-OAEP-SHA256 包装 AES key
  ↓
注册 pendingUpload
  ↓
GET zcode.z.ai/api/v1/snapshot/upload-credential
  ↓
获得 snapshot_id、OSS Host、Object Key、签名、Security Token、Callback、公钥和大小限制
  ↓
multipart/form-data POST 到 OSS
  ↓
OSS Callback 通知 ZCode 后端
  ↓
写入 lastAcceptedManifestHash,清理本地 pending

这里有两个关键设计。

第一,文件不是先传到 ZCode 业务服务器再中转。客户端直接向凭证返回的 OSS Host 发起 POST,上传文件名固定为:

repo-snapshot.tar.gz.enc

第二,只有 uploadObject(...).ok 之后,客户端才会调用 markAcceptedManifest,把当前 Manifest 写成 lastAcceptedManifestHash,并清除对应 pending 文件。

所以,lastAcceptedManifestHash 的语义不是“本地扫描完成”,而是当前客户端认为这次对象上传已经成功。

加密了,是否就安全了

本机 envelope 文件显示:

内容加密:AES-256-CTR
密钥包装:RSA-OAEP-SHA256
压缩格式:tar.gz

这是一套标准的信封加密:客户端随机生成 AES key 加密归档,再用 RSA 公钥包装 AES key。

问题是,RSA 公钥由 /api/v1/snapshot/upload-credential 接口动态下发,本机只保存 encryptedDataKey,没有用户持有的解密私钥。被包装的 AES key 还会进入 OSS Callback,交回 ZCode 服务端登记。

因此,这种加密可以保护传输和对象存储中的密文,但不能叫“用户独占密钥的端到端加密”。从架构上看,服务端只要持有对应 RSA 私钥,就具备解密能力。

如果这个功能只是为了让我在本机恢复代码,为什么恢复密钥不掌握在我手里?这是整件事最值得追问的地方。

这次到底上传成功了吗

需要分成“当前大包”和“历史快照”两个答案。

当前 748 MiB 大包:没有成功证据

它仍然留在 pending/,状态是:

kind: baseline
attemptCount: 1
activeUpload: true
pendingUpload: true

因此不能写成“这个 748 MiB 文件已经上传”。当前只能确认它已经完成扫描、打包、加密、申请上传凭证并进入上传队列。

历史快照:存在上传成功的强证据

我的多个工作区状态里都有 lastAcceptedManifestHash,包括:

而客户端代码只会在 OSS 对象上传返回成功后写入该字段。

所以可以准确地说:当前大包还没成功,但这台机器上的 ZCode 此前已经完成过多个工作区的快照对象上传流程。

我们没有访问服务端,因此无法确认这些对象现在是否仍然保留、保留多久、位于哪个 OSS Region、删除会话或注销账号后是否同步清理。

一个必须纠正的点:failureCount 不是重试次数

参考文章把 failureCount: 564 解释成上传失败了 564 次。当前 3.12.3 的代码并不支持这个解读。

单个 pending 包真正的上传次数记录在:

attemptCount

默认策略是:

单包最多尝试 3 次
最长保留 24 小时

failureCount 是一个跨任务累计值:某个已尝试但失败的 pending 到下一个 turn boundary 时才加一,并作为下一份快照的 attribution 发送。

所以,我本机的 failureCount: 108 更接近“此前累计出现过 108 个失败快照周期”,不能写成“同一个 748 MiB 文件重试了 108 次”。当前这个包的 attemptCount 明确只有 1。

这个区别很重要。文章可以骂,但证据不能跟着情绪跑。

我明明已经关了相关开关

我当前的 ZCode 设置是:

optimizeAgentExperienceEnabled: false
repoSnapshotIndexingEnabled: false
memoryEnabled: false

新的 baseline 快照依然在当天生成。

查看客户端初始化代码后,原因很清楚:RepoSnapshotUploadWorker 和 RepoSnapshotSidecarService 在 ZCode Host 启动时直接实例化,captureBeforePrompt 只检查两件事:

  1. 当前是不是本地工作区;
  2. 登录 token 能不能取到。

它没有拿“优化体验”或“仓库快照索引”做上传门禁。

repoSnapshotIndexingEnabled 会被一起写入全局配置快照,但它并不控制快照是否被捕获。关闭它,最多表示不要建立某类索引,不代表不要扫描、加密和上传。

ZCode 官方隐私政策对“优化计划”的描述也是用于产品和模型训练优化,而不是关闭服务运行所需的数据传输。把“退出优化计划”等同于“代码不再离开本机”,这个理解并不成立。

为什么这件事越过了合理边界

使用 AI Coding 工具,向模型提交当前任务相关的代码上下文,是用户可以合理预期的行为。

但这次快照做了几件完全不同的事:

数据范围不同

推理需要的是当前任务上下文;快照拿的是整个本地 Git 元数据库。历史中已经删除的代码、旧配置、未推送分支、reflog 和内部 remote 地址,都可能存在于 .git。

触发方式不同

ZCode 的安全操作确认文档说,Agent 的文件修改、命令和网络操作会在执行前展示并确认。但 Repo Snapshot Sidecar 不是普通 Agent Tool Call,它在后台捕获,不走同一套可见确认流程。

控制能力不同

官方 UI 没有一个清晰的“不要备份/上传我的工作区”开关。相关设置全部关闭后,机制仍然运行。

密钥归属不同

用户本机没有解密私钥,服务端下发公钥并接收被包装的 AES key。这更像服务端可消费的数据采集链路,而不是用户自主控制的本地备份。

官方披露粒度不同

隐私政策写了会处理“用户在对话中提交的文本、文件和代码”,但截至 2026 年 9 月 18 日,没有找到“自动扫描完整工作区、包含 .git、额外收集 Skills/MCP/Hooks/AGENTS.md、上传到对象存储”的公开说明。

同意把代码片段交给模型,不等于明确同意把完整 Git 历史做成服务端可解密快照。

如何检查自己的 ZCode

下面这些检查都是只读的,不会触发上传。

1. 查看快照目录占用

du -sh ~/.zcode/v2/checkpoints

2. 查找 pending 加密包

find ~/.zcode/v2/checkpoints -type f -name '*.tar.gz.enc' -print

3. 查看每个工作区的状态

find ~/.zcode/v2/checkpoints -name state.json -print

重点关注这些字段:

workspacePath
lastCompressedSize
activeUpload
pendingUpload
attemptCount
lastAcceptedManifestHash

4. 只统计 Manifest,不读取源码

Manifest 本身已经列出相对路径和字节数。可以统计:

不要公开 Manifest 原文,因为它会暴露项目结构、分支名和内部文件路径。

怎么阻断

目前没有找到 ZCode 官方提供的快照上传开关。按确定性从高到低,能做的是:

屏蔽阿里云 OSS 地址能不能阻止上传?

可以阻止密文真正离开电脑,但不能阻止 ZCode 扫描、打包和加密项目。

当前客户端不会把 OSS Host 写死在应用里。它先请求 zcode.z.ai/api/v1/snapshot/upload-credential,由服务端动态返回本次上传使用的 OSS Host、Object Key、签名和 Security Token。因此,只封锁当前观察到的某一个 OSS 地址并不可靠:下次凭证可能返回不同 Bucket、Region 或域名。

几种拦截方式的实际效果不同:

拦截方式阻止外传阻止本地打包主要影响
屏蔽当前 OSS Host可以不可以Host 可能动态变化,规则容易失效
屏蔽 *.aliyuncs.com大概率可以不可以会同时影响电脑上的其他阿里云服务
按进程禁止 ZCode 访问 OSS可以不可以需要 LuLu、Little Snitch 等支持进程规则的出站防火墙
屏蔽整个 zcode.z.ai可以可以上传凭证在扫描前获取,但登录、模型、额度等 ZCode 功能也会一起失效
锁定空的 ~/.zcode/v2/checkpoints可以可以Checkpoint、时间线或恢复能力可能不可用
退出账号并完全退出 ZCode可以可以客户端拿不到登录 token,云端功能无法继续使用

这里还有一个容易忽略的点:网络拦截失败后,pending 包不会立刻消失。 当前版本对单个 pending 包默认最多尝试 3 次、最长保留 24 小时。也就是说,屏蔽 OSS 解决的是“暂时传不出去”,并没有解决 CPU、磁盘占用和后台打包;规则一旦失效,已经生成的 pending 包仍可能继续尝试上传。

如果仍然需要使用 ZCode,更合理的组合是:

  1. 退出 ZCode 后先隔离现有 pending 文件;
  2. 使用进程级防火墙阻止 ZCode、ZCode Helper 和 zcode-cli 访问 OSS;
  3. 锁定重新创建的空 checkpoints 目录,从文件生成阶段阻断;
  4. 只把 shallow clone 或 Git worktree 交给 ZCode,避免暴露主仓库完整对象库。

1. 不要让敏感仓库进入 ZCode

这是唯一不依赖客户端实现细节的办法。商业项目、客户代码和带历史秘密的仓库,不要直接作为 ZCode 工作区打开。

2. 使用隔离副本

确实需要使用时,可以准备:

当前扫描逻辑对根目录是 .git 文件的 worktree 和 .git 目录的普通 clone 处理不同。使用 worktree 可以避免把主仓库完整对象库直接放在工作区根目录。

3. 退出登录并完全退出 ZCode

当前快照入口必须先从 token provider 取得登录 token;取不到 token 时会直接返回。仅关闭“优化体验”或“仓库快照索引”不够。

4. 隔离现有 pending 文件

退出 ZCode 后,可以先把 ~/.zcode/v2/checkpoints 整体移动到离线隔离目录,再检查内容。只删 .enc 文件并不能保证不再生成,下一次捕获可能重新创建 baseline。

5. 文件系统层阻止重建

在 macOS 上可以对重新创建的空 checkpoints 目录使用 immutable flag;Linux 可以使用 chattr +i。这是非官方 workaround,会让 ZCode 的相关恢复能力失效,也可能产生后台 IO 错误,但能从文件生成阶段切断后续上传。

我更关心的是行业问题

这件事并不只属于 ZCode。

Agentic Coding 工具正在从“读当前文件”变成“接管整个工程工作流”。为了实现跨轮恢复、语义索引、仓库 Wiki、Memory 和多 Agent 协同,它们越来越倾向于维护一份完整的项目表示。

真正的问题不是能不能做,而是四件事有没有说清楚:

  1. 默认范围是什么? 当前文件、当前仓库,还是完整 Git 历史?
  2. 数据去哪里? 本地索引、厂商服务器,还是第三方对象存储?
  3. 谁持有解密能力? 用户、企业,还是服务端?
  4. 用户能否真正关闭? 是关闭训练,还是关闭捕获、上传和保留?

我在商业化项目的 Loop Engineering 实践里反复强调,Harness 的价值不只是给 Agent 更多工具,而是把权限、证据和停止条件变成硬边界。

AI Coding 工具也应该接受同样的要求:它可以很强,但不能把“强”建立在用户不知道自己的整个 Git 历史已经被装箱的前提上。

写在最后

ZCode 这套机制最值得批评的,不是使用 OSS,也不是使用服务端可解密的信封加密。真正越界的是组合在一起的产品选择:

“代码备份”听起来像是在保护用户。

但当备份范围由厂商决定、密钥由厂商控制、上传由后台触发、关闭入口又不存在时,它就不再只是一个备份功能,而是一条应该被公开解释的数据管道。


正在做 Agent 产品、企业 AI 落地或智能化转型?

我长期关注 设计 Agent 、 企业 Harness / FDE 、 Agent 框架 和 AI Coding 。如果你也在这些问题里打转,欢迎探讨。

了解白苏 Elliot
Share this post:

下一篇
飞书文档已禁止下载,为什么 API 仍能导出?