总结:这简直就是z/ 正版用户有福了
省流版:
上传条件:需要oatuh登录z.ai和bigmodel的用户才会上传,用apikey的不会被上传。
上传内容:
你每一次回车发送的promot 附件(图片/视频/音频等),你的模型供应商url,模型
你的.zcode全局配置,Agent.md,MCP,Hooks,MemorySkills/Commands/Subagents/Plugins
你的项目文件:
他会过滤 .env/.npmrc/id_rsa/*.pem/*.key/*.p12/*.pfx,文件包括token/secret内容的文件,但是你的文件比如说叫env.local也一样传上去,虽然有过滤条件,但是只要提交过进了.git/object也一样会有记录一样会被传走。
总结:你的整个项目的源码,我管你这的哪的,只要提交过,哪怕你原文件删了,我从.git/object一样能还原你的源码,拿来吧你。 ^-^
这是一个非常恐怖的上传事件,哪怕你之前的代码没用zcode是手搓的,只要你点打开文件夹开始对话回车按下去,不管有没有.git,是不是zcode写的,直接整个文件夹扫描排除上传,请注意是整个目录<1mb的文件 .git无视大小多大都能传,promot,附件图片 20MB/视频 200MB/音频 20MB/其他 100MB、总 1GB,不管你什么语言和项目和附件promot,整个加密文件包2GB上限, 这把你整个人加项目蒸馏了没区别。
如果你的项目曾经提交过敏感key,哪怕你原文件已经删除,请立即更换,排除规则并不完善,zcode还可以从.git/object拿到。
test-workspace/
│
│ ── 根目录文件 ──
├── README.md ✅ 传了(明文)
├── app.js ✅ 传了(明文)
├── notes.txt ✅ 传了(明文)
├── cred.txt ✅ 传了(明文,改名密钥穿透规则)
├── .gitignore ★新 ✅ 传了(明文,内容就是 "src/local_notes.txt")
├── .env ❌ 明文没传 → 但 blob 在 .git/objects/55/e9a9c4 里传走
├── api_token.txt ❌ 明文没传 → blob d3/6304 传走
├── bigfile.bin ❌ 明文没传 → blob 69/a7cc 完整 1MB 传走
│
│ ── 二级目录(本轮重点)──
├── src/
│ ├── core/
│ │ └── engine.js ★新 ✅ 传了(files/src/core/engine.js 明文,路径完整保留)
│ ├── utils/
│ │ └── crypto.js ★新 ✅ 传了 ★★ 最狠的一个:
│ │ files/src/utils/crypto.js 明文进包,
│ │ 内容: const API_KEY = 'sk-live-abc123-SECOND-LEVEL-SECRET'
│ │ 同一内容在 .git/objects/c7/3eed 里再存一份
│ │ (文件名"无害"→敏感名规则失效;源码内嵌密钥→任何规则都管不着)
│ └── local_notes.txt ★新 ❌ 没传(.gitignore 忽略 + 未被 git 跟踪 →
│ git ls-files 不列出 → 两层都没进)← 唯一真正安全的一个
├── config/
│ └── database.json ★新 ✅ 传了(明文:「db_host": "192.168.1.100", "db_password": "p@ssw0rd-二级目录」)
├── lib/
│ └── settings.js ★新 ✅ 传了(明文)
├── dist/bundle.js ❌ 明文没传 → blob 4e/f088 传走
├── node_modules/dep.js ❌ 明文没传 → blob d8/d73a 传走
│
│ ── .git 整目录(免检通道)──
└── .git/ ✅ 39+ 个文件全部原样
├── config ✅(含假 GitHub token 的 remote URL)
├── logs/HEAD、index、refs/... ✅
└── objects/ ✅ 本轮新增 8 个对象:
├── c7/3eed → src/utils/crypto.js 完整内容(含假 API key)★★
├── 2e/c0cd → config/database.json(含假数据库密码)★
├── 87/19fc → src/core/engine.js
├── f2/0628 → lib/settings.js
├── bd/95c7 → .gitignore 自身
├── a1/890d、5e/14ef 等 → 新 commit + tree(目录树,标记文件在 src/、config/ 下的位置)
└── (上一轮的全部 blob 原样随包再传一遍)
完整版复现过程:
# ZCode 桌面版会把你的整个工作区打包上传——逆向 + 动态复现全记录
## TL;DR
ZCode 桌面版(Electron)里有一条**平时完全无感的数据上传链路**:只要你用 z.ai / BigModel 账号 OAuth 登录,**每发一条消息,客户端就会把你的工作区文件、这条 prompt 的原文、prompt 附件、以及你的全局配置(AGENTS.md、hooks、MCP、memory 等)加密打包,300 毫秒内上传到服务端**。加密用的是服务端下发的 RSA 公钥——OSS 存的是密文,但 zcode.z.ai 服务端可以解密还原全部内容。
我把 app.asar 解包做了完整逆向,又在沙箱里拉起副本 + mock 服务端做了动态复现,**从 UI 发 prompt 到 OSS 收到加密包、再到用私钥解密还原出我的文件内容,全程跑通**。以下是完整证据链。
**先说清楚边界**:
- 用**自定义 API Key 直连**(不登录账号)时,这条链路完全不执行——我本机就是这个状态,日志和目录审计零上传痕迹。
- 裸目录下过滤规则真实生效(`.env`、`id_rsa`、`*.pem`、文件名含 `token/secret`、>1MB、二进制、`node_modules`、`dist` 都不传)。**但过滤只看文件名**——密钥改个名照样上传。
- **git 仓库是重灾区**:`.git` 整目录走免检通道(无大小/敏感名/二进制过滤),等于把"所有提交过的内容、含已删除的历史版本"整体打包。工作区层被排除的 `.env` 等,内容全部从 `.git/objects` 绕出去。
- 这不一定是"恶意行为",更像官方的云同步/上下文索引功能(代码里叫 repo-snapshot,服务端配合做增量)。问题是:**默认开启、无感知、无明示告知**,而且传的东西比大多数人想象的多。
**最重要的一句话**:只要提交过 git,就全部上传——**包括你已经删除的**。
---
## 一、链路是怎么跑的
```
你发送 prompt(回车)
│
├─ ① 前置条件:本地工作区 + OAuth 登录态(取不到 token 直接 return)
│
├─ ② GET https://zcode.z.ai/api/v1/snapshot/upload-credential
│ (Bearer token,返回 snapshot_id + RSA 公钥 + OSS 签名 + max_size)
│
├─ ③ 扫描工作区
│ git 仓库 → git ls-files --cached --others --exclude-standard
│ 非 git 目录 → 递归遍历(等于全量拷贝)
│ + 显式追加 .git/ 里的元数据文件(config、refs、reflog……)
│
├─ ④ 组装:工作区文件 + prompt.json(含 prompt 原文/模型/baseURL)
│ + prompt 附件原文件 + 9 类全局配置
│ (baseline 全量 / 增量 delta 两种模式)
│
├─ ⑤ 打明文 tar.gz → AES-256-CTR 加密(随机密钥)
│ 密钥用服务端 RSA 公钥 OAEP-SHA256 包裹 → .tar.gz.enc + envelope.json
│ 明文包立即删除
│
└─ ⑥ POST 到服务端下发的 OSS 地址(multipart + 签名 policy)
+ OSS callback 通知服务端;失败重试 3 次,状态持久化,重启续传
```
**触发频率**:代码里有 `captureRepoSnapshotSidecar`(发 prompt 时)和 repo-wiki 生成两个调用点。动态复证实测:**每条 prompt 稳定触发 2 次上传**(两个路径各一个包)。
**触发条件汇总**:
| 条件 | 不满足时 |
|---|---|
| 本地工作区(非远程挂载) | 跳过 |
| OAuth 登录态有效 | 直接 return(API Key 用户永远满足不了这条) |
| upload-credential 请求成功 | 等下次 prompt |
| 磁盘配额(上限 min(服务端值,2GB),本地驻留 1/3) | 丢弃旧 pending 后再试 |
| 加密后 ≤ max_size | 删产物只记大小 |
> 注意:设置项 `repoSnapshotIndexingEnabled` 我在代码里**没有找到**它会拦截上传——真正有效的闸门只有登录态这一条。
---
## 二、到底传了什么(第一轮:裸目录实测解密)
沙箱复现时,我用文件监控器在上传瞬间抓到了 pending 目录里的 envelope(含被 RSA 包裹的 AES 密钥)和密文,完成端到端解密。tar.gz 内部结构:
```
<snapshot_id>/
├── meta/
│ ├── prompt.json ← prompt 原文 + 会话 ID + 模型名 + 你配的 API baseURL
│ ├── manifest.json ← 文件清单(路径+大小)
│ └── delta.json ← 增量模式时的增删改清单
├── files/ ← 工作区文件本体(原样)
├── extra-files/
│ ├── references/ ← 你拖进对话框的附件(原文件二进制,不做敏感过滤)
│ └── global-configs/ ← 你的全局配置(见下)
└── extra-meta/
```
实测的 `prompt.json`(这就是每条消息都会带的东西):
```json
{
"schema": "repo_snapshot_prompt/v2",
"sessionId": "a35e3e92-...",
"captureStage": "prompt",
"provider": "others",
"model": "mock-model",
"url": "http://127.0.0.1:18081",
"content": "我修改了README,请重新看一下"
}
```
全局配置逐类上传(`~/.zcode` 下):
| 内容 | 说明 |
|---|---|
| `AGENTS.md` | 全文(上限 20MB) |
| 行为设置 | 键值快照 |
| MCP 服务器 | **完整定义**,含启动命令、参数、env(env 值键名叫 token 的会被 `<redacted>`,叫别的不会) |
| Hooks | **完整定义**,含可执行命令行 |
| Memory | **全文内容**(上限 20MB) |
| Skills/Commands/Subagents/Plugins | 元数据级(名字/描述/开关) |
prompt 附件规则:有本地路径的(包括图片/视频/音频)**原文件二进制**进包;纯文本附件按内容进包;最多 16 个,图片 20MB/视频 200MB/其他 100MB,总 1GB。
### 排除规则实测(第一轮:裸目录,8 个文件只传了 3 个)
| 文件 | 结果 | 生效规则 |
|---|---|---|
| README.md / app.js / notes.txt | ✅ 传了 | — |
| `.env`(内容 SECRET_KEY=...) | ❌ | 文件名精确匹配 `.env` |
| `api_token.txt` | ❌ | 文件名含 `token` |
| `bigfile.bin`(1MB+10B) | ❌ | >1MB(就差 10 字节卡线,阈值精确) |
| `dist/bundle.js` | ❌ | 顶层构建目录 |
| `node_modules/dep.js` | ❌ | 依赖目录 |
**但请记住:以上全是文件名匹配。** `looksLikeSecretPath` 只认 `.env/.npmrc/id_rsa/*.pem/*.key/*.p12/*.pfx` 和文件名里的 `token/secret` 字样。你的密钥叫 `data.txt`、AK/SK 写在普通代码里、`.env` 改名成 `env.local`——全部照传。
---
## 三、第二轮:git 仓库才是重灾区(.git 是免检旁路)
把同一个工作区 `git init + commit` 后再测,结果完全变了。代码里 `.git` 目录被 `appendRootGitMetadataPaths` **主动追加**进采集列表,且过滤函数对路径含 `.git` 段的文件直接放行——**大小检查、敏感名检查、二进制检查全部跳过**:
```js
// shouldIncludeRepoSnapshotPath
路径含 .git 段 → { include: true } // 直接放行,后面所有检查不执行
```
解密后的包(19KB,对比第一轮 1.4KB):**`.git` 整目录 35+ 个文件原样进包**,加上 git 可见的 4 个文件。最关键的发现:
| 上传物 | 实测内容 |
|---|---|
| `.git/config` | **remote URL 里的 token 明文**(我埋的假 token `https://user:[email protected]/private/repo.git` 原样在内) |
| `.git/logs/HEAD` | 完整 reflog 操作历史 |
| `.git/objects/**` | **所有提交过的文件内容的 zlib blob** |
| `.git/objects/pack/big-pack-test.pack` | **2MB 的 pack 原样上传**——>1MB 限制对 `.git` 不生效 |
### 被"排除"的文件全部从 .git/objects 绕出去了
我把第一轮被规则拦住的每个文件的 blob 逐个解压验证:
| 被第一层排除的文件 | 藏在哪个 blob | 解压后 |
|---|---|---|
| `.env` | `objects/55/e9a9c4...` | `SECRET_KEY=should-be-excluded-by-name` |
| `api_token.txt` | `objects/d3/6304...` | `inline-token-file-should-be-excluded` |
| `bigfile.bin` | `objects/69/a7cc...` | 完整 1,048,586 字节 |
| `dist/bundle.js` | `objects/4e/f088...` | `build output should be excluded` |
| `node_modules/dep.js` | `objects/d8/d73a...` | `dependency should be excluded` |
**五个被规则明确排除的文件,内容全部随包传出。** 服务端解包后跑一遍 `git cat-file` 就能还原。
### 删了的文件也会传
git 删除文件从不删 objects 里的 blob(历史 commit 依然引用它,连 `git gc` 都不会清)。所以:**你提交过一次密钥、后来删了、以为没事了——每次发消息,那个密钥都随 `.git/objects` 完整上传一遍。** 这就是安全圈对"密钥进过 git 历史"的标准答案是"立即吊销轮换"的原因:在 git 里删除从来不是删除。
### 一句话总结两轮差异
> **裸目录:排除规则全部真实生效(真没传)。**
> **git 仓库:排除规则只锁了 `files/` 大门,旁边 `.git` 车库门敞开——提交过 = 全部上传,含已删除的。**
---
## 四、第三轮:二级目录与内嵌密钥(最贴近真实项目)
加了 `src/`、`config/`、`lib/` 二级目录再测(每个里面埋了东西):
| 二级目录文件 | 结果 |
|---|---|
| `src/core/engine.js` | ✅ 明文上传,路径完整保留(`files/src/core/engine.js`) |
| `src/utils/crypto.js`(源码里写死 `API_KEY = 'sk-live-...'`) | ✅ **明文上传**——文件名无害 + 内容不检查,双重盲区 |
| `config/database.json`(`db_password` 明文) | ✅ 明文上传 |
| `lib/settings.js` | ✅ 明文上传 |
| `src/local_notes.txt`(被 `.gitignore` 忽略且未跟踪) | ❌ **唯一没传的**——`git ls-files` 不列出,两层都躲过 |
结论:
- **目录深度不是屏障**。排除清单只认 `node_modules/dist/build/out/.next/coverage` 这几个名字(且 `dist/build/out` 只匹配**顶层**,`src/dist/xxx` 反而会传)。`src/lib/config/internal` 畅通无阻。
- **`.gitignore` 是唯一可靠的防线**(未跟踪 + 被忽略 = 真不传),比 ZCode 自己的敏感名过滤靠谱。
- **写死在源码里的密钥没有任何规则能拦**。
### 大小限制的不对称(实测确认)
| 位置 | 单文件限制 |
|---|---|
| 工作区正常路径 | **>1MB 排除** |
| `.git/` 下任何文件 | **无限制**(免检) |
| 整个快照包 | 加密后 ≤ min(服务端值, **2GB**),超了不上传 |
实测:工作区 1MB+10B 的文件被排除,同内容的 git blob(1,048,586B)原样上传,2MB 的 pack 文件原样上传。2GB 是压缩加密后的大小,git blob/pack 本身已压缩,一个大仓库的 `.git` 很容易整体塞进 2GB 限额内——**对 git 项目来说,大小护栏基本形同虚设**。超限时的行为:删产物、记录大小、下次 prompt 重试(表现为发消息时无提示的后台扫描卡顿)。
### 加密与密钥
- AES-256-CTR 加密内容,密钥随机生成,用 `upload-credential` 响应里服务端下发的 RSA 公钥(OAEP-SHA256)包裹。envelope 里有 `plaintextSha256`、AAD(workspace 哈希、manifest 哈希等)。
- 我做了交叉验证:用 A 快照的密钥解 B 快照的密文,得到乱码——**每个快照独立密钥,只有持私钥的服务端能解**。OSS 拿到的是纯密文,但服务端随时可还原。
- 明文 tar.gz 生存期 <50ms(加密完立即删),但 envelope + 密文会在 `~/.zcode/v2/checkpoints/<wsHash>/pending/` 短暂驻留,失败时保留 24 小时并跨重启续传。
---
## 五、动态复现怎么做的(可复现步骤)
给想自己验证的人,思路比结论重要:
1. **环境变量隔离副本**(和正式版共存):
- `ZCODE_DATA_BASE_DIR` → 沙箱数据目录
- `ZCODE_DESKTOP_USER_DATA_DIR` → 独立 Electron userData(打包版会忽略 `--user-data-dir` 参数强制写 `AppData\Roaming\ZCode`,不隔离会撞单实例锁直接退出)
- `HOME` → 沙箱 home(**host 进程读的是 HOME 下的 .zcode**,跟主进程不是同一套目录,凭据要两边都放)
- `ZCODE_BASE_URL=http://127.0.0.1:18080` → 这是官方预留的端点覆盖:所有 `https://zcode.z.ai` 的请求会被原样重写到这个 origin,**一个环境变量就能把 OAuth、凭证、OSS 直传地址、callback 全部接管到本地 mock**
2. **伪造登录态**:按代码逻辑构造 `credentials.json`(键名 `oauth:active_provider`、`oauth:bigmodel:access_token`、`zcodejwttoken` 等,值用 `enc:v1:AES-256-GCM` 格式自己加密,密钥来自 `ZCODE_CREDENTIAL_SECRET`)。启动后 UI 显示已登录 MockUser。
3. **mock 服务端**:`upload-credential` 返回自己生成的 RSA 公钥(私钥自己留着,就能解密上传内容)+ 假 OSS 签名;另起一个 Anthropic 兼容的 `/v1/messages` 假模型让 agent 会话能跑起来。
4. **CDP 驱动 UI**:打包版禁了内置 9229,但 Electron 仍接受 `--remote-debugging-port` 命令行参数,连上后直接 DOM 操作输入 prompt 点发送。
5. **抓包**:50ms 轮询 `checkpoints/*/pending/` 抓 envelope + 密文;mock 端 `oss-upload` 落盘收到的 multipart。
时间线实测(第一条 prompt):
```
17:22:59.859 POST /api/v1/event/report
17:23:00.187 GET /api/v1/snapshot/upload-credential?workspace_id=c5280988b067
17:23:00.308 POST /oss-upload bytes=3765 ← 第1个包(prompt 路径)
17:23:00.977 GET /api/v1/snapshot/upload-credential
17:23:01.097 POST /oss-upload bytes=3733 ← 第2个包(wiki 路径)
```
---
## 六、其他上传通道(顺带确认)
| 通道 | 内容 | 触发 |
|---|---|---|
| 遥测 `POST /api/v1/event/report` | 事件名、device_mid(随机 UUID)、版本、分辨率、时区 | 自动,无文件内容 |
| 用户反馈上传 | 日志 zip + 附件 → OSS | 仅手动提交反馈时 |
| Git Checkpoint | `git add -A`(隔离索引)→ commit-tree → `refs/zcode/checkpoints/*` | **纯本地**,无网络行为 |
| 崩溃报告 | `uploadToServer:false` | 仅本地 |
端点清单:快照凭证 `zcode.z.ai/api/v1/snapshot/upload-credential`;对象直传 OSS(host 由凭证响应动态下发,客户端无白名单);OAuth `zcode.z.ai/api/v1/oauth/token`;更新检查 CDN。另有一个开发内网地址 `http://192.168.6.166:8080` 作为 dev 回退,生产不生效。
---
## 七、怎么判断自己有没有被传 / 怎么防
**查痕迹**:
- `~/.zcode/v2/checkpoints/` 目录存在且有内容 = 传过(或至少生成过)
- 日志里搜 `upload-credential`
- git 仓库里 `git for-each-ref refs/zcode/*`(这个是本地 checkpoint,不代表上传)
**防护**:
1. **最有效:不登录 OAuth 账号,纯 API Key 直连**——token 取不到,链路第一行代码就 return。我的本机实测零上传。
2. git 项目用户要有清醒认知:**在 ZCode 里打开 git 仓库 = 全仓库(含历史)可见**。真要用的话把密钥文件 `.gitignore` + 不提交(实测唯一两层都躲过的情形);进过历史的密钥立即轮换,别指望删除文件。
3. 网络层面可以拦 `zcode.z.ai/api/v1/snapshot/*`(凭证接口,拦它最彻底——OSS host 是动态下发的,拦 OSS 域名没意义)。
4. 期待官方把 `repoSnapshotIndexingEnabled` 做成真正的开关,并在 UI 里明示这个行为——目前我在代码里没找到这个设置能拦住上传。
---
## 八、声明
- 分析对象:ZCode 桌面版 3.11.2/3.12.3(Windows x64),app.asar 静态解包 + 沙箱动态复现(三轮:裸目录 / git 仓库 / 二级目录,每轮均有解密还原的 tar.gz 为证)。
- 所有验证在本地沙箱 + 自建 mock 服务端完成,**没有向任何真实服务端发送数据**;测试工作区文件均为我自造的无害内容(含假 token、假密钥)。
- 证据包(三份报告、mock 代码、全部请求日志、密文与解密产物)随帖附上;对应版本的官方安装包也一并附上,方便自行解包核对。
- 如果这是官方有告知的云功能(比如我漏看了某处用户协议),欢迎指出;但"默认开、无感知、传 prompt 原文、git 历史免检上传"这个组合,我认为值得每个用户知道。