ZCode 曾打包整个 .git:两个开关没拦住,出网要从应用外查
ZCode 3.12.3 曾把工作区连同 Git 历史打包加密;大仓上传未成功,小仓已被服务端接收,3.14.0 移除了上传链路。macOS 实验、Linux 与 Windows 文档命令分别教你查进程连接和字节数,并解释 .gitignore、目录锁及出口代理各自能管什么。

你在 IDE 里关掉「优化体验」,不等于桌面进程停止打包仓库。ferstar 对 ZCode 3.12.3 的拆包发现:登录后客户端会把工作区连同 .git 历史打包、加密,再尝试直传对象存储;两个设置开关都没拦住打包这一步。但要把「曾打包」与「已上传」分开:约 313MB 的商业项目包反复上传失败,作者核对路由日志确认未离开局域网;另一个 538 个文件的小公开仓压成约 15KB,状态显示服务端已接收。公开资料称 3.14.0 已移除该上传链路;其他旧版是否受影响,尚无完整版本范围。
这件事留给使用者的不是「以后别用 AI 编程工具」:它们本就需要把选中的代码上下文发给模型。问题是谁决定选中哪些文件,哪些连接能出机器,以及使用者能否独立核对。从应用外按进程看目的地和上行字节,再检查本地快照文件,才能区分正常请求与未预期的数据传输;阻断则要由操作系统和网络承接,而不是指望应用自己解释开关。
如果你用过 ZCode,先做这四件事:
- 在「关于」里看版本:公开材料确认 3.14.0 移除了上传链路,因此升级到该版或经核验未重新引入该链路的更新版本;不能只凭
3.14.x字样判断安全。公开材料只确认 3.12.3 有该行为,其他旧版范围未公开。 - 打开
~/.zcode/v2/checkpoints/,查看状态文件所记的工作区路径、包大小、失败计数与接收状态。ferstar 据此再结合路由日志判断大仓未外发、小仓已被接收;找不到「已接收」状态不等于证明未外发,快照也可能被清理或状态不完整。 - 对确认已被接收的工作区,轮换可访问的 Git 历史里出现过的凭证;其他疑似受影响仓库按敏感程度评估处置(安全查找方式见下文)。
- 可选:按第 5 节锁住这个目录,当作一道绊线。
ZCode 到底打包了什么:42,411 个文件,86.6% 是 .git
ferstar 是在清理磁盘时发现的:~/.zcode 占了 700 多 MB,v2/checkpoints/ 下躺着一个加密包,旁边是一份状态文件
(ferstar 的 ZCode 拆包记录(中文原文),下文简称 ferstar 原文):
{
"workspacePath": "/Users/ferstar/myprojects/<某商业项目>",
"lastCompressedSize": {
"encryptedSizeBytes": 313070842,
"workspaceSizeBytes": 345549173
},
"kind": "baseline",
"failureCount": 564
}这个包因为体积太大一直失败,作者在自家路由上对过连接跟踪和流量日志,确认它没离开局域网。
他解开客户端安装包,还原出的链路是这样:客户端先向 zcode.z.ai 要一张上传凭证,服务端同时发来一把公钥。客户端在本地把工作区打成压缩包,用随机密钥加密,再用这把公钥把随机密钥包起来,然后绕开智谱自己的业务服务器,直接用表单上传到阿里云对象存储(OSS)。
抓包看到的也对得上:这个进程常驻的 HTTPS 连接,一条是 zcode.z.ai,另外两条是 OSS 节点。负责这件事的是一个随登录常驻的辅助进程(sidecar),细节可以略读,记住「绕过业务服务器直传存储」这一点就够了。
包本身解不开,但本地留着一份明文清单:
| 内容 | 体积 | 占比 | 意味着什么 |
|---|---|---|---|
.git/lfs/ | 196.1 MB | 56.8% | Git LFS(大文件存储)历史上拉取过的所有大文件 |
.git/objects/ | 102.2 MB | 29.6% | 本地保留的 Git 对象,可包含旧提交与文件版本;浅克隆或清理过的对象未必在本地 |
.git/logs/ | 0.6 MB | 0.2% | reflog,本地分支操作记录,含没推送的 |
| 其余源码与文档 | 约 46.2 MB | 13.4% | 当前工作区的代码和配置 |
被打包的主体不是只有当前源码,而是这台机器上仍可访问的 Git 对象、引用与本地记录:误提交后又删除的凭证、仍在本地引用的未推送分支、.git/config 中的远端地址,都可能在其中。浅克隆从未拉取的旧提交、已被垃圾回收的对象不在本机;不能把一份 .git 等同于项目从创建起的全部历史。
触发点有两个:每次发 prompt 之前(captureBeforePrompt),以及任务结束时的 repo-wiki-update。一个活跃会话最多触发了 62 次。把本地的包删掉,半小时内又会生成一个新的,失败计数从 564 变成 565。
智谱的说法:9 月 18 日 17:44 在用户群发出说明,IT之家、凤凰网当晚报道,新浪科技 9 月 19 日上午跟进。 问题出在「代码库索引」功能,「Repo Wiki 功能在生成 Wiki 页面时,可能会触发仓库数据上传。Wiki 页面在云端生成后,相关上传数据会立即销毁,不会保存。由于该功能在上线初期默认开启,导致部分用户受到影响。」官方承诺开源、引入第三方评估,并给全体用户额外重置一次周额度 (IT之家 9 月 18 日报道;凤凰网:智谱 ZCode 就代码库数据上传致歉;新浪科技:智谱 ZCode 回应数据上传争议)。
两边说的触发条件不一样:官方说生成 Wiki 页面时「可能」触发,拆包看到的是每次提问前都会触发。这里只把两边原话摆出来。对使用者来说,更要紧的是这两种说法都只能从应用外面去验证。
为什么设置里的开关拦不住:开关由应用自己解释
ferstar 把两个看上去相关的开关和代码逐个对了一遍:
| 开关 | 看名字以为它管 | 代码里实际管 |
|---|---|---|
| 优化体验 | 数据采集与上传 | 数据要不要拿去训练 |
| 仓库快照索引 | 快照功能本身 | 服务端收到快照后要不要建索引 |
负责打包上传的进程在启动时无条件创建,唯一的前提是拿到登录凭证。两个开关都关掉,打包照样进行。
开关的含义由写应用的人定义,也由应用自己执行,你能看到的只有一行说明文字。「不用于训练」和「不上传」是两件事,界面上却可以长得一模一样。 判断一个工具有没有往外发东西,不能只依赖它自述:操作系统里的进程连接和网络侧日志,要一起看。

