Claude Media
Claude Codeプラグインの環境変数とパスの解決ルール

Claude Codeプラグインの環境変数とパスの解決ルール

プラグインが使う3つの環境変数はコンポーネントごとに展開される場所が違い、パス指定も置き換えと追加で挙動が分かれます。exec形式とshell形式で扱いがどう変わるかまで扱います。

Claude Codeプラグインの環境変数とパスの解決ルール

Claude Codeのプラグインは${CLAUDE_PLUGIN_ROOT}など3つの環境変数でパスを参照します。ただしどのフィールドで展開されるかはコンポーネントごとに違い、hookのcommand定義がexec形式かshell形式かでも扱いが変わります。マニフェストのパス指定も、コンポーネントによって既定ディレクトリを「置き換える」場合と「追加する」場合があり、ここを取り違えるとスキルやhookが読み込まれなくなります。

Claude Code本体の環境変数とは別物です

「Claude Codeの環境変数」で探している場合、設定一覧はClaude Codeの環境変数ガイドにあります。本稿の3変数は、プラグインのマニフェスト・hook・MCP・LSPの定義の中でパスを指すための専用の変数です。シェルでexportして使うものではありません。

「パスをどう書くか」で迷っているなら、答えは次の3行です。

  • プラグイン同梱のファイルは${CLAUDE_PLUGIN_ROOT}で指す
  • 更新で消えてほしくない状態は${CLAUDE_PLUGIN_DATA}に置く
  • 作業中のプロジェクトのファイルは${CLAUDE_PROJECT_DIR}で指す

プラグインが使える3つの環境変数

Claude Codeはパス参照用に3つの変数を用意しています。hookプロセスやMCP・LSPサーバーのサブプロセスには環境変数としても渡されるので、スクリプト側でprocess.env.CLAUDE_PLUGIN_ROOTのように読めます。

変数

プラグインの3つの変数

  • ${CLAUDE_PLUGIN_ROOT}

    インストール先の絶対パスです。同梱スクリプト・バイナリ・設定ファイルの参照に使います。更新のたびに指す先が変わります。前バージョンのディレクトリには.orphaned_atマーカーが書かれ、14日後にバックグラウンドで削除されます。ここに書いた状態は更新で失われます。

  • ${CLAUDE_PLUGIN_DATA}

    更新を跨いで残る永続ディレクトリで、初参照時に作成されます。node_modulesなどの依存関係、生成コード、キャッシュの置き場です。

  • ${CLAUDE_PROJECT_DIR}

    現在のプロジェクトルートです。プロジェクト側のスクリプトや設定ファイルを参照するときに使います。

Bashツール経由でClaudeが実行するコマンドの環境には、この変数は入りません。コマンドに$CLAUDE_PLUGIN_ROOTと書いても空になります。スキル・コマンド・エージェントの内容は、Markdown本文に${...}と書けば、読み込み時にClaude Codeがパスを埋め込みます。

どのフィールドで展開されるか — コンポーネント別の対応表

3つの変数はどこにでも書けばよいわけではなく、コンポーネントの種類ごとに展開されるフィールドが決まっています。

コンポーネント展開されるフィールド
スキル・エージェントの本文展開されるフィールド記述箇所ならどこでも
hook・monitorのcommand展開されるフィールド記述箇所ならどこでも
MCP stdioサーバー展開されるフィールドcommand / args / env
MCP http / sse / wsサーバー展開されるフィールドurl / headers / headersHelper
LSPサーバー展開されるフィールドcommand / args / env / workspaceFolder

対応表にないフィールドに書いた変数は展開されません。「変数が展開されない」ときは、まずフィールド名を表と突き合わせます。環境変数としてプロセスに渡る範囲も一律ではありません。hookには3変数とCLAUDE_PLUGIN_OPTION_<KEY>が渡ります。MCP stdioは、プラグインのリファレンスではCLAUDE_PLUGIN_ROOTとCLAUDE_PLUGIN_DATAの2つです。一方MCPのページには、CLAUDE_PROJECT_DIRもサーバーの環境に設定されるという記述があります。LSPは3変数です。monitorには何も渡りません。

パスの記法ルール — ./必須と.が使えるようになった経緯

マニフェストのパスフィールドはすべてプラグインルートからの相対パスで、./から始める必要があります。例外はskillsの"."と、mcpServersが受け付けるhttps://のバンドルURLです。"."と"./"はどちらもプラグインルートそのものを指します。

