最近智谱 ZCode 在开发者社区里掀起的这场风波,我一开始以为又是“AI 偷代码”的标题党。结果顺着社区披露的技术细节一路看下来,这次真不是情绪问题:一个 313MB 的加密上传包,其中 86.6% 的体积来自 .git 目录,这件事本身非常值得做一次完整复盘。这篇文章我会从技术角度拆解整个事件:静默上传是怎么被发现的、为什么 .git 占比异常是一个危险信号、普通开发者用什么手段可以自己审计 AI 编程助手的本地行为,以及最后聊一个关键问题——无论你把日志翻得多仔细,有一件事是技术审计永远证明不了的。如果你平时用任何一款 AI 编程工具,这篇都值得在睡前花几分钟读完,因为它涉及的不只是某一款产品的口碑,而是整个行业对“本地数据边界”的共识。
1. 争议事件还原:ZCode 的“静默上传”究竟触发了什么
1.1 用户在本地观察到了什么
社区最初曝出的细节很具体:有人在使用 ZCode 的过程中,通过本地网络监控和代理工具发现,这个工具的进程在后台持续向智谱的服务端传输一个体积很大的数据包,总量大约 313MB。这个传输行为在界面上没有任何提示,没有进度条,没有“正在同步项目”的说明,更没有需要用户点击的授权弹窗。
对客户端应用来说,后台网络请求并不稀奇。IDE 类的工具通常都会定期上报崩溃日志、使用统计、遥测数据,很多开发者早就习惯了安装完成后去设置里手动关掉 telemetry。问题出在数据包的内容上:后续的分析结果显示,上传体是一个加密的压缩包,而在能够拆解出的部分里,大约 86.6% 的体积指向了当前项目的 .git 目录。
这个细节让事件性质发生了根本变化。普通遥测的体积单位是 KB,压缩包里也只是一些事件日志。而一个几百 MB 的加密包,内容主要来自 .git,这已经走出了“改进产品体验”的范畴,进入了“获取本地仓库完整画像”的领域。更要命的是“静默”二字——它不是用户主动触发的“导入项目”或“上传上下文”,而是工具自己在后台悄悄做的。
还有一个容易被忽略的点:上传行为并不是一次性触发,而是在某些操作后反复出现。用户最初可能以为是某个功能模块在正常调用 API,直到流量统计里累计了几百 MB 才警觉。这类“日常操作触发 + 无 UI 反馈 + 后台自动执行”的组合,恰恰是“静默”最让人不舒服的地方——它足够高频,足够自动,也足够隐蔽。
1.2 为什么“上传 Git 仓库”和普通遥测不是一回事
开发工具的上报行为,粗略可以分成三个量级。第一级是指标遥测,比如启动耗时、点击次数、崩溃堆栈,本质是“数字”。第二级是业务元数据,比如最近打开的文件名、搜索词、运行环境参数,本质是“轻量标签”。第三级是完整的源码上下文,包括当前文件内容、整个项目的文件树、甚至 Git 历史。
绝大多数用户对前两类有心理预期,因为安装协议里多少会写一句“收集基础信息用于改进产品”。但第三级完全是另一个量级。前两类泄漏之后,受害面是有限的:被拿走的是一些使用习惯的碎片,无法拼出你正在开发的完整项目。而完整上传一个项目目录,意味着源代码、配置文件、.env 文件里可能存在的密钥、目录结构、文件名、依赖清单,全部离开了本地。
再带上 .git,问题就从“上传了源码”升级成了“上传了源码的完整历史版本”。你的每一个 commit、每一次 revert、每一段曾经存在但后来删除的敏感代码,都跟着打包走了。我在后面第二节会详细拆解 .git 的信息密度,这里先记住一个结论:对任何 AI 编程工具而言,读取当前文件做上下文是合理需求,读取 .git 对象库里的历史版本不是——代码补全根本不需要知道你在三天前 reset 过什么。
2. 拆解上传包的组成:313MB 里 86.6% 的 .git 说明了什么
2.1 一个合理推算:271MB 的 .git 是怎么形成的
先做个简单的算术。假设加密包整体大小是 313MB,其中 86.6% 来自 .git,那么 .git 部分大约是 313 × 0.866 ≈ 271MB,剩余约 42MB 是项目内的其他文件。这个比例放在真实项目里非常不寻常。
正常项目的体积分布应该是怎样的?拿一个大型后端项目举例,几千个源文件加起来可能几十 MB,而 .git 里的 packed 对象通常只有几 MB 到十几 MB,除非仓库里躺着大量二进制历史(比如有人不小心提交过模型文件、安装包、设计稿素材)。换句话说,当你看到一个项目里 .git 占了大头,通常意味着这个仓库要么历史很长、要么包含过重型文件,要么就是打包逻辑对 .git 做了完整收录,一点都没有排除。
如果客户端真的只是“为了给 AI 提供完整项目上下文”,正常的工程做法应该是:对源码文件做内容快照,排除 .git、node_modules、dist 这类可以重新生成的目录。发布过 AI 编程助手的人都知道,代码上传的最小单位要么是单文件,要么是文件级 diff,要么是一个带过滤规则的项目 tar 包。把 .git 整个塞进去,很大概率不是“精心设计后的选择”,而是“没有做排除”的直接后果。
另外提一句,很多人本地仓库膨胀得厉害,是因为曾经把大文件提交进去过,哪怕后来在 .gitignore 里加了规则并删掉文件,Git 对象库里仍然留着那个大对象。一个 270MB 的 .git 目录里,很可能埋着好几个几十 MB 的历史包袱。这些包袱如果跟着一次静默上传到了别人的服务器上,等于你曾经犯过的每一个版本管理错误,都被完整复刻到了远端。
2.2 .git 目录的信息密度:它比源码本身敏感得多
.git 目录对很多开发者来说只是一个“被 Git 自动管理的隐藏文件夹”,但它的内容密度远超想象。拆开看主要有几个部分:
- objects/:所有提交对象的完整数据库,包括每一次历史提交的内容和每一个曾经被 Git 跟踪过的文件版本。只要一个文件曾经被 commit 过,哪怕后来删除,对象仍然留在 objects 里。
- refs/:分支、标签的引用信息,暴露了版本管理结构,比如哪些分支在并行开发、哪些 tag 是发布版本。
- logs/:reflog 记录了本地 HEAD 的每一次移动,包括 reset、checkout、rebase 的操作痕迹,能勾勒出开发者的工作轨迹。
- config:远程仓库的 URL(很可能包含内网 GitLab 或私有仓库地址)、user.name、user.email。
- hooks/:钩子脚本,尤其是 prepare-commit-msg、post-receive 这些,可能包含了 CI 集成、代码规范检查、自动部署逻辑,脚本里的连接串和密钥往往比源码更敏感。
- index、packed-refs、COMMIT_EDITMSG:索引与提交信息,同样有价值。
用一句话概括:工作区只是你办公桌上摊开的几张纸,.git 是整个保险柜里的全部历史档案。上传 .git,等于把保险柜完整抬走了。你曾经在代码里写过 “env 里的密码被 push 进去了,但我立刻删了” 这种补救,在 .git 面前毫无意义——因为任何一个历史提交里出现过的密钥,都永远躺在对象数据库里等着被挖掘。
更现实的问题是信息叠加效应。AI 服务端拿到你的 .git 后,可以同时提取出远程仓库地址、内网工程命名规范、某些模块的演进过程、提交者的邮箱和昵称。单看某一项可能不致命,组合在一起就成了高价值的定向情报。这已经超出了“代码泄漏”的范畴,变成了完整的开发行为画像。
2.3 加密包的双重效果:防偷听与防审计
上传包是“加密”的,这个细节在事件里很关键。加密本身是一个中性技术,但放在“客户端静默生成一个加密压缩包再上传”的语境里,就产生了双重效果。
第一重效果是传输层面的保护。加密可以防止中间人从流量层面直接读取源码,看起来像是“为你好”。第二重效果则微妙得多:加密同时让用户失去了自行核对上传内容的能力。
对比一下你熟悉的正常行为:遥测上报通常用 JSON 明文或者 protobuf,体积小、可解析;崩溃日志走加密通道时,也往往会在本地留下日志文件供用户查看。而 ZCode 这次的做法,更像是把一个打包压缩后的对象当作黑盒直接 POST 出去,用户在本地既没有明文转储,也没有上传内容清单可以核对。用户面临的局面就是:“我知道你传了东西,但不知道传的到底是什么,只能靠抓包、解包或者逆向去猜。”
所有“静默”伤害的核心,就是剥夺用户的知情权与核对权。加密包在技术上是保护传输的,在实际体验中却变成了阻挡审计的帘子。受害者要打开这个帘子,需要付出远高于工具厂商做一次日志的成本。
3. 复现一条审计链路:如何确认你的 AI 编程助手到底上传了什么
这一节我会把审计方法完整展开。搞清楚这类事情不能只靠别人的帖子,你得具备自己验尸的能力。审计分三条链路:流量层抓包、文件系统监控、静态逆向。三条链路配合,基本上能把“读了什么文件、传了什么数据、走了什么通道”拼成一张完整的证据表。
3.1 流量层:用 mitmproxy 打造本地 HTTPS 检查站
流量层回答的是“传了什么”这个核心问题。最常见的工具是 mitmproxy,它能在你的电脑和互联网之间插入一个 HTTPS 中间人。准备工作非常简单:
- 安装 mitmproxy,或者直接用 mitmdump。
- 在系统设置里把 HTTP/HTTPS 代理指向 127.0.0.1:8080。
- 访问 mitm.it,安装并信任 mitmproxy 的 CA 证书到系统信任区。这一步是为了让客户端信任你的中间人证书。
- 启动被测工具,做几次日常操作,比如打开项目、触发 AI 问答、执行代码补全。
- 在 mitmproxy 的 flow 列表里过滤 POST 请求,按请求体大小排序,重点关注体积异常大的流量。
mitmproxy 的过滤语法可以直接用表达式,例如过滤所有发给智谱域名的请求:
~q & ~u zhipu过滤大流量链接:
~q & ~s > 1m这里~s > 1m表示响应体大于 1MB,~q表示请求。实际操作时你会很快看到哪些域名在接收上传。
拿到流量后重点看几个字段:Content-Type 是不是application/octet-stream,Content-Length 是不是有几百 MB,请求路径里有没有upload、sync、archive这类关键字。正常的 JSON 遥测包不会有这些特征。下面是常规遥测和异常上传的对比表:
| 观测点 | 正常遥测特征 | 值得警惕的特征 |
|---|---|---|
| Content-Type | application/json | application/octet-stream |
| 单次请求体 | 小于 10KB | 大于 100MB |
| 请求路径 | /telemetry、/metrics | /upload、/sync |
| 触发时机 | 固定周期上报 | 与打开项目、编辑操作同步 |
| UI 提示 | 安装时已有披露 | 无任何界面提示 |
| 本地缓存 | 通常无 | 本地出现临时压缩包 |
这里要提醒你一个技术障碍:不少现代客户端会做证书固定(certificate pinning),只信任内置证书,不认 mitmproxy 签发的 CA。遇到这种情况,流量内容会直接变成 TLS 握手错误,你只能看到无法解密的原始封包。应对思路有两个:一是用 Frida 这种 hook 框架绕过证书校验,二是干脆跳到静态逆向环节,直接分析客户端代码里的请求逻辑。后面 3.3 会说逆向的事。
3.2 文件系统层:监控进程读取 .git 的真实行为
流量层回答“传了什么”,文件系统层回答“读了什么”。一个工具哪怕没触发上传,只要它开始遍历 .git 下的对象文件,这种行为本身就值得记录。Windows 上首选工具是 Sysinternals Process Monitor,Linux 上可以用 fatrace 或 strace。
在 Windows 上操作的完整思路是:
- 下载 Process Monitor。
- 设置过滤器,条件设为 Process Name 包含 zcode(或者被测客户端名字),Operation 为 ReadFile。
- 再加一条过滤器,Path 包含
.git。 - 开启过滤后,正常打开一个项目并触发 AI 操作。
- 观察事件列表。如果看到大量
objects/pack/*.pack、index、config的读取事件,就说明客户端确实在读 Git 内部数据。
Linux 上用 strace 跟踪进程对文件的访问:
strace -f -e trace=file -p <zcode_pid> -o zcode_file_access.log之后搜索.git关键字:
grep -i ".git" zcode_file_access.log这里我想强调一个实验设计上的技巧:对照实验。在同一台机器上准备一个完全离线的测试项目,项目里放一个体积很大的假 .git 对象(比如往.git/objects/pack/里塞一个 50MB 的随机文件),然后启动客户端并触发操作。如果上传流量出现明显膨胀,说明客户端是实时读取目录内容打包,而不是读取某个缓存清单。这个实验风险很低,但得到的结论非常有说服力——它能直接帮你判断上传行为与项目内容之间的因果联动。
3.3 静态逆向:从 Electron 应用里翻出上传逻辑
ZCode 这类 AI 编程工具很多基于 Electron 或 VS Code 内核。Electron 应用的所有 JS 层逻辑都打包在一个app.asar文件里,解开就能直接读代码。这让静态逆向的门槛低了不少。
先找到安装目录下的 app.asar,一般在resources文件夹里。然后执行:
npx asar extract app.asar app解包之后,在 JS 文件里搜索关键字。这是我的常用搜索组合:
grep -rn "git" app/dist --include="*.js" | grep -i "archive\|zip\|tar\|upload" grep -rn "encrypt\|AES" app/dist --include="*.js" grep -rn "telemetry\|trackEvent" app/ --include="*.js"你可以直接在搜索结果里看到上传逻辑:是调用了git archive生成压缩包,还是用fs.cpSync把整个目录复制到临时目录,再或者用child_process.exec('tar -czf')做打包。也可以找到上传 API 的端点和加密函数。
不同的结果对应不同的结论:如果代码里有“排除 .git”的逻辑,说明产品方确实考虑过仓库数据属于敏感边界;如果整段上传代码直接遍历项目根目录、连打包函数的名字都写的是packProjectDir,那 86.6% 的 .git 占比就有了直接的技术注脚,说明这是一个工程决策,而不是偶然。
有一点要提前说清楚:并不是所有工具都会把关键逻辑放在 JS 层,有些会编译成 C++ 插件或者二进制模块。但网络请求的组装、文件路径的拼接、域名的配置,这些通常很难全部藏进二进制里。静态逆向至少能帮你定位到一个大方向,再结合抓包结果,证据链就能闭合。
3.4 审计能确认的结论边界
把三条链路走完,你能拿到一份行为清单:
- 客户端是否在无人授权时读取过 .git 目录。
- 读取到的 Git 对象是否被打进压缩包。
- 上传包是否加密、实际发往哪个域名。
- 上传前界面上有没有披露或授权流程。
- 有没有开关可以完全关闭该行为。
- 哪些操作会触发上传,是打开项目还是执行补全。
这些属于事实层证据,不需要偏好判断就能回答“数据泄露是否发生”。你自己跑完这套流程得到的结果,完全可以作为一手材料用于后续讨论——比起转发别人的截图,自己的抓包记录更有说服力。
4. 审计证明不了的那件事:恶意还是缺陷,技术无法给出答案
4.1 行为证据和意图证据之间的鸿沟
我在复盘这次事件时,最深的感触是:技术取证有一个天然边界——它只能告诉你“发生了什么”,永远无法直接告诉你“当事人为什么这么做”。
一个客户端在后台把 .git 打包加密上传,可能存在三种完全不同的解释:
- 恶意数据窃取:目标是获取仓库的完整代码与历史信息,加密是为了规避审计。
- 产品设计失误:为了让 AI 能处理“整个项目上下文”,直接对根目录做了递归压缩,忘了排除 .git,加密只是传输安全的标准配置。
- 激进的数据收集策略:团队认为上传完整项目甚至 Git 历史能改善 AI 问答效果,但没有向用户充分披露,也缺乏最小化数据原则。
这三种解释,在流量和文件系统层面的证据是完全相同的。你审计得再彻底,也只是把一个“行为”固定下来,无法从行为本身反推出哪一种解释是真相。
要区分它们,需要行为之外的补充材料:产品文档里的隐私策略措辞、官方后续回应、更新日志里的修改记录、甚至是开发者访谈。这些信息虽然能帮助判断,但已经超出了“审计”的范畴,进入了组织行为分析的领域。文字和承诺可以被伪造,本地方志不会说谎,但“动机”这件事,永远只能推测,无法验证。
这个矛盾在安全领域并不新鲜。取证学早就承认,证据链能还原行为,却还原不了设计者的内心。所以我在面对这件事时,选择把“行为证据”和“意图推测”分开放置:行为证据要钉死,意图推测则留给产品方回应和市场反馈去消化。
4.2 “静默”的定义之争:合理预期与数据最小化
除了意图,“静默”本身的边界也值得掰开揉碎说清楚。用户对 AI 编程工具产生的数据上传,是有一套逐级递减的合理预期的。
比如,用户用 AI 做代码补全时,能接受“当前文件内容 + 光标上下文”发往服务端,因为这是功能必需。用户能勉强接受“打开项目时同步一次工作区文件”吗?部分人能,前提是安装协议里白纸黑字写清楚。但几乎没有用户能接受“连 .git 的历史对象库一起上传”——因为 AI 补全与历史版本没有直接关系。
这里面有一个在数据隐私领域很核心的概念,叫数据最小化。任何合法合规的数据收集,都应该只收集完成任务所需的最少数据。把这个原则套在 AI 编程工具上:代码补全不需要完整 Git 历史,不需要内网仓库地址,不需要未被当前会话引用的大文件。
当一个上传包里出现 86.6% 的 .git 时,它已经明显越过了这条线。无论出发点是什么,这都够得上“数据过度采集”的印象。做产品的人应该从这次事件里吸收教训:技术上“只是打包目录时顺手把 .git 也打进去了”,用户感知到的却是“你把我整个家抄走了”。技术细节和用户感知之间的鸿沟,往往就是这样变成公关危机的。
开发者做工具时,不妨把“数据最小化”当成一种默认习惯:先问自己,这个功能最底层需要哪些数据,再多一样都不拿。就这样一个朴素的原则,能避免绝大多数静默上传的争议。
4.3 三个立刻可以落地的防护措施
讲完审计和边界,还是得落回怎么保护自己。我把自己在本地环境里的做法整理成了三个档位,按投入成本排序,大家可以按需取用。
第一个档位:隔离运行态。重要项目尽量在虚拟机、容器或一个独立的“离线专用”环境里开发,需要联网再切回来。对安全要求极高的团队,可以让 AI 工具只操作一个影子目录,把需要喂给 AI 的代码手动同步进去,把敏感仓库和不信任的 AI 客户端物理隔离。这个方案的缺点是麻烦,优点是不依赖任何一款工具的自觉。
第二个档位:显式控制开关。安装任何 IDE 或插件后,第一时间进设置检查 telemetry、crash report、auto update、云同步以及 AI 数据使用说明,能关的全部关掉。不要相信“默认打开是为了你好”这种话——默认值就是产品希望你接受的值。同时留意有没有纯本地模式选项,敏感项目一律跑本地模型,数据不出机器,争议自然消失。
第三个档位:定期审计。给自己定一个节奏,每季度甚至每次大版本升级后,用前面讲的方法对常用开发工具做一次流量抽查。很多工具会在升级中悄悄修改默认隐私选项,定期审计能及时发现这种变化。把它当成代码仓库的定期安全检查,养成习惯后只需要十几分钟。
这三个档位之间没有矛盾,完全可以叠加。我个人的做法是:日常项目开第二个档位,核心机密项目直接上第一个档位,每隔一两个月补一次第三档位的抽查。这套组合不是为了针对某一款工具,而是面对整个 AI 辅助开发生态的通用安全基线。
最后再分享一点实际心得:在这次复盘里,我最深的一个体会是,技术审计能帮你把“发生了什么”钉在证据里,却无法替你回答“这到底算不算恶意”。遇到静默上传事件,我的习惯是先不看评论区情绪,先跑一遍流量层和文件系统的审计,确认事实,再决定要不要继续信任这款工具。ZCode 事件给所有开发者的提醒,不是“把 AI 工具卸了”,而是对本地数据边界保持清醒。你本地的每一份代码、每一个 .git 对象,都不该成为某个客户端默认配置里能够悄悄带走的行李。以后每次打开一个新工具,我都会多想一步:它正在访问哪些文件,它要往哪里上传,以及这些行为有没有经过我的允许。希望更多工具厂商把“数据最小化”写进默认代码,而不是写进道歉声明。