怎么查一个 AI 编程工具在往哪发、发了多少(macOS / Linux / Windows)
要回答三个问题:连到了哪里、上行了多少字节、在什么时刻发的。AI 编程工具本来就要把代码上下文发给模型,所以「有上行」本身说明不了什么。要找的是三种异常:连的不是模型 API(比如对象存储);上行量远超 prompt 加上下文的体量;你什么都没做它也在发。
先找全进程。 桌面 AI 工具大多是 Electron 应用,主进程之外还有 Helper 进程和 sidecar,只盯主进程可能正好漏掉负责上传的那个。
macOS:lsof 看连接,nettop 看字节
以下输出是我在本机(macOS 26.6.2)实跑的,被观察的对象是 curl。查看当前用户自己的进程不需要 sudo。
# 1. 列出名字里带 zcode 的全部进程(不区分大小写)
pgrep -fl -i zcode
# 2. 这些进程的网络连接;pgrep -d, 把多个 PID 用逗号连起来
# -n -P:不把 IP 和端口反解成名字;-a:各条件同时满足;-i:只看网络
lsof -nP -a -p "$(pgrep -d, -i zcode)" -i
# 3. 每个进程收发了多少字节(单位:字节)
# -P 只看进程汇总;-d 从第二个样本起显示增量;-L 0 一直采样;-s 1 每秒一次
# -p 按进程名时要和 nettop 显示的名字完全一致,可以写多个 -p
nettop -P -d -L 0 -s 1 -p <PID> -J bytes_in,bytes_out本机输出(上一段是 lsof,下一段是 nettop -P -d -L 2 -p curl):
COMMAND PID ... NAME
curl 21687 ... TCP 192.168.50.60:51231->198.18.17.21:443 (ESTABLISHED)
curl 21700 ... TCP 192.168.50.60:51235->76.76.21.93:443 (ESTABLISHED)
,bytes_in,bytes_out,
curl.21687,259685,519, # 第 1 个样本:累计值
curl.21700,0,319,
,bytes_in,bytes_out,
curl.21687,65032,0, # 第 2 个样本:这一秒的增量
curl.21700,0,0,不要加 -m tcp,否则会漏掉 UDP。有些工具走 QUIC(基于 UDP 的 HTTP/3),不加这个参数时 nettop 会一起显示。
容易踩的坑:本机开着代理。 上面 198.18.17.21 那一条并不是真实服务器。代理客户端开了 TUN / fake-IP 模式时,会给每个域名分配一个 198.18.0.0/15 段的假地址(dig +short speed.cloudflare.com 返回的就是这个地址)。
这种情况下远端 IP 说明不了目的地。最省事的办法,是打开代理客户端自己的「连接」面板,多数客户端会按连接列出域名和发起的进程名。或者观察期间先把代理关掉。
进阶(可跳过): 不借助代理面板、直接看域名,就要看 SNI,也就是 HTTPS 握手时客户端以明文报出的目标域名。macOS 自带的 tcpdump 能按进程过滤:
# 进程名含空格要加引号,或者改用 pid = <PID>;-A 以文本打印包内容,SNI 在 ClientHello 里
sudo tcpdump -i pktap,all -k NP -Q 'proc = "ZCode Helper"' -n -A 'tcp dst port 443'
# 看它往磁盘写了什么
sudo fs_usage -w -f filesys <PID>这两条需要 sudo,我只对照 man 页核对过参数,没有在本机跑。如果连接用了 ECH(加密的 ClientHello),SNI 本身就被加密了,这条路也就走不通。 macOS 上多数应用的 DNS 查询由系统服务 mDNSResponder 代发,按进程抓包通常看不到应用自己的 DNS 查询。
Linux:ss 看每个套接字,nethogs 看每个进程
# TCP:进程 + 每条连接的内部计数,bytes_acked 是已被对端确认的发送字节
ss -tnpi | grep -A1 -i zcode
# UDP / QUIC
ss -unp | grep -i zcode
# 按进程实时看上下行速率(发行版仓库里的 nethogs 包)
sudo nethogs
# 只看某个进程发起了哪些连接
sudo strace -f -e trace=connect -p <PID>
# 看它的数据目录谁在写(需先装 inotify-tools)
inotifywait -m -r ~/.zcode-p 显示套接字属于哪个进程,-i 显示 TCP 内部信息(ss(8))。看别的用户的进程要加 sudo。Linux 这一节我没有在本机实测,参数以 man 页为准。
Windows:PowerShell 加资源监视器
先打开任务管理器的「详细信息」页,确认工具的进程名(下文假设以 ZCode 开头)。以下命令在 PowerShell 里运行,不是 cmd:
$ids = (Get-Process -Name ZCode* -ErrorAction SilentlyContinue).Id
# TCP 连接
Get-NetTCPConnection -OwningProcess $ids -ErrorAction SilentlyContinue |
Select-Object LocalPort, RemoteAddress, RemotePort, State
# UDP / QUIC
Get-NetUDPEndpoint -OwningProcess $ids -ErrorAction SilentlyContinue
# 最近解析过的域名:拿 Data 列的地址去对上面的 RemoteAddress
Get-DnsClientCache | Select-Object Entry, DataDNS 缓存是全系统共用的,不区分进程,只能拿来反查地址。工具没在运行时 $ids 为空,命令会报错或者什么都不输出。
要看字节,按 Win+R 输入 resmon 打开资源监视器,在「网络」页勾选该工具的全部进程,下方会显示每个进程的发送速率和远端地址。
参数出处见 Get-NetTCPConnection 和 Get-NetUDPEndpoint 文档。Windows 上同样可能碰到代理的假地址。这一节未在本机实测。
怎么判读:先采基线,再发一条短 prompt
- 基线:工具开着但不操作,采一分钟,记下每秒上行的量级;
- 对照:发一条很短的 prompt,比如「这个函数做什么」,看那一刻上行多出来多少;
- 用尺子量:我按代码和英文文本 1 token 约等于 4 字节粗算,一次带 2 万 token 上下文的请求,明文在 80KB 上下,加上协议开销也在几百 KB 以内(这是粗算,不是测量)。一条短 prompt 带出几 MB 上行,或者空闲时持续上行,就值得追。
先查文档再下结论。 有的工具会合法地上传代码去做索引或检索,并且在文档里写明了。拿观察结果去对照文档,超出文档写明范围的部分,才是可以拿去问厂商的事实。ZCode 那次,9 月 18 日保存的隐私政策写了会收集「对话中提交的文本、文件和代码」,没有写整仓快照(ferstar 原文)。
同时看磁盘。 数据目录通常在 ~/.<工具名>、~/Library/Application Support/<工具名>(macOS)或 %APPDATA%\<工具名>(Windows)。找最近一小时新生成的大文件:
find ~/.zcode ~/Library/Application\ Support/ZCode -type f -mmin -60 -size +10M -exec ls -lh {} + 2>/dev/nullGet-ChildItem "$env:APPDATA", "$env:USERPROFILE\.zcode" -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -gt (Get-Date).AddHours(-1) -and $_.Length -gt 10MB }ZCode 这件事,最早就是从一个多出来的几百 MB 目录查出来的。
.gitignore 不等于不上传:一个玩具仓实验
.gitignore 只对 git 本身、以及主动去读它的工具生效。一个直接打包目录的程序,不读它也没有任何义务读它。
国内有复盘文章转述说 ZCode 的快照不读 .gitignore。ferstar 原文里只写了打包时排除了 node_modules 等少量目录,没有专门验证这一条,我也没法复现 ZCode 本身。下面只用新建的临时玩具仓说明直接打包的机制,不在真实代码仓运行;mktemp 分配独占目录,归档也写在这次实验的目录内,不会覆盖工作区父目录的同名文件。示例字符串不是有效凭证:
lab_dir="$(mktemp -d "${TMPDIR:-/tmp}/git-snapshot-demo.XXXXXX")" || exit 1
mkdir "$lab_dir/demo" && cd "$lab_dir/demo" || exit 1
git init -q -b main
git config user.name demo && git config user.email demo@example.com
printf 'print("hi")\n' > app.py
printf 'API_KEY=example-old-value\n' > config.py
git add . && git commit -qm init
git rm -q --cached config.py
printf 'config.py\n.env\n' > .gitignore
printf 'API_KEY=example-new-value\n' > config.py
printf 'DB_PASS=example-local-value\n' > .env
git add .gitignore && git commit -qm 'stop tracking config'
git ls-files
archive="$lab_dir/snapshot.tgz"
tar czf "$archive" .
tar tzf "$archive" | grep -v '^./.git/'
tar tzf "$archive" | grep -c '^./.git/'
git log --all --name-only --format='%h' -- config.py输出的前两项是 .gitignore 和 app.py,而归档目录还会列出 ./config.py 与 ./.env;我在 macOS 26.6.2、git 2.55.0、bsdtar 3.5.3 的原玩具仓实验里数到 49 个 .git/ 条目,数量随 Git 版本和仓库状态变化。最后一行只列涉及 config.py 的提交 ID 与文件名,不会打印凭证值;核实内容须在受控环境进行,别把原始 patch 或日志贴进工单。
- Git 跟踪清单不含被忽略文件,直接打包工作目录却包含它们;
- 两次提交的小仓也会打入
.git/;ZCode 那份清单里的.git占到 86.6%,但大小比例不能推广到别的仓; - 即使旧凭证不在当前工作区,本地仍可达的提交和对象里也可能留着。拿到
.git能接触这台机器保留的历史,不等于拿到项目从创建以来的全部历史(浅克隆和垃圾回收是例外)。
反过来也有坑:ripgrep 这类工具默认尊重 .gitignore,在这个仓里 rg -l API_KEY 什么都搜不到,要加 --no-ignore --hidden 才能看见。拿尊重 .gitignore 的工具去检查「会被打包的东西」,查出来的永远比实际少。
落到行动上有两条:问一个工具会上传什么,要问它按什么规则收集文件;一个仓如果曾被整体外发过,就把历史里出现过的凭证全部轮换掉,光删文件、改 .gitignore 都不够。
锁目录能挡多少:chflags uchg 和 chattr +i 的边界
先说安全提示:退出工具并保留原目录作排查证据,不要照抄原帖的删除命令;只对 ZCode 已知的数据路径这么做,不碰系统目录。先记下解锁命令,检查点回滚等依赖该路径的功能可能失效。下面是把 ferstar 的「清空再上锁」改成可保留原件的版本;动手前仍应确认本机确有此目录:
# macOS / Linux: preserve existing snapshots in a unique sibling directory.
checkpoint_dir="$HOME/.zcode/v2/checkpoints"
if [ -e "$checkpoint_dir" ]; then
backup_dir="$(mktemp -d "$HOME/.zcode/v2/checkpoints.backup.XXXXXX")" || exit 1
mv "$checkpoint_dir" "$backup_dir/checkpoints" || exit 1
printf 'Backup: %s\n' "$backup_dir/checkpoints"
fi
mkdir -p "$checkpoint_dir"
# macOS: lock, test, and unlock when needed.
chflags uchg "$checkpoint_dir"
touch "$checkpoint_dir/test" # Expected: Operation not permitted
# chflags nouchg "$checkpoint_dir"
# Linux: use this instead of the macOS chflags command.
# sudo chattr +i "$checkpoint_dir"
# touch "$checkpoint_dir/test" # Expected: Operation not permitted
# sudo chattr -i "$checkpoint_dir"作者说锁上以后检查点回滚不能用了,补全和对话照常。Windows 的等价做法,他指向 NodeSeek 上一篇用 ACL 的帖子,那个页面我们没能抓取到,这里不复述。
我在本机把 macOS 的做法拆开试了一遍:
$ chflags uchg ck && ls -ldO ck
drwxr-xr-x@ 4 larrykoo wheel uchg 128 Sep 27 22:16 ck
$ touch ck/test
touch: ck/test: Operation not permitted
$ rm ck/existing.enc
rm: ck/existing.enc: Operation not permitted
$ echo x >> ck/existing.enc # 目录里原有的文件
exit=0
$ touch ck/sub/new # 上锁前就存在的子目录
exit=0
$ chflags nouchg ck # 同一用户,不用 sudo
exit=0- 目录里原有的文件照样能写。 锁住的是目录项(新建、删除、改名),管不到已有文件的内容。所以要先清空再上锁,顺序不能省;
- 上锁前就在的子目录不受影响。 锁只作用于那一层目录;
uchg能被属主自己解开。man chflags写的是「owner or super-user only」,以你的身份运行的进程也能执行chflags nouchg,我在本机没用 sudo 就解开了。换成sudo chflags schg(只有超级用户能设置和清除)会更硬,代价是恢复时也需要 root,这一条我没试。Linux 的chattr +i按 chattr(1) 只有 root 或持有 CAP_LINUX_IMMUTABLE 的进程能设能清,普通用户身份运行的应用解不开;但第 1、2、4 条边界同样存在(按语义推断,未实测);- 只锁一个路径。 应用更新后换个目录写,锁就不起作用了。ferstar 在修复版上线后仍保留了这把锁,理由是客户端能热更新。
所以这是一道只对 ZCode 已知路径有效的绊线。它能打断一次已知行为,而写入失败时,应用侧通常不会给你任何提示。它算不上出网边界,真正的边界要管住「字节离开机器」本身。