ただし"."が使えるようになったのは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形式に分かれ、環境変数の展開のされ方が変わります。パスプレースホルダーを参照するなら、exec形式(argsを設定)が基本です。シェルの機能が必要なときだけshell形式(argsを省略)にします。

くらべる

hookのcommandの2つの形式

argsあり

exec形式

シェルを経由しないので、${CLAUDE_PLUGIN_ROOT}はcommandとargsの各要素にそのまま代入されます。引用符は要りません。アポストロフィ・$・バッククォートもシェルに解釈されずに渡ります。

argsなし

shell形式

文字列全体がシェルに渡ります。macOS/Linuxはsh -c、WindowsはGit Bashで、Git Bashが無ければPowerShellです。そのため"${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 validateで書き間違いを先に拾う

マニフェストとhookの書き間違いは、読み込ませる前にclaude plugin validateでわかります。v2.1.287で、わざと誤らせたマニフェストを検証した結果です。

claude plugin validate ./p

マニフェストは"agents": "."と、skillsに"../outside"・"./missing/"を指定しています。hooksファイルのcommandは${CLAUDE_PLUGIN_ROOT}/scripts/a.shを引用符なしで書いています。

✘ Found 5 errors:
 
  ❯ agents: Invalid string: must start with "./"
  ❯ agents: Invalid string: must end with ".md"
  ❯ skills.1: Invalid string: must start with "./"
  ❯ skills[1]: Path contains ".." which could be a path traversal attempt: ../outside
  ❯ skills[2]: Path not found: ./missing/. The runtime loader will report this as a load failure.
 
⚠ Found 1 warning:
 
  ❯ hooks.PostToolUse: Shell command uses ${CLAUDE_PLUGIN_ROOT} without quotes: ...

agentsでは./で始まり.mdで終わる指定が求められるので、2つのエラーが出ます。skillsを"."だけにしたマニフェストはパスのエラーなしで通りました。

..を含むパスと存在しないパスは、実行時にも読み込まれません。/pluginのErrorsタブには次の2種類が出ます。

  • <component> path escapes plugin directory: <path>
  • <component> path not found: <path>

引用符なしの変数は、パスに空白があると複数の語に割れるための警告です。exec形式にすれば出ません。

永続データディレクトリを使う典型パターン

${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にデータディレクトリを指定して読み込みます。

データディレクトリは、そのプラグインが最後にインストールされていたスコープからアンインストールしたときに自動で削除されます。CLIは既定で削除しますが、--keep-dataを付けると保持できます。claude plugin list --json --data-sizeを使うと、データディレクトリの容量を測れます(v2.1.285以降。値が出るのはマーケットプレイス経由のインストールのみです)。

プラグイン更新中はパスがいつ切り替わるか

セッション中にプラグインを更新しても、hookのcommand・monitor・MCPサーバー・LSPサーバーは更新前のパスを使い続けます。

手順

更新後に新しいパスへ切り替える

  1. 1

    プラグインを更新する

    更新は完了しますが、読み込み済みのコンポーネントは切り替わりません。

  2. 2

    `/reload-plugins`を実行する

    hook・MCPサーバー・LSPサーバーが新しいパスに切り替わります。/reload-pluginsで再起動なしにプラグインを反映するに再読み込みの対象があります。

  3. 3

    monitorはセッションを再起動する

    monitorだけは/reload-pluginsでは切り替わらず、再起動が必要です。無効化しても、起動済みのmonitorはセッション終了まで動き続けます。

MCPサーバーはroots/listで、起動ディレクトリと--add-dirなどで追加したディレクトリの一覧を取得できます。その集合が変わると、Claude Codeがnotifications/roots/list_changedを送ります。v2.1.203より前は起動ディレクトリだけが返り、通知もありませんでした。プロジェクトルートを固定で参照するならCLAUDE_PROJECT_DIRです。プラグインが提供するMCP設定では、${CLAUDE_PROJECT_DIR}が直接置換されます。Claude Code MCP設定ガイドでサーバー追加の基本と合わせて確認できます。

よくあるつまずき

  • Windowsでnpm系の.cmd/.batをexec形式で直接呼んで失敗する: これらは実行ファイルではなく、シェルを経由しないと起動できません。nodeをcommandにして、スクリプトパスをargsに渡す形にすれば全プラットフォームで動きます。

まとめ

変数がうまく動かないときは、フィールド名、置き換えか追加か、exec形式かshell形式か、の順に疑うと切り分けられます。プラグイン全体の流れはClaude Codeプラグイン完全ガイドに、キャッシュの解決はClaude Codeプラグインのキャッシュファイル解決にあります。

この記事を共有:XはてブLinkedIn