ZCodeがGitの全履歴を無言でアップロードする怪しい挙動を暴く
ZCodeはログイン状態でワークスペース全体と完全なGit履歴を無言でパッケージ化してクラウドオブジェクトストレージへアップロードし、復号用秘密鍵はサーバー側にのみ保存される。本記事ではローカルフォレンジックとクライアントコードのリバースエンジニアリングにより完全なアップロード経路と暗号化機構を復元し、ファイルシステムの不変ロックによってこの行為を完全に阻止する方案を示す。
日本語
コピー
ZCodeがGitの全履歴を無言でアップロードしていた件
もともとはディスクを整理していて、~/.zcode がなぜ700MB以上も食っているのかをちらっと見ただけだった。断続的に調べていくうちに、かなりおかしな事実を確認するに至った。
もともとはディスクを整理していて、~/.zcode がなぜ700MB以上も食っているのかをちらっと見ただけだった。断続的に調べていくうちに、かなりおかしな事実を確認するに至った。
ログイン状態であれば、ZCode(智谱公式のAIコーディングデスクトップクライアント)はバックグラウンドでワークスペース全体を黙って固めて暗号化し、そのままAlibaba Cloud OSSへアップロードする。対象には .git の完全な履歴、LFSの大容量ファイルキャッシュ、reflog、そしてグローバルなアプリ設定まで含まれる。
さらに皮肉なのは、暗号化に使うRSA公開鍵はサーバー側から動的に配信され、秘密鍵はクラウドにしか存在しないことだ。ローカルで生成された数百MBの暗号文は、あなた自身にもクライアント本体にも復号できない。
この記事では、調査の経緯、証拠の連鎖、そして最終的なワンクリック防御策までを一通り記録しておく。
起点:pendingのまま止まっていた313MBの圧縮ファイル
~/.zcode はZCodeのローカルデータのルートディレクトリだ。当時スキャンしたディレクトリの容量分布はおおよそ次のとおり。
cli/:約257MB(ローカルセッションデータベース、実行ログ)computer-use/:約130MB(アプリ本体と実行時依存)v2/checkpoints/:約303MB(今回の主役)
v2/checkpoints/ を開くと、313MBの .enc 暗号化ファイルと、状態ファイルが1つ転がっていた。
{
"workspacePath": "/Users/ferstar/myprojects/<某商业项目>",
"lastCompressedSize": {
"encryptedSizeBytes": 313070842,
"workspaceSizeBytes": 345549173
},
"kind": "baseline",
"failureCount": 564
}
意味は明快だ。
- クライアントはローカルで開いていた商用プロジェクトをスキャンし、
node_modulesなど少数のディレクトリを除外したうえで、残り345MBの中身を313MBの圧縮ファイルに固めて暗号化し、baseline(フルスナップショット)としてマークした。 - 状態を見るとアップロードは564回失敗しており、そのためローカルの
pending/ディレクトリに留まったままリトライを待っている。
プロジェクト全体は10GBで、依存を除外した残りの345MBはほぼすべて中核資産だ。
どこへ送るのか:ログからasarのリバースまで
ログにはアップロード先の具体的なアドレスが直接出ていなかったので、ついでにクライアントの app.asar を剥がしてコードを読んだ。アップロード経路全体を復元すると次のようになる。
sequenceDiagram
participant C as ZCode 客户端
participant S as zcode.z.ai
participant O as 阿里云 OSS
C->>S: POST /api/v1/snapshot/upload-credential
S-->>C: snapshot_id + RSA公钥 + max_size + OSS表单凭证 + callback
C->>C: tar.gz 打包 → AES-256-CTR 加密 → RSA-OAEP 包裹密钥
C->>O: PostObject 表单直传 tar.gz.enc
O->>S: callback 回调确认接收
流れは2段階に分かれる。
- まず調整サーバーに資格情報を取りに行く:クライアントは
https://zcode.z.ai(コード上はVITE_ZCODE_ENDPOINT_ORIGIN)をリクエストし、サーバーはOSSのフォーム署名(policy、x-oss-signature)、動的に割り当てたObject Key、サイズ上限、そして今回の暗号化に使う公開鍵を返す。 - フォームでOSSへ直接アップロード:クライアントはローカルで固めてストリーミング暗号化した後、智谱自身の業務サーバーを一切経由せず、HTTP POSTフォームで
tar.gz.encをAlibaba Cloud OSSへ直接投げ込む。送信後はOSSサーバー側がcallbackを発火し、智谱のバックエンドに登録を知らせる。
ZCode実行時の接続をキャプチャして見たところ、プロセスが常時張っているHTTPS接続はちょうど zcode.z.ai の解決IPと、Alibaba Cloud OSSのノード2つだった。
最も皮肉な部分:この鍵はサーバー側のもの
クライアントが使う暗号化は標準的なエンベロープ暗号化(Envelope Encryption)だ。
keyId: String(i.encryption.key_version),
keyWrapAlgorithm: "rsa-oaep-sha256",
publicKeySpkiPem: Ylt(i.encryption.public_key)
- ファイル内容はランダム生成した対称鍵でAES-256-CTR暗号化する。
- 対称鍵はRSA-OAEP-SHA256で包む。その公開鍵が、先ほどサーバーから動的に取得したものだ。
鍵となるのはこのRSA公開鍵で、アップロード資格情報の配信時にサーバーがその場で渡すもので、秘密鍵は最初から最後までクラウドにしかない。手元のすべての秘密鍵でenvelope内の鍵を解こうとしたが、当然のごとく全滅した。
つまりこういうことだ。ディスク上にある313MBの暗号文は、あなたには開けないし、ZCodeクライアント自身にも開けない。世界中で智谱のバックエンドの秘密鍵だけが復号できる。
もしこれが本当にユーザーのレジュームやクロスデバイス同期のためのものなら、鍵はローカルに紐づいているはずだ(GitやTime Machineのように)。サーバー側しか開けない鍵の目的はただ一つ、サーバーが一方的に見られることを保証することだ。
スナップショットの中身:ほぼ9割が.git
暗号文は開けないが、スナップショット生成時に残されたManifest(ファイル一覧)はローカルにはっきり書かれている。42,411ファイルを含むこの一覧を集計すると次のようになった。
| 内容 | 容量 | 割合 | 含まれる情報 |
|---|---|---|---|
.git/lfs/ | 196.1 MB | 56.8% | LFSキャッシュ。プロジェクト履歴で取得したすべての大容量ファイルとバイナリ資産 |
.git/objects/ | 102.2 MB | 29.6% | 完全なGit履歴オブジェクト庫(Commit、Tree、Blob) |
.git/logs/ | 0.6 MB | 0.2% | reflogの軌跡。ローカルのすべてのブランチ操作と未プッシュの記録 |
| その他のソースとドキュメント | ~46.2 MB | 13.4% | src/、各種設定ファイル、業務コード |
.git の1ディレクトリだけで実に 86.6% を占めている。
つまり、このパッケージがアップロードされれば、クラウドが手に入れるのは現在のワークスペースのコードだけではない。このリポジトリが作られて以来の全履歴のすべてだ。
- とっくに上書き削除された機密設定や過去のkey。
- まだリモートにプッシュしていないローカルのブランチ名(未公開の開発動向がそのまま露呈する)。
.git/configに設定された社内自前GitLabのドメインとリポジトリパス。
さらにコードには repo_snapshot_extra_manifest も含まれており、ZCodeのグローバル設定ファイル(例えば settings.behavior.json)をハッシュ化したうえで、ワークスペースをまたいでまとめ、スナップショットのたびにクラウドへ送信する。
スイッチの真相:切ったところで、そいつには届かない
多くの人はまず設定を開いてスイッチを探し、オフにしようとする。設定項目とコードのロジックを一つずつ突き合わせてみた:
| スイッチ | あなたが思う制御対象 | 実際の制御対象 |
|---|---|---|
体験を最適化 (optimizeAgentExperienceEnabled) | データ収集 / テレメトリ送信 | あなたのデータをモデルの学習に使うかどうかだけ。オフでもスナップショットは取得され送信される |
リポジトリのスナップショットインデックス (repoSnapshotIndexingEnabled) | スナップショット機能そのもの | サーバー側がスナップショットを受け取った後、インデックスを構築するかどうかだけ。オフでもローカルのパッケージングと送信は一切止まらない |
クライアント側の組み立てコードを見ればもっと露骨だ。スナップショットの取得と送信を担う sidecar は、起動時に無条件でインスタンス化される。コード内にユーザー設定を見る if 分岐など存在せず、唯一の条件は tokenProvider がログイン後の JWT を取得できることだけ。
要するに:アカウントにログインしている限り、この送信機構は常時オンであり、UI にそれを止めるスイッチは一つもない。
取得のトリガーは二つ。一つは captureBeforePrompt(Prompt を送信するたび)、もう一つはタスク終了時の repo-wiki-update マーク。ログを追うと、単一のアクティブセッションで最大 62 回のスナップショット取得が発生している。
プライバシーポリシーは何と言っているか
ZCode のプライバシーポリシーを確認したところ、「会話中に送信されたテキスト、ファイル、コード」を収集すると明記されている。これは AI アシスタントがモデル推論を呼び出す際の通常の動作で、どの社も同じだ。
しかし、ワークスペース全体を完全な Git 履歴ごと黙ってパッケージングして送信することには一言も触れていない。公式ドキュメント、FAQ、更新履歴にも、これに関する説明は一切ない。
一致するのは、あの万能な定型文だけだ:「最適化プログラムはデフォルトでオフです。自ら参加しない限り、入力を学習に使用することはありません」。
防御:削除はモグラ叩き、ディレクトリを直接ロックする
この pending パッケージに気づいた当初、ついでに削除した。すると 30 分も経たないうちに再度取得された——新たな 313MB の圧縮パッケージ、失敗カウントは 564 から 565 に跳ね上がった。アップローダーはローカルファイルが消えたことを検知し、そのまま再パッケージングする。手動削除だけではモグラ叩きだ。
最も直接的で効果的な方法は、ファイルシステム層で不変ロックをかけ、カーネルレベルでこのディレクトリへの書き込みを禁止すること:
macOS
# 清空并锁定 checkpoints 目录
rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
chflags uchg ~/.zcode/v2/checkpoints
# 验证:应该输出 Operation not permitted
touch ~/.zcode/v2/checkpoints/test
Linux
# 清空并锁定 checkpoints 目录
rm -rf ~/.zcode/v2/checkpoints
mkdir -p ~/.zcode/v2/checkpoints
sudo chattr +i ~/.zcode/v2/checkpoints
# 验证:应该输出 Operation not permitted
touch ~/.zcode/v2/checkpoints/test
影響と復旧
- ブロック効果:スナップショットロジックがディレクトリに書き込もうとした瞬間、カーネルに遮断される。生成物がなければ、その後の OSS への直接アップロードも当然起こり得ない;
- 機能への影響:ZCode の「チェックポイントロールバック / タイムライン」機能は使えなくなる(元々、全コードをクラウドに送ることで得ていたものだ)。日常のコード補完、対話、ツール呼び出しは完全に正常で、ログに飲み込まれる IO エラーは使用に影響しない;
- 復旧したい場合:
chflags nouchg ~/.zcode/v2/checkpoints(macOS)またはsudo chattr -i ~/.zcode/v2/checkpoints(Linux)を実行すればよい。
最後に
AI ツールを使えば、モデル推論がコードのコンテキストを消費するのは必然で、使う側もそれは承知している。だが、この件が一線を越えている点は明確だ:
一つはデータの範囲。推論に渡されるのは現在のタスクに関連するコンテキストだが、スナップショットはリポジトリ全体を数年にわたる Git コミット履歴ごとまとめて持ち去る。
もう一つはアーキテクチャの姿勢。本当にユーザーのためのチェックポイント復元やクロスデバイス同期なら、復号鍵はユーザーのローカルにあるべきだ。サーバー側しか解けない鍵、開示ゼロのプライバシーポリシー、オフにできないデフォルト動作、削除しても自動で再送される執念——これは明らかにバックアップではなく、採取に近い。
ツールに原罪はない。だが一線は使う側が引くべきだ。ソフトウェアの中でオフにできないのなら、OS のロックで檻に閉じ込めてやればいい。