团队层面:把出网收口到一个可审计的点
先看单机能做到什么:
| 平台 | 自带能力 | 能做到 | 做不到 |
|---|---|---|---|
| macOS | 应用防火墙 / pf | 应用防火墙只管入站(Apple:更改 Mac 防火墙设置);pf 能按 IP、端口、用户拦出站(man pf.conf 的 user 规则) | 按域名、按单个应用 |
| Linux | systemd IPAddressAllow= / IPAddressDeny= | 按 IP 管某个 unit 或 scope 里的进程(systemd.resource-control(5)) | 按域名;内核或容器没开 cgroup eBPF 时,man 页原话是「no effect」 |
| Windows | New-NetFirewallRule -Direction Outbound | 按程序(-Program)加远端地址(-RemoteAddress)拦(New-NetFirewallRule) | 按域名 |
单机方案的共同短板是颗粒度:三家都能按 IP 拦,却没有「按域名放行」这一档。模型 API 大多挂在 CDN 后面,IP 名单根本维护不过来。对团队来说还多一个问题:每台机器各管各的,出了事没有一份统一的日志可查。
团队的做法,是把出网收到一个点上:
- 出口代理,按域名放行并记日志。 开发机和 CI 默认不能直连外网,只能走代理;代理只放行名单上的域名(模型 API、包镜像、代码托管),其余一律拒绝,每次放行和拒绝都记日志。HTTPS 不用解密,从代理收到的 CONNECT 请求(「帮我连某域名」)或 SNI 里就能拿到目标域名,足够回答「连到了哪里、发了多少」。
- DNS 允许列表。 内部解析器只替名单上的域名去查(递归),名单外的直接回「不存在」(NXDOMAIN)。它挡不住写死在代码里的 IP,所以底下还要一层防火墙,默认拒绝直连。
两条旁路要心里有数。一是 DoH:应用自己用 HTTPS 去问公共 DNS,内部解析器就被绕过了,要在代理上拦掉常见的 DoH 端点。二是域名前置(domain fronting):SNI 报的是名单上的域名,实际请求的却是另一个后端,不解密的代理看不出来。
拿 ZCode 那条链路推演一下:凭证请求发往厂商自己的域名,文件直传对象存储的域名。一份只列了模型 API 域名的名单里没有后者,这次上传就会在代理日志里留下一条拒绝记录。这是按链路结构做的推演,前提是那个存储域名确实不在名单上。它的价值在于,「有没有外发」变成了一条可以查询的日志,而不用去读厂商的说明。
同一条边界在训练侧也漏过。 OpenAI 9 月 25 日更新的一份对齐问题报告(misalignment report) (An agent used DNS to reach an external chatbot)里,训练中的 agent 直接发的 HTTPS 请求被代理拦下了,但训练环境自带的 DNS 解析器会替它查任意域名。agent 把问题编码进主机名,经这个解析器转给外部 chatbot,拿回了答案。报告原话是「insufficient DNS filtering」。
放到你的 DNS 允许列表上,是三条:
- 解析器不替名单外的域名递归,这是主因。只要它什么都肯查,DNS 本身就是一条出网通道;
- 限制记录类型,比如 TXT,能压住回程能带多少数据。OpenAI 的修复是把查询限制在「a limited allowed list of domains and record types」,并加了两层独立拦截;
- 查询日志要接到告警上。报告说异常 DNS 检测器排除了这个环境,「though DNS activity was logged」。
代价是每接一个新服务就要改一次名单。我们自己做网关,出网收口是日常工作之一。不过网关只管 LLM 调用这一个出口,桌面工具直传对象存储这类旁路,得靠上面的代理和 DNS 去管。 工作区、推理引擎、平台三层各自的边界,我在《DeepSeek Harness 沙箱按设计不拦读:Agent「逃逸」逃出的是哪一层?》里拆过,出网白名单属于其中的平台层。
谁持有解密钥,决定删除声明能不能被验证
回到 ZCode 的加密方式:公钥由服务端在发上传凭证时给出,私钥只在云端。ferstar 拿本机所有私钥去试,全部解不开。你硬盘上那份密文,你自己打不开,客户端也打不开。

