Claude Code plugin userConfigの書き方 — 機密値の保存先と置換の制限
settings.jsonを手編集させずにプラグインが値を受け取る仕組みと、機密値の保存先、シェル形式hookで使えない制限をまとめます。
userConfigとは何のためのフィールドか
プラグインが外部APIのエンドポイントやトークンのようなユーザー固有の値を必要とするとき、settings.jsonを手で編集させるのは配布の障害になります。plugin.jsonのuserConfigフィールドを使うと、プラグインを有効化した時点でClaude Codeが値の入力を求めるダイアログを出し、ユーザーは項目名の意味だけ理解すれば設定を終えられます。userConfig以外を含めたplugin.json全フィールドの型はClaude Code plugin.jsonスキーマ完全リファレンスにまとめています。
チーム向けに配布する社内プラグインでは、APIのエンドポイントやトークンがユーザーごと・環境ごとに異なるのが普通です。userConfigを使わないと、インストール手順書に「settings.jsonを開いてこのキーを追記してください」という一文が要ります。userConfigを定義しておけば、この手順そのものが不要になります。プラグイン構造の基本はClaude Codeプラグイン(Plugins)完全ガイドで扱っています。plugin.json自体が初めての場合は、まずそちらで全体像をつかむと読み進めやすくなります。
値が利用者の画面から部品に届くまで
- 1
plugin.jsonに宣言する
userConfigに項目を並べます。キーは英字・数字・アンダースコアで、先頭に数字は置けません。 - 2
有効化時に入力してもらう
Claude Codeが入力ダイアログを出します。機密でない値は
settings.json、機密値は資格情報ストアに保存されます。 - 3
部品から参照する
MCP・LSPの設定やexec形式のhook引数では
${user_config.KEY}、hookプロセスではCLAUDE_PLUGIN_OPTION_<KEY>環境変数で受け取ります。
最小構成を書く
userConfigはキーごとにオブジェクトを並べる形式です。
{
"userConfig": {
"api_endpoint": {
"type": "string",
"title": "API endpoint",
"description": "Your team's API endpoint"
},
"api_token": {
"type": "string",
"title": "API token",
"description": "API authentication token",
"sensitive": true
}
}
}各項目が持てるフィールドは次の9つです。
| フィールド | 必須 | 内容 |
|---|---|---|
type | 必須必須 | 内容string / number / boolean / directory / fileのいずれか |
title | 必須必須 | 内容設定ダイアログに表示されるラベル |
description | 必須必須 | 内容項目の下に表示される説明文 |
sensitive | 必須任意 | 内容trueなら入力をマスクし、値をsettings.jsonではなく資格情報ストアに保存する |
required | 必須任意 | 内容trueなら設定ダイアログが空の値を受け付けない |
default | 必須任意 | 内容未入力時に使われる値(文字列・数値・真偽値・文字列の配列) |
options | 必須任意 | 内容string型の入力を固定の選択肢に絞る(v2.1.271以降) |
multiple | 必須任意 | 内容string型で複数値の配列入力を許可する |
min / max | 必須任意 | 内容number型の範囲制約 |
この表のフィールドの外にキーを書くと、プラグインは読み込まれません。次の節の実機確認で、そのときの出力を確かめています。
型ごとの入力例
stringとsensitive以外の型も組み合わせられます。次の例は、接続先ホスト(string)・リトライ回数(number、範囲制約つき)・詳細ログの有効化(boolean)・ローカルの作業ディレクトリ(directory)を1つのプラグインでまとめて定義したものです。
{
"userConfig": {
"host": {
"type": "string",
"title": "接続先ホスト",
"description": "監視対象サービスのホスト名",
"required": true
},
"retry_count": {
"type": "number",
"title": "リトライ回数",
"description": "接続失敗時の再試行回数",
"default": 3,
"min": 0,
"max": 10
},
"verbose": {
"type": "boolean",
"title": "詳細ログ",
"description": "デバッグ用の詳細ログを出力する",
"default": false
},
"workspace": {
"type": "directory",
"title": "作業ディレクトリ",
"description": "生成物を書き出すローカルディレクトリ"
}
}
}min/maxはnumber型の範囲を決めるフィールドです。directoryとfileは、ユーザーのローカル環境に依存するパスをプラグイン側で決め打ちしないための型です。${CLAUDE_PLUGIN_ROOT}のようなプラグイン内パスとは役割が異なります。フォルダごとまとめて指定したいならdirectory、証明書1本のように単一ファイルへの参照が要るならfile、と使い分けます。
選択肢を固定するoptionsと/configの行
string型の項目にoptionsを付けると、自由入力ではなく固定リストから選ばせられます。defaultには選択肢のどれかを指定します。
{
"userConfig": {
"tone": {
"type": "string",
"title": "Tone",
"description": "Voice for generated replies",
"options": ["neutral", "warm", "formal"],
"default": "neutral"
}
}
}optionsはmultipleでもsensitiveでもないstring型にしか付けられません。各選択肢は1〜64文字の素のラベルです。付けるならdefaultを選択肢の中から決めるか、required: trueにして必ず選ばせる必要があります。v2.1.271より前のClaude Codeを使うユーザーは、optionsを宣言したプラグインを読み込めません。社内配布ではバージョンの下限をそろえてから使う項目です。
有効化済みプラグインの各項目は、/configパネルに1行ずつ並びます。ただしsensitiveの項目とmultipleのリストは並びません。この表示はv2.1.269以降の機能です。有効化ダイアログを閉じたあとに値を変えたいときの入口が/config、という位置関係になります。VS Code拡張では、v2.1.285以降の「Manage plugins」にオプションのフォームがあり、未設定の項目はインストール時に尋ねられ、あとから各行の歯車で変更できます。
宣言の間違いはclaude plugin validateで先に見つかる
plugin.jsonを書いたら、プラグインのディレクトリでclaude plugin validateを実行できます。マニフェストと、同梱のスキル・エージェント・コマンドを検査するコマンドです。v2.1.281以降は、宣言していない${user_config.*}の参照も報告されます。v2.1.287で、defaultを綴り間違えたuserConfigを検査すると、次のとおりに止まります。
$ claude plugin validate .
✘ Found 1 error:
❯ userConfig.retry: Unrecognized key: "defualt"
✘ Validation failed終了コードは1でした。綴りを直すと、残るのはversion・description・authorなど未記入のメタデータへの警告だけで、終了コードは0に変わります。--strictを付けると、その警告も失敗扱いになって終了コードが1になります。CIに組み込むときは--strictの有無で、警告をどこまで許すかを決められます。
optionsの使い方の誤りも、同じ形で報告されます。sensitive: trueの項目にoptionsを付け、defaultもrequiredも無い選択式の項目を置くと、次の2件が出ました。
❯ userConfig.mode.options: options is only for a field of type "string" that is neither multiple nor sensitive
❯ userConfig.tone.default: a field with options needs a default among them, or required: true一方で、シェル形式のhookコマンドに${user_config.api_token}を書いたマニフェストは、同じバージョンのclaude plugin validateを通りました。拒否は有効化後の読み込み時に起きるため、このコマンドだけでは検出できません。次の節の置換の制限は、実際に読み込ませて確かめる必要があります。
値をコンポーネント側で参照する
定義した値は${user_config.KEY}という形式で、MCPサーバーやLSPサーバーの設定、exec形式のhookのargsに埋め込んで参照できます。スキルやエージェントの本文にも埋め込めますが、対象は機密でない値だけです。すべての値はCLAUDE_PLUGIN_OPTION_<KEY>という環境変数(キー名は大文字化)としてもhookプロセスへエクスポートされます。api_tokenなら、シェル形式のhookは$CLAUDE_PLUGIN_OPTION_API_TOKENで読めます。置換構文が使えない箇所では、こちらを読む選択肢があります。
MCPサーバーの設定で使う例
MCPサーバーの設定では、urlやheadersにもこの記法をそのまま使えます。トークンをsensitive: trueで定義しておけば、認証ヘッダーを平文で.mcp.jsonに書かずに済みます。
{
"mcpServers": {
"internal-api": {
"url": "${user_config.api_endpoint}",
"headers": {
"Authorization": "Bearer ${user_config.api_token}"
}
}
}
}動的にヘッダーを作るheadersHelperでは事情が変わります。ヘルパーのコマンドはシェルを通るので、${user_config.*}の埋め込みは拒否されます。しかもヘルパーの環境変数にはCLAUDE_PLUGIN_ROOT・CLAUDE_CODE_MCP_SERVER_NAME・CLAUDE_CODE_MCP_SERVER_URLが入るだけで、設定値は渡りません。値が要るなら、シェルを通らないheadersフィールドへ${user_config.KEY}を移すか、ヘルパーのスクリプトが設定ファイルや、サーバーのenvブロックに置いた環境変数から自分で読みます。MCPサーバー自体の定義方法はClaude Code MCP設定ガイドにまとめてあります。
機密値はスキル本文には渡らない
${user_config.KEY}をスキルやエージェントの本文に埋め込んだとき、実際の値に置き換わるのは非機密の項目だけです。sensitive: trueの項目をスキル本文に書くと、値ではなくプレースホルダーになります。認証情報を扱いたいときは、MCPサーバーの設定かhookのコマンド側で完結させる構成になります。
シェル形式のhookでは使えない
シェルを通るフィールドは${user_config.*}の置換を拒否します。設定値に$(...)・バッククォート・;が含まれていると、シェルがコードとして実行してしまうためです。Claude Codeはコンポーネントを起動せず、エラーで止めます。この検査はコマンドのテンプレートに対して行われるので、値をまだ設定していない段階でもエラーが出ます。v2.1.207より前はこの置換が実際に行われていました。
シェル形式のhookが拒否されたときのメッセージは、次のとおりです(公式のエラー一覧にある文面です)。
Hook from plugin formatter@acme-tools references ${user_config.*} in a shell-form command. The substituted value would be re-parsed by the shell. Use exec form instead — {"command": "<executable>", "args": ["${user_config.KEY}", ...]} — or read $CLAUDE_PLUGIN_OPTION_<KEY> from the hook's environment. Command: ./scripts/notify.sh ${user_config.webhook_url}hookの書き方が分かれ目です。argsを書かなければシェル形式、書けばexec形式として、シェルを介さずに実行されます。
同じトークンを渡すhookの書き方
シェル形式
commandにcurl -H "Authorization: ${user_config.api_token}" ...と1本の文字列で書く形です。置換後の文字列をシェルが解釈し直すため、読み込みの時点で止まります。
exec形式
commandに実行ファイル名だけを置き、argsの各要素に${user_config.KEY}を書く形です。置換された値は引数1つとしてそのまま渡り、シェルは介在しません。
exec形式で書き直すと、次のようになります。v2.1.287のclaude plugin validateは、この形のマニフェストをエラーなしで通しました。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "curl",
"args": [
"-H", "Authorization: Bearer ${user_config.api_token}",
"${user_config.api_endpoint}"
]
}
]
}
]
}
}拒否されるフィールドごとに、値の渡し方が違います。モニターとheadersHelperには、Claude Codeが値を渡す経路がありません。
| 拒否されるフィールド | 値の渡し方 |
|---|---|
| シェル形式のhookコマンド | 値の渡し方exec形式に切り替えてargsで渡す、またはhookの環境変数からCLAUDE_PLUGIN_OPTION_<KEY>を読む |
| モニターのコマンド | 値の渡し方Claude Code経由では渡せない。モニターのプロセスはCLAUDE_PLUGIN_OPTION_<KEY>を受け取らないので、スクリプトが自分で値を取得する |
MCPのheadersHelper | 値の渡し方Claude Code経由では渡せない。headersフィールドへ移すか、ヘルパーが自分で読む |
モニターとheadersHelperが拒否されたときのメッセージも、公式のエラー一覧にあります。
Monitor "deploy-status" from plugin deploy-tools references ${user_config.*} in its command. The substituted value would be passed to a shell. Monitor commands cannot safely reference ${user_config.*}; have the monitor script read the value from a config file or prompt instead.
headersHelper for MCP server 'internal-api' references ${user_config.*}. The substituted value would be passed to a shell; read the value inside the helper script instead (e.g. from an env var set in the server's "env" block).v2.1.207より前のプラグインでこの置換に依存していたものは、上記のいずれかへの書き換えが必要です。
sensitiveな値の保存先
sensitive: trueを付けた値はsettings.jsonには書かれません。macOSならKeychainに保存し、Keychainが書き込みを拒んだときは~/.claude/.credentials.jsonに落ちます。対応するキーチェーンがないプラットフォームでは、最初から~/.claude/.credentials.jsonです。ログイン情報が使う保存先と同じ仕組みです。macOSのフォールバック先とLinuxではファイルのモードが0600、Windowsではユーザープロファイルのアクセス制御を引き継ぎます。
機密でない値は、ユーザーのsettings.jsonのpluginConfigsキーの下にpluginConfigs[<plugin-id>].optionsとして保存されます。<plugin-id>はdeployer@acme-toolsのようなプラグイン名@マーケットプレイス名の形です。Claude Codeがダイアログの入力時に書き込むので、手で編集する必要はありません。settings.jsonの他のキーとの関係やスコープの全体像はClaude Code settings.json完全ガイドで扱っています。
どの設定ファイルのpluginConfigsが読まれるか
pluginConfigsのスコープは、公式の設定リファレンスで「ユーザーまたは管理設定」とされています。Claude Codeで有効化ダイアログが書き込むのは、ユーザー設定の~/.claude/settings.jsonです。組織が配る管理設定からも読まれます(--settingsは後述)。
プロジェクトの.claude/settings.jsonや.claude/settings.local.jsonにあるpluginConfigsのエントリは無視されます。値がhookのコマンドやMCP/LSPサーバーの設定へ置換されるため、cloneしたリポジトリが値を供給できてはいけない、というのが理由です。v2.1.207より前は、プロジェクト設定とローカル設定も読まれていました。
この制限はpluginConfigsに限った話です。どのプラグインを有効にするかを決めるenabledPluginsは、引き続きプロジェクト設定とローカル設定からも読まれます。
社外や第三者が触れるリポジトリでプラグインを使う場面で、この違いが効きます。cloneしたリポジトリの.claude/settings.jsonにpluginConfigsが仕込まれていても、プラグインの動作は変わりません。APIエンドポイントやトークンを、リポジトリ側の設定ファイル経由ですり替える経路が塞がれているためです。
--settingsで渡す設定は、v2.1.207のchangelogで、ユーザー設定・管理設定と並んで読まれると書かれています。設定リファレンスのスコープ欄はユーザーと管理のみです。Agent SDKのsettingSourcesとの関係は説明がないので、使う構成で値が反映されるかを確かめるのが確実です。
channelsのuserConfigとの違い
channelsフィールドを使うプラグインでは、チャンネルごとに独自のuserConfigを持てます。トップレベルのuserConfigと同じ形で、serverで指定したMCPサーバーに紐づけて定義します。保存された値は、そのサーバーのenvの${user_config.KEY}に置換されます。
{
"channels": [
{
"server": "telegram",
"userConfig": {
"bot_token": {
"type": "string",
"title": "Bot token",
"description": "Telegram bot token",
"sensitive": true
},
"owner_id": {
"type": "string",
"title": "Owner ID",
"description": "Your Telegram user ID"
}
}
}
]
}serverフィールドは必須で、プラグインのmcpServersに定義したキーと一致させます。Telegramボットのトークンのように、特定のサーバー1つのためだけに要る値は、このチャンネル側に置くと対応関係が見えやすくなります。
defaultとrequiredは別の役割
defaultは、ユーザーが何も入力しなかったときに使う値です。requiredは、設定ダイアログが空の値を受け付けるかどうかを決めます。同じ項目に両方を付けることもできます。
まとめ
userConfigは、利用者にsettings.jsonを編集させずに値を受け取る仕組みです。配布前にはclaude plugin validate --strictで宣言を検査し、シェル形式の${user_config.*}だけは、実際に読み込ませて確かめる手順が残ります。