AI · 隐私安全

你电脑里的AI编程工具,正在偷偷打包你的整个代码库上传云端

💡 核心摘要

一位开发者周末清理磁盘,发现 ~/.zcode 占了 700 多MB——顺藤摸瓜,挖出一个 313MB 的加密"包裹":里面是他的整个商业项目,连同完整 Git 历史、LFS 缓存、全局配置,被打包加密后直传阿里云 OSS。更讽刺的是,解密的私钥从头到尾只在服务器手里,你自己都打不开。设置里的开关关不住它,删掉它半小时就会重新打包。这篇文章带你复盘完整证据链,并给出三条命令的永久封堵方案。

每个写代码的人,大概都有过一次"周末大扫除":磁盘飘红,硬盘告急,你打开存储管理工具,按体积从大到小排序,准备把那些吃空间的缓存一个个揪出来删掉。

九月中的一个周末,一位网名叫 ferstar 的开发者也这么干了。他发现有个目录不太对劲:~/.zcode,700 多MB。这是智谱官方 AI 编程桌面应用 ZCode 的数据目录。他本来以为无非是些会话记录、模型缓存——AI 工具吃点磁盘空间,太正常了。

结果在其中一个叫 v2/checkpoints/ 的子目录里,他翻出了一个 313MB 的 .enc 加密文件。加密文件旁边,躺着一份明文的状态记录:

# 状态文件(节选,项目名已脱敏)

{

  "workspacePath": "/Users/ferstar/myprojects/<一个商业项目>",

  "encryptedSizeBytes": 313070842,

  "workspaceSizeBytes": 345549173,

  "kind": "baseline",

  "failureCount": 564

}

翻译一下这份"快递单":他正在开发的一个商业项目,整个工作区 345MB,被打包成了 313MB 的加密档案,类型是 baseline——也就是全量快照。而 failureCount 那一栏赫然写着:564。上传失败了 564 次,这个包裹一直卡在本地的 pending 目录里,等着下一次重试。

打个比方:你在储物间大扫除,发现角落里有一个贴了封条的大箱子。箱子上的标签写着你的名字和你公司的名字,寄件人一栏,写的是你家请的保姆,而收件地址是一个你从没听说过的仓库。你问保姆这是什么,保姆说:别管,这是为你好。

接下来的调查断断续续持续了几天。结论比最初发现的还要劲爆:只要你是登录状态,ZCode 就会在后台把你的整个工作区——包括完整的 .git 历史、LFS 大文件缓存、reflog、甚至全局应用配置——打包、加密,直传阿里云 OSS。

小贴士:~/.zcode 是 ZCode 的数据根目录,正常情况下会存放会话数据库、执行日志、内置运行时——这些占空间都不冤枉。这次的问题出在 v2/checkpoints 子目录:它是"代码快照"功能的落盘位置。本文基于该开发者公开的完整调查证据链(日志、逆向代码、socket 抓取)复盘,截至发文,官方未公开回应。

01 // 流水线复盘:313MB 是怎么一步步上云的

日志里没有明晃晃的上传地址,这位开发者干脆把客户端的 app.asar 拆开来逆向。所谓 asar,你可以理解成 Electron 应用的"压缩行李箱"——前端代码全在里面,解包之后,逻辑就一目了然了。

还原出来的上传流水线分两步,整个流程设计得相当"专业":

  1. 第一步,找"调度中心"要凭证。客户端向 zcode.z.ai 发起请求(POST /api/v1/snapshot/upload-credential)。服务器返回的东西很有意思:阿里云 OSS 的表单签名、一个动态的存储路径、大小限制——以及一枚当场下发的 RSA 公钥。
  2. 第二步,直传阿里云。客户端在本地把工作区打成 tar.gz,用临时的对称密钥做 AES-256-CTR 加密,再用服务器给的那枚公钥把对称密钥包起来,然后绕过智谱自己的应用服务器,直接以 HTTP 表单 POST 的方式把加密包扔到阿里云 OSS 上。OSS 收到货之后,再回调智谱后端登记这笔快照。

光看代码还不够,他又看了一眼运行中进程的网络连接:这个 ZCode 进程确实长期保持着到 zcode.z.ai 的 HTTPS 连接,外加两个阿里云 OSS 存储节点的连接。代码说的和机器做的,对上了。

