ZCode 工作区快照静默上传:检测方法与三层防护实践
大家好,我是邵奈一,一个爱折腾的实战派技术博主。
文章对你有帮助是我的荣幸,欢迎评论区交流。
标签:#ZCode #隐私安全 #AI编程工具 #数据上传 #macOS防护
基于 ZCode 桌面版客户端的实测与代码逆向,梳理其工作区快照静默上传的行为特征、本地检测手段与防护方案。
核心结论
2026 年 9 月 18 日,技术博客《扒一扒 ZCode 静默上传全量 Git 历史的骚操作》(作者 ferstar,blog.ferstar.org)指出:ZCode(智谱 Z.ai 的 AI 编程工具)在登录状态下会把整个工作区(含完整 .git 历史)静默打包上传至云端,且 UI 内的隐私开关无法阻止该行为。本文通过实测验证,结论如下:
- 该行为属实。实测环境中 5 个工作区被快照,其中 4 个已确认上传成功,内容包含完整 Git 历史、全局配置、MCP 服务器列表与对话记录。
- 加密所用的 RSA 公钥由服务端下发,数据仅厂商可解密。
- 即便关闭"优化智能体体验"等隐私开关,上传行为依旧发生。
- 采用"文件锁 + 常驻守卫 + 网络层封锁"三层防御,可阻断换账号、更新版本后的上传行为,本文给出全部可复现命令。
- 已上传数据无法本地撤回。
时效声明:以下结论基于 2026-09-19 的 ZCode 桌面版客户端(9 月 16 日更新版本)实测与客户端代码逆向,未来版本可能变化。向原始爆料作者 ferstar 致谢。
第一节 背景与风险
AI 编程工具的工作区通常同时承载多个项目:AI 方向的主项目(实测本地仓库约 178MB)、企业项目、个人小工具(如视频字幕插件、文档库)等。评估该上传行为的风险,需明确两点:
其一,Git 历史等同于项目档案馆。 .git 目录保存每一次 commit 记录,可能包含硬编码口令、内网地址、客户名称、未清理的调试代码、离职同事注释等敏感信息。上传"当前代码"与上传"完整历史"在数据暴露量级上完全不同。
其二,隐私开关不构成有效防护。 ZCode 设置项中 优化智能体体验、repoSnapshotIndexingEnabled、自动更新均可关闭,但实测表明这些开关与该快照上传行为无关。
本节给出一套本地检测流程,约十分钟可判断本机是否受影响。
第二节 检测方法
打开终端,依次执行以下命令。
步骤 1:检查快照目录及占用空间
bash
du -sh ~/.zcode/v2/checkpoints
实测结果:153MB。
步骤 2:列出被快照的工作区
bash
grep -o '"workspacePath": "[^"]*"' ~/.zcode/v2/checkpoints/*/state.json
实测结果:5 个工作区(含主项目与企业项目)。
步骤 3:确认是否包含 Git 历史
bash
python3 - <<'EOF'
import json, glob
for p in glob.glob('~/.zcode/v2/checkpoints/*/manifests/*.json'):
d = json.load(open(p))
n = sum(1 for f in d['files'] if f['path'].startswith('.git/'))
print(d['workspaceKey'], '| 总文件', len(d['files']), '| .git 下', n)
EOF
实测主项目:6384 个文件,其中 3462 个位于 .git/ 下,完整历史无遗漏。
步骤 4:查询服务端接收回执
bash
grep -l lastAcceptedManifestHash ~/.zcode/v2/checkpoints/*/state.json
代码逆向确认:该字段仅在服务端确认收到快照后写入。实测 4 个项目携带此回执,接收日期分别为 9 月 10 日、11 日、12 日、16 日,表明数据已落于厂商服务器。
例外情况:主项目 151MB 加密基线包上传失败,滞留于本地 pending 目录,可作为未上传成功的物证。
步骤 5(可选):查看加密信封
bash
cat ~/.zcode/v2/checkpoints/*/pending/*.envelope.json
输出组合为 aes-256-ctr + rsa-oaep-sha256。其安全边界见第三节分析。
第三节 上传机制技术分析
对客户端 app.asar 反编译并通读上传链路后,确认以下三个事实:
1. 解密密钥由服务端掌握。 RSA 公钥非客户端内置,而是每次上传时由服务端接口 /api/v1/snapshot/upload-credential 临时下发。数据用服务端公钥加密后传回,形成仅厂商可解的信封。厂商称为"传输安全",其实质是用户无法获知代码去向。
2. 隐私开关不参与上传决策。 任务结束时触发快照的标志在代码中硬编码为 true;从"打包 → 加密 → 上传"全链路,无任何读取隐私开关的逻辑。关闭开关两日后快照仍正常生成。
3. 触发条件单一:已登录且一次任务结束。 无弹窗、无首次授权、无提示。上传失败静默重试(主项目加密包即留有完整滞留记录)。
综合评估:此类功能可能服务于跨设备体验或代码索引等产品能力。但"默认开启 + 无开关 + 含全量 Git 历史 + 私钥在服务端"四项叠加,已构成越界的数据收集。
第四节 防护方案:三层防御体系
基本原理:上传前客户端须先将打包文件写入本地磁盘,阻断写盘即可阻断上传。
第一层:文件系统不可变锁
bash
# 先将滞留的加密包隔离(如有)
mkdir -p ~/snapshot-quarantine
find ~/.zcode/v2/checkpoints -name '*.tar.gz.enc' -o -name '*.envelope.json' \
| xargs -I{} mv {} ~/snapshot-quarantine/
# 整树加 macOS 用户级不可变锁
chflags -R uchange ~/.zcode/v2/checkpoints
# 顺手锁住两个同族目录(代码中的备选路径)
mkdir -p ~/.zcode/v2/repo-snapshots ~/.zcode/v2/repo-wiki
chflags uchange ~/.zcode/v2/repo-snapshots ~/.zcode/v2/repo-wiki
验证:
bash
touch ~/.zcode/v2/checkpoints/test
# 应报错:Operation not permitted
ls -lO ~/.zcode/v2/checkpoints # 应看到 uchg 标志
效果:上传流程第一步(写清单与加密包)当场失败,无字节发出。该锁与账号无关,退出登录、换账号、加账号均被拦截。
第二层:常驻守卫(自动自愈)
文件锁的弱点是:客户端大版本更新改路径或重装清目录后,锁失效。为此部署 launchd 守卫:每分钟巡检,发现快照目录缺失则重建并锁死;发现任何位置出现快照产物(*.tar.gz.enc / *.envelope.json)则搬至隔离区。
实测该守卫非常驻进程(launchd 拉起、跑完即退,内存占用为零),单次约 0.3 秒 CPU,合计约 1% 单核。
守卫脚本 ~/Library/Application Support/zcode-snapshot-guard/guard.sh:
bash
#!/bin/bash
# zcode-snapshot-guard --- 阻断 ZCode 工作区快照静默上传的常驻守卫
set -u
GUARD_DIR="$HOME/Library/Application Support/zcode-snapshot-guard"
QUARANTINE="$GUARD_DIR/quarantine"
LOG="$HOME/Library/Logs/zcode-snapshot-guard.log"
STATE="$GUARD_DIR/lastrun"
LOCKDIR="/tmp/zcode-snapshot-guard.lock"
DEFAULT_DATA="$HOME/.zcode"
SNAPSHOT_SUBDIRS="v2/checkpoints v2/repo-snapshots v2/repo-wiki"
mkdir -p "$QUARANTINE"
# 节流:25 秒内不重复执行
now=$(date +%s)
if [ -f "$STATE" ]; then
last=$(cat "$STATE" 2>/dev/null || echo 0)
[ $((now - last)) -lt 25 ] && exit 0
fi
echo "$now" > "$STATE"
if ! mkdir "$LOCKDIR" 2>/dev/null; then exit 0; fi
trap 'rmdir "$LOCKDIR" 2>/dev/null' EXIT
if [ -f "$LOG" ] && [ "$(stat -f %z "$LOG" 2>/dev/null || echo 0)" -gt 262144 ]; then
mv "$LOG" "$LOG.1" 2>/dev/null
fi
log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG"; }
# 兼容设置里的 dataBaseDir 覆盖
db_override=$(grep -o '"dataBaseDir"[[:space:]]*:[[:space:]]*"[^"]*"' \
"$DEFAULT_DATA/v2/setting.json" 2>/dev/null | sed 's/.*:[[:space:]]*"//;s/"$//' | xargs)
DATA_LIST="$DEFAULT_DATA"
[ -n "${db_override:-}" ] && [ "$db_override" != "$DEFAULT_DATA" ] && [ -d "$db_override" ] && \
DATA_LIST="$DATA_LIST
$db_override"
log "----- run start -----"
printf '%s\n' "$DATA_LIST" | while IFS= read -r DATA; do
[ -z "$DATA" ] && continue
# 1) 锁死/预创建已知快照目录
for sub in $SNAPSHOT_SUBDIRS; do
d="$DATA/$sub"
if [ ! -d "$d" ]; then
if mkdir -p "$d" 2>/dev/null; then
chflags uchange "$d" 2>/dev/null && log "pre-created+locked $d"
fi
else
chflags -R uchange "$d" 2>/dev/null
fi
done
# 2) 全目录扫描快照产物并隔离(锁定目录内的搬不动=已受保护)
while IFS= read -r -d '' f; do
skip=0
for sub in $SNAPSHOT_SUBDIRS; do
case "$f" in "$DATA/$sub"/*) skip=1; break;; esac
done
[ "$skip" -eq 1 ] && continue
chflags nouchange "$f" 2>/dev/null
dest="$QUARANTINE/$(basename "$f").$(date +%s).$$"
if mv "$f" "$dest" 2>/dev/null; then
log "QUARANTINED $f"
else
log "WARN unmovable artifact (unknown locked path): $f"
fi
done < <(find "$DATA" -type f \( -name '*.tar.gz.enc' -o -name '*.envelope.json' \) -print0 2>/dev/null)
done
log "----- run end -----"
安装(把 你的用户名 换成自己的,脚本先 chmod +x):
bash
cat > ~/Library/LaunchAgents/com.user.zcode-snapshot-guard.plist <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.user.zcode-snapshot-guard</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>你的用户主目录/Library/Application Support/zcode-snapshot-guard/guard.sh</string>
</array>
<key>RunAtLoad</key><true/>
<key>StartInterval</key><integer>60</integer>
<key>WatchPaths</key>
<array>
<string>你的用户主目录/.zcode</string>
<string>你的用户主目录/.zcode/v2</string>
</array>
<key>StandardErrorPath</key>
<string>你的用户主目录/Library/Logs/zcode-snapshot-guard.err.log</string>
</dict>
</plist>
PLIST
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.zcode-snapshot-guard.plist
验证测试:在陌生路径伪造两个快照产物,数秒内被守卫隔离,日志留下 QUARANTINED 记录。守卫首次运行亦将此前手动隔离的 151MB 加密包再次转移至其自身隔离区。
第三层:网络层封锁(终极保险)
文件层防御存在理论竞态窗口:若未来版本改用新路径且小包在两次巡检间隙传完,文件锁可能漏挡。网络层封锁可彻底封死,二选一:
方案 A(优先,单命令)------在 DNS 层拉黑已确认的 OSS 上传端点:
bash
sudo sh -c 'echo "0.0.0.0 zcode-codebase-snapshot.oss-cn-beijing.aliyuncs.com" >> /etc/hosts' \
&& sudo dscacheutil -flushcache
该 bucket 域名经 DNS 验证真实存在。
方案 B(终极方案) ------安装 LuLu(免费开源)或 Little Snitch,为 ZCode 建立规则:只允许连接 zcode.z.ai,拒绝其余一切地址。无论未来版本上传至哪个存储端点均无法出网,是唯一无视版本变化的方案。
第五节 常见问题
Q1:退出登录是否安全?
是。上传流程第一步校验登录令牌,未登录直接返回,连打包都不发生。但退出后内置模型基本不可用,实操意义有限。
Q2:换新账号呢?
换账号重新获得有效令牌,捕获流程会恢复尝试;但第一层文件锁与账号无关,写盘失败,仍无一字节发出。前提:已实施第一层。
Q3:守卫是否长期占用内存?
不会。非常驻进程,内存占用为零;CPU 实测约 1% 单核(每分钟至多 2 次、每次 0.3 秒)。介意可将节流与间隔放宽,或装 LuLu 后直接移除。
Q4:已上传数据如何处理?
本地操作无法删除服务端数据,只能联系官方。重点应复盘 Git 历史中的口令、密钥、内网地址、客户信息,该换则换、该改则改。
Q5:如何恢复官方功能?
bash
chflags -R nouchange ~/.zcode/v2/checkpoints
launchctl bootout gui/$(id -u)/com.user.zcode-snapshot-guard
第六节 小结
本事件的核心结论是:关闭可见开关不等于阻断数据上传。ZCode 的快照上传行为与 UI 隐私开关无关联。
对厂商的三点建议:默认关闭而非默认开启;首次触发明确告知(至少一个角标);若需上传则排除 .git 目录或采用本地持钥的端到端加密。做到这三条,该能力可成为合理功能。
对使用者:AI 编程工具读取代码是其品类成立的前提,但"对话时读取"与"将整库历史静默搬入对象存储"性质不同。工具越强,越应确认其数据去向。
本文操作环境为 macOS(Apple Silicon,macOS 15.x)。Windows 用户思路相同:NTFS 无 chflags,可用 ACL 拒绝写入 + 计划任务巡检实现同款防线。
转载声明:本文为博主原创内容,转载请注明出处,并保留原始爆料来源(ferstar,《扒一扒 ZCode 静默上传全量 Git 历史的骚操作》)。
邵奈一 三百篇原创沉淀,十万+读者同行。感谢你的阅读,咱们下篇接着聊。