UserPromptExpansion(ユーザープロンプトエクスパンション)

フック
UserPromptExpansion
ユーザープロンプトエクスパンション
ユーザーが打ったスラッシュコマンドが、中身のプロンプトに展開されてClaudeに届く直前に走るフック。その展開そのものを止めたり、チームの注意書きを添えたりできる。PreToolUseが拾えない「/skillnameを直接タイプしたルート」で発火するのが最大の役割。

スキルや自作スラッシュコマンドを配っていて、特定コマンドの直接実行を止めたり実行時にチームのルールを差し込んだりしたい人向け

本番公開の/deployのように勝手に走らせたくないコマンドがあるとき、settingsファイルにこのフックを登録して、承認の目印ファイルが無い限り展開を止める。レビュー用スキルにチームのチェックリストをadditionalContextで毎回添えたいときや、どのコマンドが叩かれたか記録したいときにも使う。

スキルや自作のスラッシュコマンドをチームに配ると、必ず「これは勝手に走らせてほしくない」というコマンドが1つ2つ出てきます。本番公開の /deploy がその代表で、誰かが軽い気持ちで打って事故る、というやつですね。UserPromptExpansion は、ユーザーが打ったスラッシュコマンドが「中身のプロンプトに展開されて Claude に届く直前」に割り込んで、その展開そのものを止めたり、チームのルールを差し込んだりできるフックです。

もう少し踏み込むと、このフックの一番の存在意義は PreToolUse の死角を塞ぐことにあります。スキルを PreToolUse で見張っても、ユーザーが /skillname と直接タイプしたルートは素通りしてしまう。その直接ルートで唯一発火するのが UserPromptExpansion です。

噛み砕くと

会社の受付に立っている警備員をイメージすると早いです。スラッシュコマンドという「来客」が建物(Claude)に入る一歩手前で、受付が身分証(コマンドの名前)を見て、通すか・追い返すか・「この人にはこの注意書きも渡しておいて」と一筆添えるかを決める。UserPromptExpansion はちょうどこの受付の役回りです。

大事なのは、止めるのが「来客が建物に入る前」だという点。Claude が何か作業を始めてから止めるのではなく、そもそも届く前にせき止めます。だから止めたときの理由は Claude ではなく、打った本人の画面に出ます。ここが地味に効いてくる。

大事な前提:このフックは「展開されるタイプ」のコマンドだけを見る

UserPromptExpansion が反応するのは、中身のプロンプトに展開されるタイプのスラッシュコマンドです。具体的にはスキル、自作のスラッシュコマンド、それと MCP サーバーが提供するプロンプトの3種類。打った瞬間に画面が切り替わるような組み込み機能が対象に入るかは公式に記載がないので、ここは断言しません。

見分けの目印は expansion_type という入力データの値です。スキルと自作コマンドなら slash_command、MCP サーバーのプロンプト経由なら mcp_prompt が入ります。この2値しか無い、というのが「対象は展開型だけ」という裏付けになります。

「料理ブログチームの /deploy をブロック」を例に、実際の手順を見る

公式が挙げているのとそのままの例でいきます。料理ブログを数人で運営していて、記事を本番サイトに公開する /deploy という自作コマンドを配ってある。ただし承認ファイルが置かれていない限り、誰が打っても止めたい。これを UserPromptExpansion で組みます。

ステップ1: 見張る対象の /deploy が存在している前提を確認する

まず、止めたい /deploy 自体がチームに配られている状態が前提です。マッチャー(matcher)はこのコマンドの名前そのものに一致させるので、実在しないコマンド名を書いても永遠に発火しません。料理ブログチームの場合、公開担当が作った /deploy がスキルか自作コマンドとして既にある、という想定で進めます。

ステップ2: settings ファイルにフックを登録する

プロジェクトの設定ファイル、たとえば .claude/settings.json にフックを書きます。イベント名は UserPromptExpansion、マッチャーには deploy を指定して、/deploy が展開されるときだけ自分のスクリプトが走るようにします。登録できたかは /hooks を打って一覧で確かめられます。