这里最有意思的,是它的加密方案——教科书级的"信封加密"。听着复杂,其实你在生活中天天见:

想象你要给远方的朋友寄一批黄金。你把黄金放进一个带一次性锁的箱子(临时对称密钥 AES-256-CTR 加密),箱子的钥匙太小容易丢,于是你又把钥匙装进一个小保险盒(RSA-OAEP-SHA256 包装)。问题来了:这个保险盒的钥匙,只有收件人有。

也就是说:那 313MB 的密文,就躺在你自己硬盘上,但你打不开,ZCode 客户端自己也打不开。这位开发者也真的试了——用他机器上所有的本地私钥去解那个被包装的密钥,全军覆没,符合预期。全世界只有智谱的后端能打开它。

小贴士:信封加密本身是业界标准做法,云厂商内部传输敏感数据都用它。技术上无可指摘——问题从来不在"怎么加密",而在"密钥在谁手里"。这个区别,我们下面细说。

02 // 86.6% 都是 .git:上传的不是代码,是你的前半生

密文虽然打不开,但打包时生成的文件清单(Manifest)是明文存在本地的。这份清单里,一次快照共 42,411 个文件。拆开来看体积构成,事情就非常清楚了:

📊 一次快照的体积构成(共 42,411 个文件)

  • .git/lfs/ —— 196.1MB(56.8%):LFS 大文件缓存,所有下载过的二进制素材和媒体原件
  • .git/objects/ —— 102.2MB(29.6%):完整提交历史对象库,每一次 commit、每一棵目录树、每一个文件快照
  • .git/logs/ —— 0.6MB(0.2%):reflog,本地分支操作记录
  • 源码和文档 —— 约46.2MB(13.4%):src/、配置文件、内部文档

.git 目录一项,占了整个包裹的 86.6%。

很多人对 Git 历史有个误解:以为删掉文件就是删掉了。其实 Git 更像一座档案馆——你每一次提交都被永久存档,后来"删除"敏感内容的那次提交,只是在档案馆最新一层贴了张新标签,下面那些旧档案,一页都没少

所以这个包裹一旦上云,对方收到的不只是你现在的代码,而是这个仓库从建库第一天起的全部家谱:

  • 那些你在后来的提交里删掉的 API 密钥和敏感配置——它们还安静地躺在历史对象库里;
  • 没推送过的本地分支名——分支名往往会泄露未发布的功能计划;
  • .git/config 里配置的内部 GitLab 主机名和仓库路径——企业内网的一角被顺手带走了。

还有一个加戏的细节:有个叫 repo_snapshot_extra_manifest 的额外清单,会把你的全局 ZCode 配置文件也哈希打包,跟着每一次快照跨工作区一起上传。

这个仓库总共 10GB,去掉依赖之后剩 345MB——几乎全是核心知识产权。俗话说得好:明枪易躲,暗箭难防。你交给 AI 的每一行代码,是你主动递过去的;而整个档案馆被悄悄搬走,是你根本不知道的箭。

03 // 密钥在谁手里,谁就是主人

看到这里,你可能会想:也许这就是个"云备份"或"跨设备同步"功能呢?给用户做回滚,出发点是好的?

判断一个功能到底是"备份"还是"收集",不看宣传话术,看三处硬指标。

第一处:密钥的归属。如果一个功能真是给用户做的回滚、做的同步——就像 Git、就像 macOS 的 Time Machine——那解密密钥必然在你手里,备份应该你随时能打开。而现在,密钥只在服务器手里,你连自己硬盘上那个 313MB 的文件都打不开。只有服务器能读的备份,服务的是服务器。

第二处:开关的真实作用。这位开发者把 UI 里的设置项和代码逐一对照,结果是这样的:

🎛️ 你以为的开关 vs 实际的开关

  • "体验优化"开关:你以为关掉它会停止数据收集——实际上它只控制"数据是否授权用于模型训练"。快照的捕获和上传照常运行。
  • "仓库快照索引"开关:你以为关掉它会关闭快照功能——实际上它只控制"服务器端是否为已上传的快照建立索引"。本地打包和上传分毫不停。

更直接的证据在宿主装配代码里:捕获和上传的组件在启动时无条件实例化,没有任何针对用户偏好的 if 判断。唯一的运行条件,是登录令牌有效。

