Claude Codeプラグインのコマンドソースでマーケットプレイスを動的生成する
プラグインのコマンドソースはローカルツールが出力するディレクトリをそのままインストールする仕組みです。v2.1.287で動かして確かめた失敗パターンとcopy/linkの差を載せます。
Claude Codeプラグインのコマンドソースでマーケットプレイスを動的生成する
marketplace.jsonのプラグインエントリにはcommandというsource種別があり、ローカルにインストール済みのツールが吐き出すディレクトリを、そのままプラグインとして取り込めます。IDEが選択中のツールチェーンに応じてプラグイン内容を生成するような、固定の配布物を持たないケース向けの仕組みです。対応はClaude Code v2.1.229以降です。
この記事の出力は、v2.1.287で空のHOMEと設定ディレクトリを使い、一時ディレクトリに置いたマーケットプレイスで実行したものです。成功・失敗それぞれの実際のメッセージと、copy/linkモードでバージョンがどう決まるかを載せています。
コマンドソースは何を指定するのか
github / url / git-subdir / npm / ローカルパスといった通常のsource種別は、いずれも「どこかに固定された配布物」を指します。Claude Codeはそこからファイル一式を取得し、キャッシュへコピーするだけです。commandはこの前提が違います。指定するのは配布物の場所ではなく、配布物をその場で作り出すコマンドです。
{
"name": "formatter",
"source": {
"source": "command",
"command": "my-tool claude-plugin-path",
"timeout": 120
}
}sourceオブジェクトが受け付けるフィールドは3つです。
| フィールド | 型 | 説明 |
|---|---|---|
command | 型string | 説明必須。印字可能なASCII文字のみ、500文字以内、4スペース以上の連続なし |
timeout | 型number | 説明省略可。1〜600の整数秒(既定60秒) |
mode | 型string | 説明省略可。"copy"(既定)または"link" |
文字数と空白の制約は、利用者が承認前にコマンド全文を読めるようにするためです。長すぎるコマンドは、ラッパースクリプトに包んでスクリプト名だけを書く形に直します。
コマンドはプラットフォームのシェル(macOS/Linuxはsh、Windowsはcmd.exe)で、ユーザーのホームディレクトリをカレントディレクトリとして実行されます。標準出力にはプラグインの中身が入った絶対パスをちょうど1行だけ出し、終了コード0で終わります。相対パスを渡すとホームが基準になるので、絶対パスかPATH上のコマンドを使います。出力パスは実行のたびに変わってかまいません。
各source種別の性格を並べると、コマンドソースの立ち位置がはっきりします。
| source種別 | 中身の決まり方 | version未指定時のバージョン |
|---|---|---|
github / url / git-subdir | 中身の決まり方リポジトリの特定コミット | version未指定時のバージョンソースのコミットSHA(12文字) |
npm | 中身の決まり方パッケージの特定バージョン | version未指定時のバージョンunknown |
archive | 中身の決まり方zipファイルの内容 | version未指定時のバージョンSHA-256のダイジェスト(12文字) |
command | 中身の決まり方コマンド実行のたびに再生成 | version未指定時のバージョンcopyは出力のハッシュ、linkは後述のとおり |
command以外では、plugin.jsonかマーケットプレイスエントリのversionが優先され、表の値はどちらも無いときだけ使われます。commandは常に出力から計算し、エントリのversionは無視されます。マーケットプレイス全体の仕組みはClaude Codeプラグイン完全ガイドにあります。
手元で動かすと何が起きるか
マーケットプレイスに、成功するエントリと失敗するエントリを並べて試しました。マーケットプレイスの追加はclaude plugin marketplace add <path>の1行で通り、この時点ではコマンドの内容は表示されません。ここまでは何も実行されず、コマンドが動くのは個別のインストールからです。
{
"name": "lab-mk",
"owner": { "name": "lab" },
"plugins": [
{ "name": "gen-ok", "source": { "source": "command", "command": "echo /tmp/lab/out" } }
]
}まずターミナル以外(標準入力が端末でない状態)でclaude plugin install gen-ok@lab-mkを実行すると、コマンドは表示されるだけで実行されません。
"gen-ok" is installed by running a command from marketplace "lab-mk" on this machine:
echo /tmp/lab/out
(mode "copy": its output directory is copied into the plugin cache)
Not an interactive terminal, so the command was only displayed, not accepted.--jsonを付けると、同じ拒否がfailureCode: "command_source_refused"の1行JSONで返ります。shownCommandには、コマンドのsha256とマーケットプレイスのcatalogRevisionが入ります。--accept-command <sha256>(v2.1.271以降)はこの値を渡して承認する仕組みで、--accept-commandの使い方に手順があります。
コマンドソースが入るまでの関門
- 1
承認
表示されたコマンドを利用者が受け入れます。非対話の環境では
--yesか--accept-commandが必要です。 - 2
実行
コマンドが
timeout秒以内に、絶対パス1行だけを出力して終了コード0で終わる必要があります。 - 3
中身の検査
出力先にプラグインの中身があり、セッションの起動ディレクトリやその親ではないことが確認されます。
- 4
キャッシュへの取り込み
copyなら複製、linkならリンクの作成です。ここで初めてバージョンが決まります。
Claude Codeのセッション内では--yesが効かない
claude plugin installを、別のClaude Codeセッションの中のシェルから--yes付きで叩いても承認されませんでした。
-y/--yes is ignored inside a Claude Code session: run this in your own terminal to accept the command shown above.承認は人間が自分の端末で行う設計です。CIのセットアップスクリプトや、エージェントにインストールまで任せる構成では、この手順が詰まりどころになります。Claude Codeの環境変数を外した通常のシェルでは、同じ--yesでインストールが通りました。
失敗の文言はこう出る
次は--yes付きで実行したときの、失敗側の実際のメッセージです(パスは<LAB>に置き換えています)。
| 出力したもの | v2.1.287のメッセージ |
|---|---|
| プラグインの中身が無い空ディレクトリ | v2.1.287のメッセージprinted <LAB>/empty, but that directory has no plugin content (...) Nothing was installed. |
| 2行を出力 | v2.1.287のメッセージprinted 2 lines; it must print exactly one absolute path. |
| 起動ディレクトリ(中身あり) | v2.1.287のメッセージprinted the working directory or one of its parents (<LAB>/run); refusing to use it as a plugin. |
timeout超過(1秒に設定してsleep 5) | v2.1.287のメッセージdid not finish within 1s and was stopped. |
起動ディレクトリの拒否は、そのディレクトリにプラグインの中身がある場合に出ます。中身が空なら、先に「プラグインの中身が無い」エラーで止まります。「実在するディレクトリを渡しているのに失敗する」ように見えたときは、この順序で原因を絞れます。
「中身が無い」エラーが期待するトップレベルの要素は、公式ページの例より多く並びます。公式のmarketplace-referenceは例として.claude-plugin/とskills/・commands/・agents/・hooks/を挙げています。v2.1.287のメッセージは、次の要素まで挙げています。
.claude-plugin/commands/skills/agents/hooks/themes/output-styles/monitors/workflows/SKILL.md.mcp.json.lsp.json
Windowsでは、出力パスがUNCパス(\\server\share形式)でも拒否されます。copyモードには、256MiBまたは2万エントリを超えるとコピーできないという上限もあります。
copyモードとlinkモードの違い
mode の選び方
copy(既定)
ディレクトリをキャッシュへ複製し、複製したファイルのハッシュからバージョンを出します。Windowsでも使えます。
link
トップレベルの各エントリへのリンクだけをキャッシュに作ります。中身のハッシュも容量の上限もありません。Windowsでは使えず、"mode": "copy"を明示します。
バージョンの出方は、実際にcopyとlinkで差がありました。copyでgen-okを入れると、キャッシュには0.0.1-d452d4bba325というディレクトリができました。plugin.jsonのversion(0.0.1)にハッシュが付いた形です。SKILL.mdに1行足してclaude plugin update gen-ok@lab-mkを実行すると、0.0.1-eea0881501beに更新され、古い側も残りました。
linkで同じことをすると結果が逆になります。gen-linkのSKILL.mdに1行足して更新しても、返ったのは次の表示でした。
✔ gen-link is already at the latest version (a6669aca4453).linkのバージョンは、出力ディレクトリの実パスとトップレベルのエントリ一覧から決まります。ファイルの中身を書き換えただけでは変わりません。linkで更新を反映したいときは、コマンドが別のパスを出力するようにします。
linkで作られたキャッシュは、実体のコピーではなく.claude-plugin-linkというマーカーと、出力元のskillsディレクトリへのシンボリックリンクでした。出力元を消すと次回の起動で読み込めなくなるのは、この構造のためです。
linkモードには、ほかにも制約があります。
- トップレベルのエントリが出力ディレクトリの外を指すシンボリックリンクだと、インストールは失敗する
- Node.jsパッケージの依存関係インストールが省かれるため、出力ディレクトリに
node_modulesを入れておく - 出力ディレクトリの中でセッションを始めると、そのプラグインは読み込まれない
コマンドが再実行される場面
再実行の3つのきっかけ
インストール・更新のたび
明示的な
installとupdateは毎回コマンドを実行します。セッション開始の直後
有効な各コマンドソースプラグインについて、バックグラウンドで1回走ります。マーケットプレイスの自動更新設定や
DISABLE_AUTOUPDATERとは無関係です。キャッシュにそのバージョンが無いとき
起動時または
/reload-pluginsで、有効なプラグインのインストール済みバージョンがキャッシュに無いと再実行されます。
出力のハッシュが変わっていれば新バージョンとして入り、実行中のセッションにも反映されて通知が出ます。反映するとプロンプトキャッシュが無効になる場合は、代わりに/reload-pluginsの実行を促されます。CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICを設定すると、2つのバックグラウンド実行だけがスキップされます。明示的なインストールと更新は、この変数があっても実行されます。
承認の効き方と組織側の制御
承認したコマンドは、そのインストールに記録されます。配布側がcommandかmodeを変えても、既存の利用者には自動で追随しません。
- バックグラウンドの再実行が止まり、ツールの新しい出力が届かなくなる
/pluginのErrorsタブに、新しいコマンドと実行すべきclaude plugin updateが出る- 利用者が端末でそのコマンドを実行し、新しい内容を承認して初めて再開する
コマンドソースのプラグインは、依存関係としても自動導入されません。他のプラグインがそれを依存に挙げている場合、利用者が先に単体でインストールしておく必要があります。この制限は、/reload-pluginsやマーケットプレイスの自動更新にも当てはまります。依存元のclaude plugin installの再実行と、claude plugin marketplace addも同じです。
組織側は、managed settingsのdisableCommandPluginSourcesで全体を止められます。trueにすると、コマンドを一切実行せず、インストール済みのコマンドソースも読み込みません。allowManagedHooksOnlyを有効にした組織では、falseを明示しない限り既定でブロックされます。マーケットプレイスのheadersHelperコマンドも同じ設定で止まります(v2.1.238以降)。管理設定の全体像はClaude Code組織管理ガイドにあります。
向く場面・向かない場面
| 用途 | 向くか | 理由 |
|---|---|---|
| IDEが選択中のツールチェーンに応じてプラグインを生成する | 向くか向く | 理由ツールの状態が変わるたびに、再インストールなしで反映できる |
| SDKのビルド成果物をそのままプラグインにする | 向くか向く(link) | 理由大きな出力をコピーせずにリンクだけで済む |
| 一般公開するOSSプラグインの配布 | 向くか向かない | 理由利用者ごとにツールの実行環境が要るため、githubソースで足りる |
| 他プラグインからの依存として組み込む | 向くか向かない | 理由依存としては自動導入されないため |
配布物を固定できるなら、通常のsource種別のほうが利用者の手間は小さく済みます。
まとめ
コマンドソースを選ぶかどうかは、出力を利用者の端末で毎回生成する必要があるかで決まります。生成が要るなら、更新を中身で検知したいときはcopy、巨大な出力を置いたままにしたいときはlinkで、承認は利用者の端末で人が行う前提で配布手順を組みます。