ここで初心者がやりがちな勘違いを1つ。マッチャーを空にすれば安全側、と思って空欄にすると、逆に展開型スラッシュコマンド全部に発火します。ログ目的のつもりが全スキルの起動に割り込む、という事故につながるので、特定コマンドだけ見たいなら名前を必ず書きます。

ステップ3: 承認ファイルの有無をチェックするスクリプトを書く

スクリプト側は、プロジェクト内に承認の目印になるファイル、たとえば .deploy-approved があるかどうかを見ます。無ければ展開を止める、あれば素通しする、という単純な分岐です。止めるときは公式の出力形式に合わせて、次の形の JSON を標準出力に出します。

{
  "decision": "block",
  "reason": "Deploy commands require approval",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptExpansion",
    "additionalContext": "Approval file: .deploy-approved"
  }
}

スクリプト本体のサンプルは公式に無いので「公式の決まった形」とは言いません。ただし、この出力 JSON の形だけは公式の例とぴったり合わせます。bash で書くなら、ファイルの有無を見る test -f、JSON を組み立てる catjq あたり、環境差の出ないコマンドで足ります。

ステップ4: 承認ファイルが無い状態で /deploy を打ってみる

承認ファイルを置かないまま /deploy を打つと、展開される前にせき止められます。画面には reason に書いた「Deploy commands require approval」がそのまま出ます。Claude が「公開はできません」と返事を作るわけではない、というのがポイント。展開前に止まっているので、Claude にはそもそも何も届いていません。

ステップ5: 承認ファイルを置いて再実行する

公開担当が確認を終えて承認ファイル .deploy-approved をプロジェクトに置いてから /deploy を打つと、今度はスクリプトの分岐が「あり」と判定し、decision を返さずに終わります。展開はそのまま通って、いつもどおり公開の処理が動きます。通すときは "approve" のような値を返すのではなく、decision を省くか、JSON を一切出さずに終了コード 0 で終える、というのが決まりです。

ステップ6: フックに渡ってくる入力データの中身を見ておく

スクリプトを書くとき、何が渡ってくるか分かっていると判定が組みやすいです。/deploy production のように打った場合、フックには次のような JSON が入力として渡ります。

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../00893aaf.jsonl",
  "cwd": "/Users/project",
  "permission_mode": "default",
  "hook_event_name": "UserPromptExpansion",
  "expansion_type": "slash_command",
  "command_name": "deploy",
  "command_args": "production",
  "command_source": "plugin",
  "prompt": "/deploy production"
}

共通の項目に加えて、UserPromptExpansion では expansion_typecommand_namecommand_argscommand_source・展開前の prompt 文字列が渡ります。command_args はコマンドの後ろに書き足した文字が入る項目で、この例なら production が入ります。本番だけ止めて検証環境は通す、みたいな細かい分岐もここで組めます。

つまり UserPromptExpansion は何をしてくれるのか

  • やってくれる: ユーザーが直接タイプしたスラッシュコマンドが Claude に届く前に、展開を止める/チームの注意書きを additionalContext で差し込む/どのコマンドが叩かれたかを記録する
  • やってくれない: Claude がツールを呼ぶ瞬間を止めること。それは PreToolUse の役目です。止めた事実を Claude に伝えて謝らせること。止めるのは展開の手前で、理由は本人の画面に出るからです
  • 意味が薄い場面: 画面に展開されないタイプの組み込みコマンドだけを止めたいとき。対象かどうかは公式に記載がなく、当てにできません

使いどころ3シナリオ(具体題材で再現)

シナリオ1: 料理ブログチームで本番公開を承認制にしたいとき

5人で回している料理ブログで、記事を本番サイトへ出す /deploy を配っている。誰かが下書き段階で誤って公開すると、未完成のレシピが検索に乗ってしまう。マッチャーに deploy を指定し、編集長が .deploy-approved を置いた時だけ通すようにすれば、公開の最終ゲートを1つ作れます。止められた人の画面には「Deploy commands require approval」が出るので、何が起きたかも伝わる。

シナリオ2: レビュー用スキルにチームのチェックリストを毎回くっつけたいとき

家計簿アプリを作っているチームで、コードレビュー用のスキル /review を配っているとします。レビューのたびに「テストは通したか」「個人情報のログ出力は無いか」を確認させたい。マッチャーを review にして、decision は返さず additionalContext にチームのチェックリストを入れて返すと、/review が展開されるたびにそのリストが Claude のコンテキストに自動で添えられます。止めるのではなく、味付けして通す使い方ですね。