一句话:只要你登录着,这条后台流水线就永远在跑,任何 UI 设置都关不掉它。捕获的触发时机也相当勤快:每次提问前各捕获一次(captureBeforePrompt),任务完成时再来一次。一份会话日志里,单个活跃会话产生了多达 62 次捕获事件。

第三处:隐私协议说了什么。ZCode 的隐私政策明明白白写着会收集"对话中提交的文本、文件和代码"——这是喂上下文给大模型的行业惯例,没毛病。但翻遍整个政策、FAQ 和更新日志,没有任何一个字提到"打包整个工作区和完整 Git 历史并上传"。最接近的表述,是一句模板话:"优化程序默认关闭,未经同意不会将输入用于训练"。

奥威尔在《1984》里写过那句著名的话:老大哥在看着你。但这件事比小说更微妙——你以为墙上摄像头的指示灯已经灭了,后来才发现,灭掉的只是指示灯。

04 // 手把手锁死它:三条命令,一劳永逸

先说说最直觉的方案:删掉那个 pending 包。这位开发者第一时间就是这么干的——结果半小时之内,客户端重新捕获了一个 313MB 的新包,重试计数器从 564 跳到 565。上传器一发现文件没了,就再打一个新的。手动删除是打地鼠,地鼠还自带繁殖能力。

真正干净有效的方案,是在文件系统层面做一个"内核级门禁":给 checkpoints 目录打上不可变标记,让任何写入请求在内核层就被拒绝。应用层再怎么勤快,也翻不过操作系统的墙。

场景 A: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

场景 B:Linux 用户

思路完全一样,只是换成了 Linux 的 chattr 命令(需要 sudo):

# 清空并锁死 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

验证的方式很有仪式感——你可以想象成和系统的一段对话:

你输入:touch ~/.zcode/v2/checkpoints/test
它返回:Operation not permitted
你放心了:连你自己都写不进去,ZCode 更别想。

之后的效果是:捕获逻辑每次尝试写盘,都会被内核直接挡回去。本地没有产物,上传流水线就没有东西可发。日志里被吞掉的 I/O 报错,无伤大雅。

小贴士:Windows 用户思路相同——找到 %USERPROFILE%\.zcode\v2\checkpoints 目录,右键"属性" →"安全" →"编辑权限",对自己的账户设置"拒绝写入",效果等同于内核锁。另外记得先删旧的 pending 包再加锁,别让最后一个包裹留在锁外面。

⚖️ 代价与后悔药

  • 代价:"检查点回滚 / 时间线"这个 UI 功能会失效——毕竟它本来就建立在"上传你的代码"之上。日常的对话、代码补全、工具执行,一切正常。
  • 后悔药:macOS 执行 chflags nouchg ~/.zcode/v2/checkpoints,Linux 执行 sudo chattr -i ~/.zcode/v2/checkpoints,即可恢复原状。

05 // 未来影响:AI 时代,边界要自己画

先把话说公道:用 AI 编程工具,模型推理必然需要代码上下文——这是所有人进门时就接受的规则。你让 AI 改一个函数,它当然要读到这个函数。这属于"你递给对方一张纸条"。

但这次的事件,越过的线在两处。

一是数据范围。推理发送的是与任务相关的上下文;快照搬运的是整个仓库加上若干年的提交历史。前者是看纸条,后者是把你的档案柜整个复印一遍搬走——包括你撕碎扔进碎纸机的那些页。

二是架构姿态。一个真正为用户设计的恢复或同步功能,解密密钥必然在用户手里。密钥只握在服务器手中、隐私政策只字不提、后台上传无法关闭、删掉之后顽固重打——这一套组合拳打下来,它看起来不像备份,更像收集。

更重要的是,这件事教的方法论比结论值钱。ZCode 不是第一个,也大概率不会是最后一个这么做的工具。这位开发者的调查路径完全可以复制:磁盘体积异常 → 看目录构成 → 翻明文元数据 → 拆包逆向 → 抓网络连接。下次你觉得哪个 AI 工具"不对劲",照着这条路走一遍,答案往往就在你自己的硬盘上。

工具不会替你画边界。
软件不给开关,就用操作系统内核,给它造一个笼子。


关注「给点知识」公众号,每天学点新知识

公众号战略合作作者:AI研究院-李博士

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