ZCode 静默上传你整个 Git 仓库:加密不等于安全,密钥在谁手里才算数?
如果你装了智谱的 AI 编程助手 ZCode,你的代码可能在你毫不知情的情况下被打包传到了它的云端——不是几行代码,而是整个 Git 仓库:LFS 大文件、历史提交、甚至疑似 .ssh 密钥和 .env 环境变量。
9 月 18 日,开发者 ferstar 在清理磁盘时发现,~/.zcode 隐藏目录下躺着一个约 313MB 的加密文件包,旁边还有一份 42,411 个文件的明文清单。顺着清单往下翻,越来越多开发者意识到:这不是个例,而是一整套「静默上传」机制的冰山一角。
这件事的争议点不在「AI 工具收集数据」——业界早就默认了。争议在于三点:没问过我、传的是整个仓库而不是代码、密钥类文件疑似也在其中。加密在这里不解决信任问题,只解决「事后查不出问题」的问题。
一、72 小时:从一张磁盘清单开始
我按公开报道的时间线把事件捋了一遍,先看全貌:
| 时间 | 关键节点 | 事实 |
|---|---|---|
| 9/18 上午 | 曝光 | 开发者发现 ~/.zcode 下约 313MB 加密包 + 42,411 个文件明文清单,.ssh 密钥与 .env 疑似在列 |
| 9/18 下午 | 智谱首次回应 | 公开致歉,称「Repo Wiki」默认开启导致上传,已修复;承诺开源并接受第三方审计 |
| 9/21 | 开源与审计 | ZCode 正式开源;信通院、绿盟科技审计确认 zcode-prod 桶零数据;v3.14.0 移除 RepoWiki 上传链路;MaaS 上线「数据内容不留存」 |
这里最值得注意的不是「传了」,而是节奏:曝光当天道歉、第三天就给出「第三方审计零数据」的结论。对于一次涉及上万开发者代码文件的泄露传闻,这个公关速度不算慢;但对用户来说,「云端零数据」验证的是结果,验证不了传输过程中的拷贝是否被留存——这两件事在技术上完全不同。
二、技术链路:一次「合规」的静默上传
根据开发者公开的逆向分析,ZCode 客户端本地的上传链路是这样的:
- 客户端向
zcode.z.ai请求上传凭证,同时下发 RSA 公钥; - 本地用 AES-256-CTR 加密打包数据,再用 RSA-OAEP 包裹 AES 密钥;
- 加密包直传阿里云 OSS 桶;
- OSS 回调登记上传事件,云端完成登记后解密私钥只在云端。
链路本身设计得很严谨:数据流全程加密、私钥不下发客户端、直传对象存储不经中转。技术设计越严谨,「静默上传」这个行为就越显得刻意——这是一套为「合规传输敏感数据」设计的生产级链路,而不是什么误触发的缺陷。
而且它不是一次性行为。逆向数据显示:客户端上传失败后自动重试了 564 次,单个会话最多捕获 62 次上传动作。显然,这不是某个开关的偶发副作用,而是有一套完整的上传状态机在兜底。
三、上传了什么:Git 历史占 86.6%,源码只有 13.4%
最扎心的是清单本身。42,411 个文件里,按体积拆开看:
.git/lfs(LFS 大文件缓存):56.8% ——通常是设计稿、数据包、二进制资产;.git/objects(提交对象与历史版本):29.6% ——你的每一次 commit,包含中间状态和已删除的敏感内容;- 源码与文档:13.4%;
.git其他(logs、分支引用):0.2%。
Git 历史合计占了打包内容的 86.6%。这意味着什么?意味着被上传的最有价值的部分,不是「你现在的代码」,而是你删掉的、改过的、临时提交过的所有历史——包括你可能已经移除的密钥、.env、数据库备份。Git 历史不删就永远在,而 git filter-branch / git filter-repo 重写历史这件事,绝大多数开发者根本没做过。
更值得注意的是过滤逻辑的顺序缺陷:逆向分析显示,历史记录目录的放行判断先于 PEM/KEY 密钥过滤与 1MB 体积限制。也就是说,如果一个文件名同时落在「历史记录目录」和「密钥黑名单」里,放行逻辑会先命中。密钥过滤形同虚设——这能解释为什么 .ssh、.env 会出现在清单里。
四、加密≠安全:三个问题比密钥算法更重要
网上很多讨论在辩「AES-256-CTR + RSA-OAEP 够不够安全」,这是跑偏了。传输加密解决的是「链路窃听」,而事件的核心争议是信任边界。三个问题,每个都比密码学强度更要命:
- 密钥在谁手里? 解密私钥只存在云端。也就是说:无论本地怎么加密,智谱云端都保留了解开一切的能力。安全意义上的「你的数据」在离开你机器的那一刻,就已经不是你的了。
- 用户有没有知情同意? 事件的爆发点恰恰是「Repo Wiki 默认开启」。默认开启 = 用户在安装时从未主动选择过上传粒度。争议文章里流行一句话:「免费 3 亿 tokens 换你整个仓库」——话难听,逻辑没错。
- 「未检测到存储」能证明什么? 官方结论说审计确认云端零数据。但审计验证的是「最终没有留存」,验证不了「传输过程有没有被第三方截获、日志有没有记录、样本有没有用于训练」。这两个问题的性质完全不同。
拿行业里另外两个「AI 工具收集数据」案例放一起看,争议的核心就更清楚了:
| 工具 | 行为 | 用户可见度 | 官方说法 |
|---|---|---|---|
| ZCode | 静默打包整个 Git 仓库上传云端(含密钥疑似) | 不可见,默认开启 | 已修复、开源、第三方审计零数据 |
| Grok Build | 将整个项目打包上传 Google Cloud(含用户禁止读取的文件) | 项目上传时有可见进度 | 社区曝光后聚焦「权限边界」讨论 |
| Claude Code | 本地回传位置/身份等元信息 | 不可见 | Anthropic 工程师承认「是有意的实验」 |
三起事件有一个共同模式:AI 时代,工具默认「为你做更多」,而「更多」的边界是由厂商单方面划定的。用户协议写了 100 页,不如一次默认开启的勾选框说明问题。
五、两分钟自查:你的仓库有没有暴露面
不管你是 ZCode 用户、其他 AI 编程工具用户,还是只用 Git 的开发者,这套自查都值得跑一遍,估算「如果打包发生,会被带走什么」:
# 1. 看看本地有没有这类工具的隐藏数据目录
ls -d ~/.zcode ~/.zcode_data ~/.continue ~/.codex 2>/dev/null
# 2. 这些目录到底占多大(别小看,313MB 就是这么来的)
du -sh ~/.zcode 2>/dev/null
# 3. 敏感文件是否「意外」躺在会被打包的目录里
find ~ -name '*.pem' -o -name '*.key' -o -name '.env' 2>/dev/null | head -20
# 4. 你的 .git 有多大(Git 历史 = 打包大头)
du -sh .git 2>/dev/null
更系统的做法,我写了一个只读的暴露面评估脚本,不读取任何文件内容,只统计文件名与体积:
#!/usr/bin/env python3
"""audit_pack_surface.py — 评估目录中会被「整仓打包上传」波及的暴露面
用法: python3 audit_pack_surface.py [--path 目录]
只统计文件名与体积,不读取任何文件内容。
"""
import argparse
import os
from pathlib import Path
SENSITIVE_SUFFIX = {".env", ".pem", ".key", ".p12", ".pfx"}
SENSITIVE_NAMES = {"id_rsa", "id_ed25519", "id_ecdsa", ".netrc", ".npmrc"}
SUSPICIOUS_DIRS = {".zcode", ".zcode_data", ".continue", ".codex", ".cursor"}
def human(n: int) -> str:
for unit in ("B", "KB", "MB", "GB"):
if n < 1024:
return f"{n:.1f}{unit}"
n /= 1024
return f"{n:.1f}TB"
def dir_size(p: Path) -> int:
total = 0
try:
for root, _dirs, files in os.walk(p):
for f in files:
try:
total += os.path.getsize(Path(root) / f)
except OSError:
pass
except OSError:
pass
return total
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--path", default=".")
args = ap.parse_args()
root = Path(args.path).expanduser().resolve()
if not root.is_dir():
print(f"路径不存在: {root}")
return
git_dir = root / ".git"
git_size = dir_size(git_dir) if git_dir.is_dir() else 0
print(f"[1] .git 体积: {human(git_size)}")
if not git_dir.is_dir():
print(" 当前目录不是 Git 仓库,跳过后续 .git 分析")
print("\n[2] 高危敏感文件(打包即泄露):")
hits = 0
skipped_git = False
for p in root.rglob("*"):
if not p.is_file():
continue
try:
rel = p.relative_to(git_dir)
skipped_git = True
continue
except ValueError:
pass
if p.suffix.lower() in SENSITIVE_SUFFIX or p.name in SENSITIVE_NAMES:
try:
size = p.stat().st_size
except OSError:
continue
print(f" {p.relative_to(root)} {human(size)}")
hits += 1
if skipped_git:
print(" (已跳过 .git 内部文件;若同步逻辑未排除 .git,历史本身就会被打包)")
print(f" 共 {hits} 个高危文件")
print("\n[3] 已知数据收集类产品目录:")
for d in SUSPICIOUS_DIRS:
p = root / d
if p.is_dir():
print(f" {d}: {human(dir_size(p))}")
print("\n完成。以上仅为暴露面提示,不构成安全结论。")
if __name__ == "__main__":
main()
诊断结果里,.git 体积越大的项目越要注意——你觉得自己只分享了「代码」,实际上分享的是整个项目的历史。历史里有没有曾经提交又删除的密码、一次性密钥、云服务 AccessKey,你自己可能都不记得了。
六、智谱回应之后,还剩哪些问题没回答
复盘整个事件,智谱的回应质量在一众中国 AI 公司里算不错:道歉及时、开源承诺兑现(9/21 仓库确实公开了)、第三方审计进场。但抛开「事件本身」,行业层面的问号一个都没消失:
- Repo Wiki 这个功能的价值是什么? 一个向 AI 编程工具上传「仓库 Wiki」的功能,为什么会触发「整个
.git目录 + 工作区全部文件」的打包?如果上传的真是 Wiki,打包内容里 86.6% 的 Git 历史怎么解释? - 「默认开启」的开关,设计时有没有人评审过? 任何涉及出站数据传输的功能,默认值应该是「关」——这个共识在安全行业存在几十年了,为什么 AI 工具时代会倒退?
- 审计覆盖了存储,覆盖了训练吗? 信通院与绿盟科技确认
zcode-prod桶零数据,很好。但公开信息没有说明:上传样本是否进入过任何训练集、中间态缓存、日志系统。「没存下来」和「没用过」是两个需要分别回答的问题。
七、结语:Agent 时代缺的不是加密,是数据审计规则
ZCode 事件最值得记住的,不是「智谱干了件坏事然后道歉」,而是它给整个 Agent 生态立了一个反面教材:
当工具被设计成「自作主张」时,技术越严谨,用户越危险。
链路加密、直传 OSS、云端密钥——这套东西放在「用户主动上传文件」的场景里是教科书级的传输方案;放在「用户不知道自己在被上传」的场景里,就变成了教科书级的瞒报方案。同样的技术,信任模型完全不同。
给所有用 AI 编程工具(包括但不限于 ZCode、Copilot、Cursor、各种国产 Agent)的开发者三条实际建议:
- 安装任何 AI 工具时,先去找「上传」「数据」「遥测」相关的开关,默认全关,用到再开;
- 敏感项目(含密钥、客户数据、未公开商业计划)用隔离环境开发,别让 Agent 工具看到整个工作区;
- 定期用上面的脚本扫一次暴露面,把
.git历史里的敏感内容清掉——git filter-repo不是高深课程,是基本功。
AI 编程工具会越来越强,能力边界也会越来越宽。「它默认做了什么」这件事,值得每个开发者在下一次 npm install 之前,花两分钟搞清楚。
本文事实均来自公开报道与开发者公开的逆向分析(新浪科技、IT之家、中国经营报及 GitHub 公开讨论,2026-09-18 至 09-21),自查脚本为通用安全工具,与本事件无直接归属关系。