Z.ai ZCode 静默上传代码库:把 Agent 的出网行为管起来
💡 工具推荐:正则测试器、gitignore 生成器、Base64 编解码
2026 年 9 月,多名开发者发现 Z.ai(智谱)的桌面编码 Agent ZCode 在用户不知情的情况下,把整个本地代码工作区打包、加密后上传到云服务器。整个仓库——包括 .git 历史——被一起带走了。路透社 9 月 21 日报道,Z.ai 随后禁用了该助手的部分功能并公开致歉。这起事件的技术教训很朴素:当「代码库索引」这种功能默认开启、并且索引在云端构建时,你的仓库会成为一次数据传输事件。而大多数团队,根本不知道自己每天使用的开发工具到底往外面发什么。
看不见的出网流量,才是最难治理的那部分
一、发生了什么:一条时间线
9 月 18 日,一名付费订阅者公布了抓包记录,显示 ZCode 把已登录用户的整个工作区打包上传,包括 .git 历史、LFS 缓存、reflog 和应用配置——根据该技术分析者的估算,约 87% 的上传内容就是 .git 本身,最终流向的对象存储是阿里云 OSS。开发者随后在 X 与小红书上指出两个更棘手的问题:其一,这个「代码库索引」功能默认开启,且没有可关闭的开关;其二,隐私政策里事先没有任何相关说明。9 月 21 日,Z.ai 表示问题源自默认开启的 Codebase Indexing 功能,已修复相关软件漏洞并禁用部分特性,同时把 ZCode 开源、承诺更透明,并为开发者与企业启用了零数据保留(zero-data retention)。
# 1) Default-deny egress for the agent process, then allowlist only
# the endpoints its job actually requires. MacOS/Linux sketch:
# block everything, permit the model API, deny cloud object storage.
nft add table inet agent
nft 'add chain inet agent output { type filter hook output priority 0; policy drop; }'
nft add rule inet agent output ip daddr 127.0.0.0/8 accept
nft add rule inet agent output tcp dport { 443 } accept # then tighten below
# A code indexer has no business reaching an object-storage bucket.
# If it can POST your repo to one, assume it eventually will.二、为什么「索引」等于「什么都传」
要理解这次事件的必然性,得看索引的工程原理:要让模型回答关于你代码库的问题,它需要你的代码。如果嵌入(embedding)与索引构建在云端完成,那么整个仓库就会一起上传。而「整个仓库」通常比你以为的大得多:除了源码,还有完整的提交历史、reflog、LFS 缓存、未跟踪文件、本地配置文件,以及那些你以为早就被 .gitignore 排除、实际上仍被跟踪的敏感文件。也就是说,问题不在于某个厂商「偷偷多传了一点」,而在于「云端索引」这个设计本身,默认需要一个完整的上传动作。用户随后的质疑也集中在这里:他们发现数据用一个只掌握在 Z.ai 后端的私钥加密,这意味着他们既打不开、也无法独立核实自己的文件是否被删除。
// 2) Audit what an indexer WOULD send before you enable it.
// Enumerate the workspace minus excludes, and weigh .git separately.
import { execSync } from "node:child_process";
import { statSync } from "node:fs";
const EXCLUDES = [".env", "node_modules", "dist", "*.pem", "*.key"];
const files = execSync("git ls-files -co --exclude-standard", { encoding: "utf8" })
.split("\n")
.filter((f) => f && !EXCLUDES.some((e) => f.endsWith(e.replace("*", ""))));
let bytes = 0;
for (const f of files) bytes += statSync(f).size;
const gitBytes = Number(execSync("du -sb .git | cut -f1", { encoding: "utf8" }).trim());
console.log("tracked+untracked files:", files.length);
console.log("workspace MB:", (bytes / 1e6).toFixed(1));
console.log(".git MB:", (gitBytes / 1e6).toFixed(1), "- history travels too");三、后续与争议:一次坦诚的、也一次撤回的指控
事件并非只有一面。Chengming Technology 曾声称公司有六个编码工作区在未获同意的情况下被 ZCode 上传,其中包含完整源代码、数据库口令和员工个人信息;随后该公司在周一撤回了这一说法,表示「证据有误」。Z.ai 则在周一表示,由工信部下属的 IT 标准研究机构与中国网络安全公司 NSFOCUS 共同完成的独立安全评估发现,用户代码数据已被删除、并未被云平台保留。把这两条并排看,其实是一个很好的提醒:在安全事件里,「未经证实的指控」和「未经证实的清白」同样需要被标注来源——包括本文引用的技术分析,也应当被视为分析而非官方结论。同一时期,中国网络监管机构发布了更新版 AI 安全框架,警告模型可能存在关机抵抗、欺骗评估者与沙箱逃逸;而 Z.ai 上月也曾因安全原因推迟发布 GLM-5.3,成为首个明确以安全为由延后发布的中国实验室。
# 3) Keep indexing local. Embed on your machine, ship nothing.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2") # runs locally, no API
def index_repo(paths):
index = {}
for path in paths:
text = open(path, "r", errors="ignore").read()
index[path] = model.encode(text[:8192]) # vectors stay on disk
return index
# Rule of thumb: if the feature is called "cloud indexing" and it is on
# by default, that is the first setting to turn off.四、技术治理:四道可落地的防线
无论厂商如何解释,结论对你都是一样的:必须自己管住出网。第一道防线是默认拒绝的出网策略——为 Agent 进程单独设一条网络规则,只放行它完成本职必须触达的端点,明确禁止它访问对象存储;一个代码索引器没有任何理由能够 POST 你的仓库到某个 bucket。第二道防线是「先审计、再启用」——在任何索引功能打开之前,先用脚本枚举工作区实际会被发送的文件,并把 .git 的体积单独算出来,让「历史也会一起走」这件事变得可见。第三道防线是把索引放到本地:用本地嵌入模型在你的机器上构建向量,数据不出门。第四道防线是密钥预检:在嵌入或上传之前跑一遍模式扫描,发现疑似密钥就直接中止,然后回头检查 .gitignore 是否真的排除了你以为已排除的东西——被跟踪过的密钥属于历史,只能轮换,不能靠删除。
# 4) Scan for secrets BEFORE anything gets embedded or uploaded.
# A short, boring gate catches most of the damage.
PATTERNS=('AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|sk-[A-Za-z0-9]{20,}|BEGIN [A-Z ]*PRIVATE KEY')
if grep -RInE "$PATTERNS" --exclude-dir=.git --exclude-dir=node_modules . ; then
echo "secrets found - refusing to build any index" >&2
exit 1
fi
# Then verify your .gitignore actually excludes what you think it does.
# Tracked secrets are history, not configuration; rotate them anyway.五、落地代码:从防火墙到审计日志
第一段给出网规则:默认拒绝,只放行必要端点。第二段是「会发生什么」的审计脚本:枚举 tracked 与 untracked 文件、累加体积、把 .git 的字节数单独打印出来。第三段是本地索引:用本地嵌入模型构建向量,让数据留在磁盘上。第四段是密钥预检门禁:用一组常见密钥模式扫描仓库,命中就拒绝构建任何索引。第五段是出网审计表:记录每一次对外请求的主机、路径、发送字节数、是否包含仓库数据、以及厂商声称的保留策略——然后对「新主机」「首次包含仓库数据的上传」「无法独立核实的保留声明」这三类事件告警。这五段代码不高级,但它们把「看不见」变成「看得见」,而这是所有治理的前提。
// 5) Log every outbound call the agent makes, and diff it weekly.
// What you cannot see, you cannot govern.
function recordEgress(req, bytesOut, classification) {
return db.egress.insert({
ts: new Date().toISOString(),
host: new URL(req.url).host,
path: new URL(req.url).pathname,
method: req.method,
bytesOut,
containsRepoData: classification.repoData, // true = escalate
tool: process.env.AGENT_NAME,
retentionClaim: classification.retention, // "zero" | "unknown"
});
}
// Alert on: new host, first upload containing repo data, or a tool
// whose retention claim you cannot independently verify.六、清单:把 Agent 的对外通信当成数据流
五个检查项。第一,你能不能列出你正在使用的每一个编码 Agent、插件和 MCP 服务器会访问哪些域名?第二,其中哪些功能默认开启、且默认上传数据?第三,你的索引与嵌入是在本地还是云端构建?第四,你有没有一道密钥预检门禁,挡在「索引」之前?第五,当厂商说「数据已删除」时,你有没有任何办法独立核实?这五个问题里,最容易被忽略的是第二个:默认值是产品决策,不是安全承诺。ZCode 的核心缺陷不是加密算法不强,也不是谁的实现有多差,而是「默认开启 + 云端构建 + 没有开关」这个组合让人无从选择。Agent 时代最该建立的一条肌肉记忆是:一个工具能不能读你的代码是一回事,它会把你代码发到哪里是另一回事——后者才真正需要你签字。
把索引放在本地,让数据不必上路
📌 常见问题 FAQ
ZCode 到底上传了什么?
据开发者公布的抓包与该事件的技术分析,ZCode 把已登录用户的整个工作区打包上传,包含 .git 历史、LFS 缓存、reflog 与应用配置,据估算约 87% 的载荷是 .git 本身,最终流向阿里云对象存储。
Z.ai 是怎么回应的?
据路透社报道,Z.ai 称问题源自默认开启的 Codebase Indexing 功能,已修复漏洞、禁用部分特性并公开致歉,同时开源了 ZCode、为开发者与企业启用零数据保留,并公布了独立安全评估结论。
为什么「代码库索引」会连 .git 历史一起传?
因为云端索引需要完整源码才能构建立量与索引。仓库不只是源码,还包括提交历史、reflog、LFS 缓存、未跟踪文件和本地配置;如果索引在云端构建,一次上传就会把这些全部带走。
我该如何防止编码 Agent 私自外传代码?
四道防线:为 Agent 进程设默认拒绝的出网策略并只放行必要端点;启用任何索引功能前先审计实际会被发送的文件与体积;把嵌入与索引放在本地构建;在索引之前加一道密钥预检门禁。
厂商说「数据已删除」,我该怎么核实?
先看有没有可核实的证据链,比如第三方独立评估或可审计的删除凭证;同时反思架构——如果数据加密用的密钥只有厂商持有,用户就无法独立核实内容与删除。更稳的做法是从一开始就不让数据离开本机。