• BiLo8

    总结:这简直就是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(这就是每条消息都会带的东西):

    {
    "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` 段的文件直接放行——**大小检查、敏感名检查、二进制检查全部跳过**:

    // shouldIncludeRepoSnapshotPath
    路径含 .git 段 → { include: true } // 直接放行,后面所有检查不执行```

    解密后的包(19KB,对比第一轮 1.4KB):.git 整目录 35+ 个文件原样进包,加上 git 可见的 4 个文件。最关键的发现:

    上传物实测内容
    .git/configremote URL 里的 token 明文(我埋的假 token https://user:[email protected]/private/repo.git 原样在内)
    .git/logs/HEAD完整 reflog 操作历史
    .git/objects/**所有提交过的文件内容的 zlib blob
    .git/objects/pack/big-pack-test.pack2MB 的 pack 原样上传——>1MB 限制对 .git 不生效

    被"排除"的文件全部从 .git/objects 绕出去了

    我把第一轮被规则拦住的每个文件的 blob 逐个解压验证:

    被第一层排除的文件藏在哪个 blob解压后
    .envobjects/55/e9a9c4...SECRET_KEY=should-be-excluded-by-name
    api_token.txtobjects/d3/6304...inline-token-file-should-be-excluded
    bigfile.binobjects/69/a7cc...完整 1,048,586 字节
    dist/bundle.jsobjects/4e/f088...build output should be excluded
    node_modules/dep.jsobjects/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 Checkpointgit 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 历史免检上传"这个组合,我认为值得每个用户知道。
    主楼
  • momo-linuxdo

    这回真的是码都没了。grok刚刚被喷完,zcode就出来,还好我用codex,暂时没被锤 ^-^

    #1
  • Taosu

    这是个人数据一网打尽,被Z/顷刻炼化了。真是服了,拿对话数据训练还不满足,直接开偷了 ^-^

    #2
  • Ys_Ltr

    只用了一次,领的鸡蛋又慢又难吃。

    拿的还是私人项目,没有公开的,不知道会不会被z/牛走。

    #3
  • 纯真的薯薯

    这下知道国产模型的训练来源之一了 ^-^ 原来是这样的

    #4
  • Iza

    我想知道他这么偷已经偷多久了,感觉像是惯犯

    #5
  • Eevee

    这已经是犯罪级别了吧

    难以想象,每天给码农发鸡蛋,然后……

    #6
  • LY99

    偷数据偷了个爽这是……

    #7
  • Ringo

    有佬反馈起码三个月,不确定真假。但是最近发鸡蛋用的人多了肯定比之前偷的爽多了。

    #8
  • AIInAll

    佬是怎么发现这个事的?看起来很难发现啊

    #9
  • 汉武帝(的大熊猫)

    Aoe国模你也是牛逼。 kimi和dsh都是开源的。

    #10
  • 南朝朴素的灯花

    a/都不敢明偷

    #11

发表回复

登录后回复