Claude Codeプラグインのキャッシュとファイル解決の仕組み
マーケットプレイス経由のプラグインはバージョンごとにキャッシュへコピーされ、npm依存は自動インストール・古いパスの参照はパストラバーサル制限で拒否されます。symlinkの扱いも3パターンに分かれます。
Claude Codeプラグインのキャッシュとファイル解決の仕組み
マーケットプレイス経由でインストールしたプラグインは、元のディレクトリではなく~/.claude/plugins/cacheにコピーされたバージョンごとのディレクトリから読み込まれます。「更新したのに古い挙動のまま」「スクリプトが隣のファイルを見つけられない」「依存パッケージが入っていない」といったつまずきは、ほぼこのコピー方式から生じます。症状ごとに原因を切り分けられるよう、読み込み方・旧バージョンの掃除・依存の自動インストール・パスの制限・symlinkの順に見ていきます。
コピーされるプラグインと、その場で読まれるプラグイン
すべてのプラグインがキャッシュにコピーされるわけではありません。読み込み方で挙動が変わります。
キャッシュにコピーされるか、その場で読まれるか
コピーされる
リモートのマーケットプレイスや、ローカルのdirectory以外から入れたプラグインです。インストール時にcache/<marketplace>/<plugin>/<version>/へコピーされ、${CLAUDE_PLUGIN_ROOT}はそのコピーを指します。プラグインディレクトリの外のファイルはコピーされません。
その場で読まれる
--plugin-dirや.claude/skills/のプラグイン、ローカルディレクトリから追加したマーケットプレイスの相対パスプラグイン、リンクモードのコマンドソースです。ローカルディレクトリのマーケットプレイスでは、ソースの編集が次のセッション開始か/reload-pluginsで反映され、バージョンを上げる必要もありません。
--plugin-urlのアーカイブや--plugin-dirの.zipは、セッション用の一時ディレクトリに展開されます。キャッシュなどの置き場所はCLAUDE_CODE_PLUGIN_CACHE_DIRで変えられます。名前に反して、これはキャッシュそのものではなく親ディレクトリ(既定は~/.claude/plugins)を指定する変数です。この親ディレクトリには、cache/のほかにmarketplaces/、synced/、.trash/があります。
バージョンごとのディレクトリ名は、プラグインのマニフェストに書いたversionが最優先で決まります。"version": "1.0.0"を固定したままコミットだけ進めても、claude plugin updateは更新済みと判断し、キャッシュのコピーは置き換わりません。githubやgit-subdirソースでversionを省くと、ソースのコミットSHAを12文字に縮めた値がバージョンになります。
優先順位は、マニフェストのversion、マーケットプレイスエントリのversion、ソース種別の順です。ソース種別では、archiveがSHA-256の12文字になります。npmと、gitでないローカルはunknownです。commandソースは常にハッシュになります。
更新後に旧バージョンはどう片付くか
更新やアンインストールをすると、旧バージョンのディレクトリに.orphaned_atというマーカーが書かれ、14日後のバックグラウンド処理で削除されます。
旧バージョンが消えるまで
- 1
更新またはアンインストール
新しいバージョンのディレクトリが作られ、旧ディレクトリに
.orphaned_atが書かれます。 - 2
14日間の猶予
旧バージョンを読み込み済みの別セッションは、そのまま動き続けます。
- 3
バックグラウンドで削除
14日後の掃除処理で旧ディレクトリが消えます。インストール記録(
installed_plugins.json)に1件も残っていないと、この処理は走りません。
最後の1つをアンインストールすると、孤立したディレクトリは次にプラグインを入れるまでディスクに残ります。容量が気になるときは、cache/の下を手で確認する場面があります。
プラグインの永続データは別の場所で、~/.claude/plugins/data/<plugin-id>/(環境変数${CLAUDE_PLUGIN_DATA})に置かれます。更新しても残り、最後のスコープからアンインストールしたときに既定で削除されます。バージョンごとに変わる${CLAUDE_PLUGIN_ROOT}へ状態を書くと、更新のたびに失われます。
Node.jsの依存が入らないとき
プラグインをキャッシュにコピーするとき、Claude Codeはそのプラグインのpackage.jsonが宣言するnpm・Bunのパッケージも同じディレクトリに入れます。新しいバージョンのディレクトリを作るたびに走り、新しいマシンで未キャッシュのプラグインを有効にした場合のセッション開始時にも走ります。実行されるのは、プラグインのルートにpackage.jsonと対応するlockfileが両方あるときです。
| lockfile | 使われるパッケージマネージャー |
|---|---|
bun.lock | 使われるパッケージマネージャーBun |
npm-shrinkwrap.jsonまたはpackage-lock.json | 使われるパッケージマネージャーnpm |
複数のlockfileが同居する場合は、bun.lock、npm-shrinkwrap.json、package-lock.jsonの順で最初に一致したものが使われます。そのパッケージマネージャーがPATHに無くても、別のlockfileにはフォールバックしません。bunが入っていない環境でbun.lockを同梱すると、そこで止まります。npm経由で配布するプラグインでは、npmが公開パッケージからpackage-lock.jsonを除外するのでnpm-shrinkwrap.jsonを使います。
次のlockfileはインストールがスキップされます。
bun.lockb(バイナリ形式で検査できない)yarn.lockとpnpm-lock.yaml- 形式を読めないもの(npmは
lockfileVersionが2か3、Bunのbun.lockは2以下が条件)
インストールには、コードが一切実行されないよう次の制限がかかります。
依存インストールの制限
レジストリのパッケージだけ
lockfileで正確なバージョンに固定されたレジストリパッケージのみが対象です。git・GitHub・フォルダ・workspace・リンクの依存が1つでもあると、インストール自体が行われません。npmの
overridesやBunのpatchedDependenciesを使うプラグインも同じです。スクリプトは動かない
--ignore-scripts付きなので、preinstall・install・postinstallは実行されません。ネイティブモジュールはダウンロードされるだけで、コンパイルされません。厳密な固定と60秒
lockfileの固定バージョンだけを入れ、
package.jsonと依存の一覧が食い違えばスキップします。60秒を超えたインストールは失敗扱いです。lockfileのダウンロードリンクはhttpsでなければならず、例外は利用者自身の既定のnpmレジストリを指すリンクだけです。
インストールは、プラグインの.npmrc・.env・bunfig.tomlを読まない専用のフォルダで行われ、成功するとnode_modulesがプラグインへ移されます。失敗してもプラグイン自体は読み込まれ、足りないパッケージを使う部分だけが動かなくなります。自動インストールを止める設定や環境変数はありません。
失敗したかどうかは、/pluginとclaude plugin listのメモで分かります。ロックファイルとpackage.jsonのランタイム依存があるのにnode_modulesが無いプラグインに、メモが付きます。メモの文面は2通りで、対処が逆です。
are not installed: インストールは実行可能だが終わっていない状態です。タイムアウトや失敗が原因なので、claude plugin update <plugin>@<marketplace>で再試行します。再試行も失敗すれば、出力に原因が出ます。ただしCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICが設定されていると、claude plugin updateと/pluginは再試行を飛ばし、最新版だと報告します。were not installed, because ...: このプラグインでは実行できない状態で、理由がメモに入ります。Yarn・pnpm・bun.lockbのlockfile、またはパッケージマネージャー未導入が例です。この理由が残る限り、更新しても入りません。lockfileが理由なら作者が差し替え、未導入ならそのマネージャーを入れてから更新します。
ネイティブビルドが要るパッケージ、Python依存、Yarn・pnpmでロックされた依存、gitの依存は自動インストールの対象外です。プラグインの環境変数とパスの解決ルールのSessionStartフックの型で、${CLAUDE_PLUGIN_DATA}へ入れます。制限されたネットワークでは、許可すべきホストを公式のネットワーク要件ページで確認します。
「../」で参照すると拒否される理由
プラグインは、自分のディレクトリの外にあるコンポーネントを宣言できません。plugin.jsonでもマーケットプレイスのエントリでも、プラグインルートの外へ解決されるパスは拒否され、そのコンポーネントを除いた残りで読み込まれます。拒否されるのは、../shared-utilsのようにパス自体が外を指す場合と、外へ抜けるsymlinkです。macOSとLinuxでは、パスのどこかにバックスラッシュを含むと、内側を指していても拒否されます。コンポーネントのパスは./commands/deploy.mdのようにスラッシュで書きます。
エラーは/pluginのErrorsタブに出ます。
commands path escapes plugin directory: ./../shared.mdclaude pluginコマンドの出力ではPath escapes plugin directory: ./../shared.md (commands)と表示されます。マーケットプレイスエントリのcommandsパスは、v2.1.251より前はプラグインの外を指しても読み込まれていました。v2.1.251から拒否されます。
事前に検査するときの挙動
claude plugin validateで、plugin.jsonに..を含むパスを書いたプラグインを調べると、v2.1.287では次のように失敗します。作業用の一時ディレクトリで、HOMEとCLAUDE_CONFIG_DIRを空に差し替えて実行した結果です。
claude plugin validate p✘ Found 1 error:
❯ commands[0]: Path contains ".." which could be a path traversal attempt: ./commands/../../shared/shared.md
✘ Validation failed上はエラー部分の抜粋です。このあとに⚠ Found 2 warnings:が続き、descriptionとauthorが未設定だという警告が並びます。終了コードは1です。--strictを付けると警告もエラー扱いになるので、CIで未知のフィールドや不足メタデータまで落とせます。
一方、commands/deploy.mdが外のファイルへのsymlinkで、plugin.jsonには./commands/deploy.mdとだけ書いたプラグインは、同じコマンドでValidation passedになりました。マニフェストの文字列だけを検査しており、symlinkの解決先までは追っていません。外へ抜けるsymlinkの拒否は、読み込み時に起きます。symlinkの解決先まで見て拒否するのはv2.1.257以降で、それより前の版は、パスの綴りだけを検査していました。validateが通っても、Errorsタブが空とは限りません。
インストール時にも、プラグインディレクトリの外のファイルはキャッシュにコピーされません。コピー後のスクリプトがプラグインルートより上のパスを読んでも、そこには何もありません。
共有ファイルを持たせるsymlinkの3通り
同じマーケットプレイス内の他のプラグインとファイルを共有したいときは、プラグインディレクトリの中にsymlinkを作ります。リンク先の解決場所で、キャッシュへのコピー時の扱いが変わります。
| リンク先 | キャッシュでの扱い |
|---|---|
| プラグイン自身のディレクトリ内 | キャッシュでの扱い相対symlinkとして保存され、コピー先のターゲットを指し続ける |
| 同じマーケットプレイス内の他の場所 | キャッシュでの扱い実体化(dereference)され、ターゲットの内容がコピーされる。メタプラグインのskills/が他プラグインのスキルをリンクする用途に使える |
| マーケットプレイスの外 | キャッシュでの扱いセキュリティ上スキップされる |
次のコマンドは、兄弟プラグインの共有スキルへリンクを張る例です。Windowsでは管理者権限のコマンドプロンプトからmklink /Dを使うか、開発者モードを有効にします。
ln -s ../../shared-plugin/skills/foo ./skills/fooローカルパスからのインストールと、コピーモードのコマンドソースでは、プラグイン自身のディレクトリ内に解決されるsymlinkだけが保持されます。それ以外はすべてスキップされます。上の表のうち、マーケットプレイス内の他の場所へ張ったリンクが実体化されるのは、マーケットプレイス経由のコピーのときです。
症状から探す
| 症状 | 見る場所 |
|---|---|
| コミットしたのに反映されない | 見る場所マニフェストのversionが固定されていないか。固定ならclaude plugin updateは更新済みと判断する |
スクリプトが../sharedを読めない | 見る場所コピー先ではプラグインの外が存在しない。共有ファイルは同じマーケットプレイス内に置き、symlinkで取り込む |
| パッケージが無いと言われる | 見る場所/pluginのメモがare not installedかwere not installed, becauseか。lockfileの種類、git依存、overridesの有無 |
| 共有ファイルのsymlinkが消えた | 見る場所リンク先がマーケットプレイスの外。キャッシュ時にスキップされる |
| ディスクに古いバージョンが残る | 見る場所更新から14日未満、または最後のプラグインを消した後で掃除が止まっている |
プラグインの基本構造と自作の手順はClaude Codeプラグイン完全ガイドにあります。Bunのlockfileを同梱する場合の背景はBunとClaude Codeの関係が参考になります。全体像はClaude Code完全ガイドから辿れます。
まとめ
配布する側は、同梱するlockfileをnpm-shrinkwrap.jsonかpackage-lock.jsonにそろえ、共有ファイルを同じマーケットプレイス内に置くと、キャッシュ経由でも手元と同じ構成で動きます。使う側は、更新が反映されないときにまずマニフェストのversionを疑うと、原因に早く行き着きます。