サプライチェーンとしての Claude Code スキル: 220 万件の採用、レジストリなし、スキャナーがソースを読む一方で Python はキャッシュを実行する
スキルは実行する前に読む、というのが定番の助言ですが、この助言には穴が 2 つあります。コピーしたスキルには更新の経路がないため、読んだコピーは知らないうちに元のスキルとずれていくことがあります。そして Python を同梱するスキルでは、読んだファイルとインタープリターが読み込むファイルが同じとは限りません。
今週、2 本の論文が 1 日違いで arXiv に公開されました。この 2 本は、同じ 1 本のパイプの両端を描いています。Skill Constellations は、SKILL.md ファイルが GitHub のリポジトリ間をどう移動するかを調べています。PyCache Trap は、そうしたファイルが Python を伴って届き、スキャナーが安全かどうかを判定するときに何が起きるかを調べています。
定番の助言は「スキルは実行する前に読む」です。2 本の論文は、合わせるとこの助言の 2 つの穴になります。読んでいるのはコピーであり、元のスキルが変わっても何も知らせてくれません。そしてスキルが Python を同梱している場合、インタープリターは開いたファイルとは別のファイルを読み込むことがあります。
どちらの論文も Claude Code だけを扱っているわけではありません。スキルのフォーマットはオープンで、Codex や Cursor なども読み込みます。ただ、このフォーマットを導入したのは Anthropic で、普段使っているのも Claude Code です。以下の問いは、自分のスキルフォルダーについて確かめたかったことです。論文の数値は引用であり、再現はしていません。記事の中ほどにある Python の挙動は、手元のマシンで実際に確認しました。
Claude Code スキルはレジストリなしで GitHub 上をどう広がるのか
コピーによって、それも多くはまとめて広がります。Skill Constellations は GitSkills データセットを使っています。ファイル名が正確に SKILL.md で、有効なフロントマターを持ち、2025 年 10 月 1 日以降にフォークではないリポジトリへ最初にコミットされたファイルをすべて集め、git の履歴をたどったものです。論文が数えた採用は 2,193,119 件です。採用とは 1 つのリポジトリが 1 つのスキルを取得することで、元の作者による最初のコミットも含みます。つまりこれはリポジトリとスキルの組の数です。何人がそれを実行しているかは分かりません。
注目すべき単位はコピーイベントです。10 個以上のスキルを一度に追加するコミットで、そのリポジトリが持つスキル系統の半分以上を、ある 1 つの先行リポジトリがすでに持っていた場合を指します。スキルは、開発者が道具を 1 つ選んで入れるというより、ある先行リポジトリにさかのぼるフォルダーごと届くのが典型です。
分布はそこから予想できるとおりです。大半の採用はスキルを誰にも渡さず、少数のリポジトリがほぼすべての伝播を占めています。
GitHub のスターで、監査すべきスキルリポジトリは分かるのか
分かりません。それも大差で、です。論文によると、スターは、あるリポジトリからどれだけ多くのリポジトリがコピーするかについてほとんど情報を持ちません。すでにコピーされた回数で順位を付けると、スターで順位を付けた場合の 4.0 倍の後続の伝播を特定できます。
コピー数を素朴に数えても誤ります。コピー数の上位 20 リポジトリのうち、最初に持っていたスキルだけを各リポジトリの実績とすると、上位 20 に残るのは 7 つだけです。残りは、出どころのように見える後発の採用者です。
中心となる数値はリプレイから出ています。2026 年 4 月 1 日より前のデータで、コピーイベントがどの先行リポジトリを選ぶかのモデルを当てはめ、そのモデルで上位 100 のリポジトリを監査します。ここでの監査とは、フラグの付いたスキルを取り除くことです。これで後のハイリスクな採用の 14.9% を防げます。スター上位 100 を監査した場合は 0.5% です。どちらにしても上限は低いままです。論文によれば、同じ 100 件の監査なら、後知恵で選んだ順位付けでも防げるのは約 4 分の 1 にとどまります。ハイリスクな採用のおよそ半分は新しいスキルとして現れ、既存の出どころを監査しても見えなかったはずだからです。
コピーされた Claude Code スキルは元のスキルの修正を取り込むのか
ほとんど取り込みません。論文によれば、スキルのコピーのうち、元のスキルの後の編集に追従するのは 20.9% です。比較として論文は、コピーされたコードファイルに関する先行研究を挙げています。そちらでは上流のセキュリティ修正が 47% から 84% の割合で取り込まれます。同一コピーのグループ内で、全メンバーにそろって反映された変更は 11.1% です。
理由は仕組みにあります。スキルはフォルダーです。自分のリポジトリやホームディレクトリにコピーされた時点で、どこから来たかを覚えておく義務は何にもありません。出どころを記録するインストーラーもあり、論文はその記録を日付の検証に使っています。しかし単純なコピーには、バージョンも、ロックファイルのエントリも、元の URL もありません。上流の修正は、自分で見に行かない限り届きません。
手元のマシンには両方があります。マーケットプレイスから入れたプラグインには出どころとバージョンがあり、更新はそこを通って届きます。手作業で ~/.claude/skills にコピーしたフォルダーにはどちらもなく、元のスキルがその後変わったかどうかを知らせてくれるものはありません。
Skill Constellations でスキルの「ハイリスク」とは何を意味するのか
悪意ではなく、能力です。論文は、実行ファイルを同梱するスキル、ツールを事前承認するスキル、危険な操作を指示するスキルにフラグを付けます。そして、このフラグはスキルに何ができるかを示すもので、作者の意図を示すものではないと明言しています。ラベル付きの 200 スキルのサンプルで、厳格なフラグの適合率は 98.3%、再現率は 80.9% です。つまりリスクの件数は下限です。
Claude Code では、2 つ目の項目に名前があります。スキルのフロントマターにある allowed-tools フィールドです。ドキュメントの言葉では、次のものを列挙します。
Tools Claude can use without asking permission during the turn that invokes this skill.
フロントマターに allowed-tools: Bash(python3 *) があると、Claude はスキルの隣にあるスクリプトを毎回確認せずに実行できます。管理ポリシーでこの許可が無効にされている場合は別です。これは設計どおりの挙動で、便利でもあります。同時に、同梱の Python スクリプトが、誰もコマンドを見ないまま実行されうるということでもあります。PyCache Trap が調べているのは、まさにこの状況です。
PyCache Trap はどうやってスキルをスキャナーに通すのか
スキャナーにはあるプログラムを、インタープリターには別のプログラムを渡します。パッケージには無害な helper.py と、__pycache__ の中に同じモジュールのコンパイル済みファイルが入っていて、後者は挙動が違います。スキャナーはソースを読みます。Python は、モジュールがインポートされたとき、キャッシュのヘッダーがローダーのチェックを通ればキャッシュを読み込みます。
論文は 7 つのスキルスキャナーを 100 個のスキルで試しています。これは適応的な条件での結果です。攻撃者はスキルごとに最大 5 つの候補を出せて、スキャナーのフィードバックも見られます。この条件で、攻撃はスキャナーごとに 94% から 100% の割合で通過し、7 つのどれもキャッシュの動作を認識できませんでした。うち 4 つのスキャナーについて、著者らは通過したスキルを MiniMax-M3 が駆動する Claude Code のハーネスでも実行しています。100 件中 76 から 83 件で、スキルはスキャナーを通過し、隠された挙動が実行されました。ペイロードは合成されたもので、シークレットはダミーであり、実際のプラットフォームには何も公開されていません。
ヘッダーの検査だけでは足りない理由を、論文は 1 文で述べています。
Header consistency alone cannot establish derivation because the publisher controls
both header and body.
差し替えられた Python キャッシュは git clone を生き残るのか
キャッシュの種類によります。そして論文は配布経路をまったく検証していません。そこで Linux 上の Python 3.12.3 と git 2.43 で確かめました。使ったのは、ソースからはある文字列を、仕込んだキャッシュからは別の文字列を返すモジュールです。以下はその環境で観察した結果です。
Python が .pyc を検証する方法は 3 つあり、ヘッダーで指定されます (PEP 552)。デフォルトはソースファイルの更新時刻とサイズを保存します。ハッシュベースの 2 つのモードは、代わりにソースのハッシュを保存します。一方はインポートのたびに照合し、もう一方はインタープリターに照合を指示されない限りハッシュを信頼します。
| 仕込んだキャッシュ | git clone | zip, tar | --check-hash-based-pycs always |
|---|---|---|---|
| タイムスタンプ (デフォルト) | ソースが実行される | キャッシュが実行される | 該当なし |
| ハッシュ、照合なし、誤ったハッシュ | キャッシュが実行される | キャッシュが実行される | ソースが実行される |
| ハッシュ、どちらのモードでも、本物のソースハッシュ | キャッシュが実行される | キャッシュが実行される | キャッシュが実行される |
タイムスタンプ型はクローンを生き残りませんでした。git は更新時刻を保持しないので、ヘッダーが一致しなくなったためです (クローンが元と同じ秒に展開された場合は一致したままです)。zip と unzip、tarball、cp -a では生き残りました。これらは時刻を復元するからです。時刻を復元しないツールなら、クローンと同じようにキャッシュは無効になります。
ハッシュ型はファイルの時刻をまったく気にしません。最後の行を見てください。配布者はクリーンなソースの本物のハッシュを計算してヘッダーに書き込み、好きなボディを付けます。Python はソースをハッシュし、一致を確認して、そのボディを読み込みます。インタープリターの最も厳格なフラグでも防げません。ヘッダーとボディについての論文の指摘を、数行で再現したことになります。以上はすべて、Python のマイナーバージョンが同じであることが前提です。別のバージョン向けに作られたキャッシュは無視されます。
これには限界が 2 つあります。キャッシュが git 経由で届くのは、配布者がそれをコミットした場合だけです。多くの Python プロジェクトは __pycache__ を無視しますが、.gitignore のないスキルリポジトリでは、単純な git add -A でキャッシュも追加されます。テストに使ったリポジトリがそうでした。また、キャッシュから読み込まれるのはインポートされるモジュールだけです。スキルが直接実行するスクリプトは、必ずソースからコンパイルされます。
python -B や __pycache__ の削除は Claude Code スキルを守るのか
-B は守りません。削除は守ります。python -B と PYTHONDONTWRITEBYTECODE は、Python がキャッシュを書き込むのを止めます。すでにあるキャッシュを読むのは止めません。テストでは、仕込んだキャッシュは、-B なしで実行されたすべてのケースで、-B を付けても実行されました。
テストで有効だったのは 3 つです。1 つ目は __pycache__ ディレクトリの削除で、その後 Python は読めるソースからコンパイルします。2 つ目は PYTHONPYCACHEPREFIX を自分のディレクトリに設定することで、Python はソースの隣をそもそも見なくなります。3 つ目はフォルダーに対して python -m compileall -f を実行することで、論文の修復手順は実質これです (著者らが試したコードを含む 99 パッケージすべてで、差し替えられたバイトが取り除かれました)。この 3 つ目が最も弱い方法です。書き直されるのは実行に使ったインタープリターと最適化レベルのキャッシュだけで、ディレクトリ内の他の .pyc はそのまま残ります。まず削除してください。
バージョン付きの参照は Claude Code スキルの何を変えるのか
コピーに、見に行く先ができます。Skill Constellations はこの提言で終わっています。プラットフォームはコピーではなく、バージョン付きの参照を配布すべきだというものです。参照には、固定したり取り消したりできる出どころがあります。コピーされたフォルダーの向こう側には何もありません。
現在の Claude Code でそれに最も近いのはプラグインです。プラグイン経由で入れたスキルには出どころとバージョンがあり、更新の仕組みもあります。自動で更新されるマーケットプレイスもあれば、設定で有効にするものもあります。これで凍結したコピーの問題は解決しますが、もう 1 つの問いが開きます。プラグインディレクトリの記事で取り上げた問いです。掲載はレビューについての表明であり、レビューは決まった時点で行われます。ディレクトリは新しいバンドルバージョンをすべてスキャンします。マーケットプレイスの URL から追加したプラグインは、レビューをまったく受けません。どちらの場合も、更新の経路は修正を届けますし、配布者が次に出すものも何であれ届けます。
論文自身が示した上限も、ここに当てはまります。同じ 100 リポジトリという予算では、後知恵で選んだ順位付けでも、後のハイリスクな採用の約 4 分の 1 しか防げません。順位付けやレジストリは問題を狭めます。スキルがマシンに入った後に何をしてよいかは、やはりそのマシンの上で決まります。インストールするどのツールでも同じです。
今週、Claude Code のスキルフォルダーをどう点検するか
コマンドは 4 つで、最初の 3 つはキャッシュに関するものです。個人のスキルが ~/.claude/skills に、プラグインが ~/.claude/plugins にある前提です。プロジェクトをクローンしているなら、その .claude/skills も加えてください。
# 1. Bytecode caches sitting next to skill code
find ~/.claude/skills ~/.claude/plugins -name '__pycache__' -type d
# 2. Hash-based caches: the kind that survives a clone
find ~/.claude/skills ~/.claude/plugins -name '*.pyc' \
-exec sh -c 'xxd -s 4 -l 4 -p "$1" | grep -qE "^0[13]000000" && echo "$1"' _ {} \;
# 3. Remove them all; Python recompiles from the source you can read
find ~/.claude/skills ~/.claude/plugins -name '__pycache__' -type d -prune -exec rm -rf {} +
# 4. Skills that pre-approve tools
grep -rlE '^allowed-tools:' --include=SKILL.md ~/.claude/skills ~/.claude/plugins手元のマシンでは、1 つ目のコマンドでディレクトリが 1 つ見つかります。自分で書いたスキルの下にあり、ヘッダーはタイムスタンプ型なので、作ったのは手元のインタープリターです。2 つ目では何も見つかりません。キャッシュを同梱していたプラグインはありませんでした。これは予想どおりの結果です。ハッシュベースのキャッシュには、再現可能なビルドなどの正当な用途があるので、見つかったこと自体は何の証明にもなりません。それでも、自分で書いていないスキルに同梱されたキャッシュは、種類を問わず確認する価値があります。削除しても失うものはありません。Python がソースから作り直します。
4 つ目の一覧は、時間をかけて読んでください。シェルのパターンを事前承認してスクリプトを同梱するスキルに、何も問題はありません。ただ、そこは SKILL.md を読んでも得られる情報が最も少ない場所です。実行されるのは文章ではないからです。クローンしたリポジトリの設定ファイルと同じ教訓です。ざっと読むファイルと実行されるものは 2 つの別物で、承認が及ぶのは後者です。