シナリオ3: どのコマンドが何回叩かれているか記録したいとき

社内に20個近くスキルを配ったものの、実際どれが使われているか分からない、というケース。マッチャーを空にして全展開型コマンドで発火させ、command_name をファイルに書き出すだけのスクリプトにすれば、利用ログが溜まります。ただし空マッチャーは全コマンドに割り込むので、止める判定は一切入れず、必ず decision を返さずに通す作りにするのが安全です。

初心者が踏みやすい落とし穴

  • スクリプトが終了コード 2 で死ぬと、それだけで「展開ブロック」扱いになるjq が入っていない、文法ミスでこける、といった理由で異常終了すると、判定を返していなくても展開が止まります。「なぜか毎回スラッシュコマンドが弾かれる」状態になったら、echo $? でフックの終了コードを真っ先に疑う。
  • PreToolUse でスキルを見張れば直接タイプも拾える、と思い込む/skillname の直接タイプは PreToolUse を素通りします。その穴を塞ぐのがこのフックなので、両方を使い分ける前提で設計する。
  • UserPromptSubmit と同じものだと勘違いする。Submit は打った文章すべてで発火し、Expansion はスラッシュコマンドが展開されるときだけ。役割が違います。ただし、終了コード 0 のとき素の標準出力が Claude の見えるコンテキストに入る、という特例はこの2つ(と SessionStart)に共通します。
  • block の reason を書き忘れる。理由を空にすると、打った本人には「理由なしで弾かれた」ように見えます。何で止めたかは必ず reason に一言入れる。
  • スキルだけのつもりが MCP プロンプトで誤発動するexpansion_type には mcp_prompt も来ます。スキル前提で書いた条件が MCP 経由で意図せず動くことがあるので、必要なら expansion_type で絞る。
  • 通すときに "approve" のような値を返そうとする。許可の専用値はありません。decision を省くか、JSON を出さずに終了コード 0 で抜けるのが正解。block 以外の値は返さない。
  • ツール実行そのものを止めたくてこのフックを選ぶ。ここはコマンド展開の入口を見る場所で、Claude がツールを呼ぶ瞬間を止めたいなら PreToolUse の出番です。

書き方

settingsファイルにイベント名 UserPromptExpansion とマッチャー(command_nameに一致、空なら展開型コマンド全部)を登録し、スクリプトから decision:"block" 等のJSONを返す

やってみるとこうなる

入力

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../00893aaf.jsonl",
  "cwd": "/Users/project",
  "permission_mode": "default",
  "hook_event_name": "UserPromptExpansion",
  "expansion_type": "slash_command",
  "command_name": "deploy",
  "command_args": "production",
  "command_source": "plugin",
  "prompt": "/deploy production"
}

出力例

{
  "decision": "block",
  "reason": "Deploy commands require approval",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptExpansion",
    "additionalContext": "Approval file: .deploy-approved"
  }
}

このページに出てきた言葉

フック
Claude Codeの決まった場面で自動で自分のスクリプトを差し込める仕組み
展開
スラッシュコマンドの中身が、Claudeが読む本文のプロンプトに変換されること。<code>/deploy</code>が「本番に公開して」という文章に化けるイメージ
expansion_type
このフックに渡る入力データの項目。スキル・自作コマンドなら<code>slash_command</code>、MCPサーバーのプロンプト経由なら<code>mcp_prompt</code>が入る
command_args
コマンドの後ろに書き足した文字。<code>/deploy production</code>なら<code>production</code>がここに入る
decision
フックの判定結果。<code>"block"</code>を返すと展開が止まる。通したいときはこの項目を書かない(省く)
additionalContext
展開後のプロンプトに添えてClaudeのコンテキストに足す文字列。チームのチェックリストを差し込むときなどに使う
マッチャー(matcher)
フックをどのコマンドに発火させるかの条件。<code>command_name</code>で一致を見る。空にすると展開型コマンド全部で発火する

関連項目

公式ドキュメント

https://code.claude.com/docs/en/hooks

-

← 戻る