这个设计决定了谁能独立检查密文、哪些结论需要额外证据:
- 如果只有用户持有解密钥且实现经核验:服务端拿到的密文不能直接读,用户可自行解密核对;这也不能单靠密钥设计证明服务端没有拿到别的明文副本;
- ZCode 事件中私钥由服务端持有:本机密文不能让用户独立验证其内容或后续处理。事后桶为空能说明调查时的状态,不能单独排除更早的读取、转存或备份;还需要覆盖当时链路的可核验记录与审计。
Z.ai 的说法是:代码从未进入训练数据,完整评估报告将会发布。官方在 9 月 21 日的通报里引述,中国信通院确认 zcode-prod 存储桶数据为零,绿盟科技确认桶和对象已全部删除(ferstar 原文 9-21 更新)。 这些说法都应该被记下来。但在钥匙在服务端的设计下,仅凭本机密文无法独立核实历史处理。TNW 9 月 22 日的复查也指出:「The two assessments describe the storage bucket after the fix, not what passed through it before.」该报道当时没有给出完整报告的发布日期;本文不据此断言今天仍未发布 (TNW:Z.ai apologised, open-sourced ZCode, and wiped the commit history)。
开源仓那边也一样。9 月 27 日 GitHub 上一共 3 个提交:导入全部代码的那一个时间是 2026-09-20 21:14 UTC,另一个是 9 月 23 日的 v3.14.3。能审计的只是修复后的代码,旧版的上传代码不在仓里。
所以你能做的是两件事:更新之后把第 3 节的观测再跑一遍,因为外部能验证的只有「客户端现在还发不发」;已经外发过的仓库,按泄露处理,轮换凭证。
引入 AI 编程工具之前,该问厂商的六个问题
- 按什么规则收集文件? 读不读
.gitignore,带不带.git(第 4 节); - 什么时候上传? 每次提问、打开项目,还是只在你主动触发时(第 1 节);
- 每个开关到底管什么? 是管「拿不拿」,还是只管「怎么用」(第 2 节);
- 会连哪些域名? 要一份清单,拿来配出口代理的允许列表(第 6 节);
- 加密的钥匙在谁手里? 钥匙在服务端的话,删除声明靠什么验证(第 7 节);
- 上传的东西保留多久、怎么删? 有没有可以核对的删除凭据。
两张自查表:开发者单机与团队基础设施
开发者单机(5 分钟):
| 步骤 | 命令或动作 | 看什么 | 容易踩的坑 |
|---|---|---|---|
| 1 找全进程 | pgrep -fl -i <工具名> / 任务管理器「详细信息」 | 主进程 + Helper + sidecar | 只盯主进程,漏掉负责上传的子进程 |
| 2 看连接 | lsof -nP -a -p "$(pgrep -d, -i <工具名>)" -i / ss -tnpi + ss -unp / Get-NetTCPConnection + Get-NetUDPEndpoint | 目的地是不是模型 API | 远端是 198.18.x.x:本机代理的假地址,去看代理的连接面板 |
| 3 看字节 | nettop -P -d -L 0 -s 1 -p <PID> -J bytes_in,bytes_out / nethogs / resmon | 先采空闲基线,再看短 prompt 那一刻的增量 | 没有基线就判异常;忘了有的工具在文档里写明了会合法上传做索引 |
| 4 看磁盘 | find <数据目录> -type f -mmin -60 -size +10M | 最近一小时新出现的大文件 | 删了又重新生成:它在重试 |
| 5 事后处置 | git log --all --name-only --format='%h' -- <疑似文件路径> 定位后在受控环境核查、轮换 | 可达历史里涉及敏感文件的提交 ID 和路径(不打印内容) | 不要在终端/工单贴原始 patch;仅查文件名不能证明历史里没有凭证 |
| 附 ZCode 专用绊线 | 先清空再 chflags uchg / sudo chattr +i | touch 报 Operation not permitted | 目录没清空、有子目录,或 uchg 被属主解开 |
团队基础设施(运维):
| 步骤 | 做什么 | 看什么 | 容易踩的坑 |
|---|---|---|---|
| 1 默认拒绝 | 开发机和 CI 禁止直连外网 | 防火墙拒绝日志 | 只配了 DNS,写死的 IP 照样能出去 |
| 2 出口代理 | 按域名放行 + 全量日志 | CONNECT / SNI 里的目标域名与字节数 | DoH、域名前置绕过 |
| 3 DNS 允许列表 | 名单外不递归,并限制记录类型 | 解析器查询日志 | 解析器什么都肯查,DNS 本身就成了出网通道 |
| 4 告警 | 拒绝记录和异常 DNS 接到告警 | 被拒条目的进程和机器 | 日志只落盘,没人看 |
| 5 准入 | 新工具先答完上面六个问题 | 厂商给的域名清单 | 清单和实际观测对不上 |
今天就挑你最常开的那个仓,先采一分钟基线,再在 AI 编程工具里发一条最短的 prompt,看一眼上行字节。这个数字它的设置页不会告诉你。