Claude Codeプラグインの環境変数とパスの解決ルール
プラグインが使う3つの環境変数はコンポーネントごとに展開される場所が違い、パス指定も置き換えと追加で挙動が分かれます。exec形式とshell形式で扱いがどう変わるかまで扱います。
Claude Codeプラグインの環境変数とパスの解決ルール
Claude Codeのプラグインは${CLAUDE_PLUGIN_ROOT}など3つの環境変数でパスを参照します。ただしどのフィールドで展開されるかはコンポーネントごとに違い、hookのcommand定義がexec形式かshell形式かでも扱いが変わります。マニフェストのパス指定も、コンポーネントによって既定ディレクトリを「置き換える」場合と「追加する」場合があり、ここを取り違えるとスキルやhookが読み込まれなくなります。
プラグインが使える3つの環境変数
Claude Codeはパス参照用に3つの変数を用意しています。いずれもhookプロセスやMCP・LSPサーバーのサブプロセスに環境変数としてエクスポートされるため、スクリプト側ではprocess.env.CLAUDE_PLUGIN_ROOTのように直接読み取ることもできます。
| 変数 | 解決先 | 主な用途 |
|---|---|---|
${CLAUDE_PLUGIN_ROOT} | 解決先プラグインのインストール先ディレクトリの絶対パス | 主な用途同梱スクリプト・バイナリ・設定ファイルの参照 |
${CLAUDE_PLUGIN_DATA} | 解決先更新を跨いで残る永続ディレクトリ(初参照時に作成) | 主な用途node_modulesなど依存関係のインストール先、生成コード、キャッシュ |
${CLAUDE_PROJECT_DIR} | 解決先現在のプロジェクトルート | 主な用途プロジェクト側のスクリプト・設定ファイルの参照 |
${CLAUDE_PLUGIN_ROOT}はプラグインが更新されるたびに指す先が変わります。前バージョンのディレクトリは猶予期間だけディスクに残りますが、あくまで一時的な扱いです。そこに書き込んだ状態は更新のたびに失われます。永続させたい情報は${CLAUDE_PLUGIN_DATA}に置きます。
どのフィールドで展開されるか — コンポーネント別の対応表
3つの変数はどこにでも書けばよいわけではなく、コンポーネントの種類ごとに展開されるフィールドが決まっています。
| コンポーネント | 展開されるフィールド |
|---|---|
| スキル・エージェントの本文 | 展開されるフィールド記述箇所ならどこでも |
| hook・monitorのcommand | 展開されるフィールド記述箇所ならどこでも |
MCP stdioサーバー | 展開されるフィールドcommand / args / env |
MCP http / sse / wsサーバー | 展開されるフィールドurl / headers / headersHelper |
| LSPサーバー | 展開されるフィールドcommand / args / env / workspaceFolder |
たとえばMCPのhttpサーバーで${CLAUDE_PLUGIN_ROOT}をurlに書けば展開されますが、対応表にないフィールドに書いても展開の対象にはなりません。設定を書いてから「変数が展開されない」と感じたら、まずこの対応表でフィールドを確認するのが早道です。
パスの記法ルール — ./必須と.が使えるようになった経緯
マニフェストのパスフィールドはすべてプラグインルートからの相対パスで、./から始める必要があります。例外はskillsフィールドだけで、プラグインルート自体を指す"."も受け付けます。"."と"./"はどちらもプラグインルートそのものを指します。
ただし"."が使えるようになったのはv2.1.221以降です。それより前のバージョンでは"."はマニフェストの検証に失敗してプラグインが読み込まれませんでした。古いバージョンとの互換性を考えるなら"./"を使うほうが安全です。パスは配列でも複数指定でき、カスタムパスから読み込んだコンポーネントの命名規則・名前空間の扱いは既定ディレクトリのものと変わりません。
スキルパスはSKILL.mdを直接含むディレクトリを指すこともできます。"skills": ["."]と書けばプラグインルート自体を1つのスキルとして扱えます。この場合、呼び出し名はSKILL.mdのfrontmatterにあるnameから取られるため、インストール先のディレクトリ名がバージョン文字列に変わっても呼び出し名は安定します。nameが未設定なら、ディレクトリのベース名にフォールバックします。なお、プラグインルート直下にSKILL.mdがあり、skills/サブディレクトリもskillsキーも無い構成であれば、この設定を書かなくても自動的に単一スキルのプラグインとして読み込まれます。
デフォルトディレクトリを「置き換える」か「追加する」か
マニフェストで独自パスを指定したとき、既定ディレクトリのスキャンが止まるか続くかはフィールドによって決まっています。
| 挙動 | 対象フィールド |
|---|---|
| 既定ディレクトリを置き換える | 対象フィールドcommands / agents / workflows / outputStyles / experimental.themes / experimental.monitors |
| 既定ディレクトリに追加する | 対象フィールドskills(マーケットプレイスのsourceがルート直下を指すエントリでサブディレクトリを明示指定した場合を除く) |
| 独自のマージルールを持つ | 対象フィールドhooks・MCPサーバー・LSPサーバー |
commandsをマニフェストで指定すると、既定のcommands/ディレクトリはスキャンされなくなります。既定を残したまま追加したいなら、"commands": ["./commands/", "./extras/"]のように明示的に列挙する必要があります。一方skillsは常に既定のskills/ディレクトリがスキャンされ、マニフェストで指定したディレクトリはそこに追加される形です。
既定フォルダとマニフェストのキーが両方存在する場合、claude plugin listや/pluginの詳細画面には無視されたフォルダについての警告が出ます。それでもプラグイン自体はマニフェストのパスで正常に読み込まれます。マニフェストのキーが既定フォルダの中を指している場合("commands": ["./commands/deploy.md"]など)は、パスがフォルダを明示的に名指ししているため警告は出ません。
exec形式とshell形式で環境変数の扱いが変わる
hookのcommand定義はargsの有無でexec形式とshell形式に分かれ、環境変数の展開のされ方が変わります。パスプレースホルダーを参照するなら、引用符なしで1引数として渡されるexec形式(argsを設定)を使うのが基本です。パイプや&&などシェルの機能が必要なときだけshell形式(argsを省略)にします。
exec形式ではシェルを経由しないため、${CLAUDE_PLUGIN_ROOT}のようなプレースホルダーはcommandとargsの各要素にそのまま文字列として代入されます。アポストロフィや$、バッククォートのような特殊文字もシェルに解釈されずそのまま渡ります。shell形式では文字列全体がシェル(macOS/Linuxはsh -c、Windowsは既定でGit Bash)に渡されるため、"${CLAUDE_PLUGIN_ROOT}"/scripts/process.shのようにダブルクォートで囲む必要があります。
プラグインのhookはさらに${user_config.*}の値も展開しますが、これはexec形式に限られます。shell形式のhookが${user_config.*}を参照するとエラーになり実行されません。shell形式からユーザー設定値を使いたい場合は、$CLAUDE_PLUGIN_OPTION_<KEY>環境変数(webhook_urlオプションなら$CLAUDE_PLUGIN_OPTION_WEBHOOK_URL)を読むか、argsを設定してexec形式に切り替えます。なお、この${user_config.*}のshell形式での非対応はv2.1.207以降の挙動で、それより前のバージョンではshell形式でも展開されていました。hookが発火するイベントとsettings.jsonの書き方はHooks完全ガイドにまとめています。
monitorのcommandはこの点でさらに制限があります。monitorのcommandは常にシェルを経由して動くため、${user_config.*}を参照するとClaude Codeがエラーとして拒否し、値を代入しません。monitorスクリプトからユーザー設定値を使いたいときは、スクリプト自身が持つ設定ファイルから読み込む形にする必要があります。monitorプロセスにはCLAUDE_PLUGIN_OPTION_<KEY>環境変数も渡されません。
永続データディレクトリを使う典型パターン
${CLAUDE_PLUGIN_DATA}は~/.claude/plugins/data/{id}/に解決されます。{id}はプラグイン識別子のうちa-z・A-Z・0-9・_・-以外の文字を-に置き換えたものです。formatter@my-marketplaceとしてインストールされたプラグインなら、ディレクトリは~/.claude/plugins/data/formatter-my-marketplace/になります。
自動インストールの対象にならないパッケージ — Python依存関係や、Yarn・pnpmでロックされた依存関係 — の置き場所に使います。初回だけインストールすれば、セッションやプラグインの更新を跨いで再利用できます。データディレクトリは特定のプラグインバージョンより長生きするため、ディレクトリの存在チェックだけでは更新に伴う依存関係マニフェストの変更を検知できません。推奨パターンは、同梱のpackage.jsonとデータディレクトリに保存したコピーを比較し、差分があれば再インストールする形です。
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "diff -q \"${CLAUDE_PLUGIN_ROOT}/package.json\" \"${CLAUDE_PLUGIN_DATA}/package.json\" >/dev/null 2>&1 || (cd \"${CLAUDE_PLUGIN_DATA}\" && cp \"${CLAUDE_PLUGIN_ROOT}/package.json\" . && npm install) || rm -f \"${CLAUDE_PLUGIN_DATA}/package.json\""
}
]
}
]
}
}diffはコピーが存在しないか、同梱版と内容が異なる場合に非ゼロで終了するので、初回実行と依存関係が変わった更新の両方をこの1行でカバーできます。npm installが失敗したときは末尾のrmがコピーしたマニフェストを削除し、次回セッションで再試行させます。永続化したnode_modulesを使うスクリプト側は、NODE_PATHにデータディレクトリを指定して読み込みます。
データディレクトリは、そのプラグインが最後にインストールされていたスコープからアンインストールしたときに自動で削除されます。/pluginのインターフェースはディレクトリサイズを表示し、削除前に確認を求めます。CLIは既定で削除しますが、--keep-dataを付けると保持できます。
プラグイン更新中はパスがいつ切り替わるか
プラグインをセッション中に更新すると、hookのcommand・monitor・MCPサーバー・LSPサーバーはいずれもすぐには新しいパスに切り替わらず、更新前のバージョンのパスを使い続けます。hook・MCPサーバー・LSPサーバーを新しいパスに切り替えるには/reload-pluginsを実行します。/reload-pluginsで再起動なしにプラグインを反映するではこのコマンドが再読み込みする対象を詳しく扱っています。monitorだけは/reload-pluginsでは切り替わらず、セッションの再起動が必要です。
MCPサーバーはroots/listリクエストを呼び出すことで、実行時にセッションの作業ディレクトリを読み取ることもできます。作業ディレクトリが変わったときにClaude Codeがサーバーへ通知するかどうかは、サーバーの接続方式によって挙動が異なります。Claude Code MCP設定ガイドでサーバー追加の基本と合わせて確認できます。
よくあるつまずき
- パスの先頭に
./を付け忘れる:skills以外のフィールドで"."や相対パスの断片だけを書くとマニフェストの検証に失敗します。./から始める書式を徹底します。 - shell形式のhookから
${user_config.*}を使おうとしてエラーになる: shell形式では展開されず、そのままエラーになります。$CLAUDE_PLUGIN_OPTION_<KEY>を読むか、argsを設定してexec形式に切り替えます。 - Windowsでnpm系の
.cmd/.batをexec形式で直接呼んで失敗する: これらは実行ファイルではなく、シェルを経由しないと起動できません。nodeをcommandにして、スクリプトパスをargsに渡す形にすれば全プラットフォームで動きます。 ${CLAUDE_PLUGIN_ROOT}直下に状態を書き込んで更新後に消える: このディレクトリは更新のたびに変わり、旧ディレクトリは一時的にしか残りません。永続させたい状態は${CLAUDE_PLUGIN_DATA}に書きます。
よくある質問
skillsフィールドだけ既定ディレクトリに「追加」なのはなぜですか
複数のスキルを同梱するプラグインで、既定のskills/ディレクトリとカスタムディレクトリを両方使えるようにするためです。他のフィールド(commandsなど)は指定した時点で既定を置き換える設計なので、両方使いたい場合は既定パスを明示的に配列へ加える必要があります。
古いバージョンとの互換性を考えると.と./のどちらを使うべきですか
./のほうが安全です。"."はv2.1.221以降でしかマニフェスト検証を通らず、それより前のバージョンでは読み込みエラーになります。配布先のバージョンを問わないなら./を使います。
プラグインを無効化した直後にmonitorはすぐ止まりますか
止まりません。セッション中にプラグインを無効化しても、既に起動しているmonitorは停止されず、セッションが終了するまで動き続けます。
exec形式でも${CLAUDE_PLUGIN_ROOT}のようなパス変数は使えますか
使えます。exec形式・shell形式のどちらでも同じパスプレースホルダーに対応しており、展開された値はCLAUDE_PROJECT_DIR・CLAUDE_PLUGIN_ROOT・CLAUDE_PLUGIN_DATAという環境変数としてもプロセスにエクスポートされます。両形式で違うのは${user_config.*}の扱いだけです。
更新直後にhookが古いバージョンのスクリプトを呼んでしまうのは不具合ですか
不具合ではなく仕様です。プラグインが更新されてもセッション中のhook・monitor・MCPサーバー・LSPサーバーは更新前のパスを使い続けます。新しいパスに切り替えるには/reload-pluginsを実行するか(monitorはセッション再起動が必要)、次のセッション開始を待ちます。
まとめ
プラグインの環境変数は3つに絞られますが、展開される場所はコンポーネントごとに固定されているため、対応表を見ながら設定する必要があります。パス指定はskillsだけが既定に追加され、それ以外は指定した時点で既定を置き換える点も見落としやすいところです。hookのexec形式とshell形式では引用符の要不要と${user_config.*}の扱いが変わり、永続化したい状態は${CLAUDE_PLUGIN_ROOT}ではなく${CLAUDE_PLUGIN_DATA}に置きます。プラグインの基本構造や配布の流れはClaude Codeプラグイン完全ガイドに、キャッシュファイルの解決やnpm依存の自動インストールについてはClaude Codeプラグインのキャッシュファイル解決にまとめています。