UserPromptSubmit(ユーザープロンプトサブミット)

フック
UserPromptSubmit
ユーザープロンプトサブミット
ユーザーがプロンプトを送信した直後・Claude が処理を始める前に発火するフック。フックの中で書いた標準出力が「ユーザーのプロンプトに連結された追加情報」として Claude に渡るので、毎回タイプする前置き(「※日本語で」「※ドラフト一覧を見て」等)を自動で添えられる。さらに exit 2 で「危ない単語が入ってたら遮断」もできる玄関ガード兼アシスタント機能

Claude Codeに毎回同じ前置きを書いて投げてる(「※日本語で」「※コードはTypeScriptで」等)のに飽きてきた人向け

Claude Code に同じ前置きや状況メモ(プロジェクトのフォルダ構成・現在のブランチ・ドラフト一覧など)を毎回タイプして渡している場面で、その前置きを settings.json 側に常駐させて自動添付したいときに使う。または社内コード名・パスワードのような送信NGワードが含まれていたらプロンプト自体を Claude に届かせず差し止めたいときに使う

Claude Code に毎回「※日本語で」「※TypeScriptで」「※既存のドラフト一覧も見て」みたいな前置きを書いてから本題を投げてる人向けの仕組みです。UserPromptSubmit は、私が Enter を押した瞬間に裏で短いコマンドを走らせて、その結果を「私のプロンプトに連結された追加情報」として Claude に渡してくれます。前置きを毎回タイプする作業がそのまま消えます。

もう一つの顔があって、危ない言葉を含むプロンプトをそもそも Claude に届かせず差し止める「玄関ガード」にもなります。社内コード名が混じったら遮断、みたいな使い方ですね。

噛み砕くと

レストランの注文伝票を厨房に渡す前に、ホール係が「この客、いつも辛さ控えめね」「アレルギー卵だから注意」と一言メモを足してから出す感じです。

客(私)は本題だけ伝票に書く。ホール係(UserPromptSubmit)が常連メモを足す。厨房(Claude)はメモ込みで読む。客は毎回アレルギーを口頭で伝えなくていい。

地味だけど効きます。

ちなみにホール係は「この客 NG」と判断したら厨房に伝票を回さず突き返すこともできます。これが遮断機能です。

画面に何が出るのか

言葉だけだと掴みづらいので、私の手元のターミナルでの動きを再現します。料理ブログ cooking-blog のプロジェクトで、UserPromptSubmit に「下書きフォルダの一覧を出すコマンド」を仕込んだ状態で、私がプロンプトを送った瞬間です。

> napolitan の続きを書いて

⎿  [UserPromptSubmit hook] running...
⎿  [現在のドラフト一覧]
   bibimbap.md
   napolitan.md
   ramen.md

I'll continue writing napolitan.md. Let me first check
the current content of that draft...

Claude が応答を始める前に、ドラフト一覧がふわっと挿入されてるのが見えます。

このとき Claude が実際に受け取っているのは、私が打った「napolitan の続きを書いて」だけではありません。フックの出力が連結された「napolitan の続きを書いて + [現在のドラフト一覧] bibimbap.md, napolitan.md, ramen.md」相当のかたまりです。

だから「napolitan.md は実在するんだな、書きかけがあるんだな」を Claude が前提にして応答を始められます。

料理ブログ「cooking-blog」で実際に動かす4ステップ

順を追って動かしてみます。私の手順を上から実行すれば同じ画面が出るはずです。

ステップ1: settings.json にフックを登録する

プロジェクトのルートに .claude/settings.json がない場合は作ります。中身はこれだけ。

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "type": "command",
        "command": "cd \"$CLAUDE_PROJECT_DIR\" && echo \"[現在のドラフト一覧]\" && ls drafts/"
      }
    ]
  }
}

「ユーザーがプロンプトを送ったら、プロジェクトのルートに移動して、ドラフト一覧を画面に出せ」と書いてるだけです。

ステップ2: drafts フォルダを用意する

料理ブログ側で、下書きを置くフォルダと中身を作っておきます。

$ mkdir -p ~/projects/cooking-blog/drafts
$ touch ~/projects/cooking-blog/drafts/bibimbap.md
$ touch ~/projects/cooking-blog/drafts/napolitan.md
$ touch ~/projects/cooking-blog/drafts/ramen.md

これで cooking-blog 配下に3つの空ファイルが並んだ状態になりました。

ステップ3: Claude Code を起動してプロンプトを送る

$ cd ~/projects/cooking-blog
$ claude
> napolitan の続きを書いて

Enter を押すと、フックが走って ls drafts/ の結果が連結されます。さっきの「画面に何が出るのか」セクションで見た出力が、そのまま再現されます。

ステップ4: フックが動かない時の確認

もし上記をやってもドラフト一覧が出てこない場合、よくあるのは3つです。

1つ目は .claude/settings.json の置き場所がプロジェクトルートじゃないケース。Claude Code はプロジェクトルートの .claude/ を見るので、サブフォルダに置いても拾ってくれません。

2つ目は JSON の書式エラー。最後のカンマが余ってる、ダブルクォートが片方しかない、みたいな単純な打ち間違いで丸ごと無視されます。cat .claude/settings.json | python3 -m json.tool で構文チェックすると一発です。

3つ目は --bare モードで起動してしまっているケース。claude --bare はフックを全部スキップする起動方法なので、そもそも UserPromptSubmit は走りません。普通の claude で起動し直します。

仕組み:フックに渡ってくるもの・返せるもの

もう一段踏み込んで、フックの中で何が起きてるか整理します。

UserPromptSubmit が発火すると、Claude Code はフックスクリプトの標準入力に下記のような JSON を流し込みます。

{
  "session_id": "abc123",
  "transcript_path": "/Users/me/.claude/projects/.../xxx.jsonl",
  "cwd": "/Users/me/projects/cooking-blog",
  "permission_mode": "default",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "napolitan の続きを書いて"
}

注目すべきは prompt フィールド。ユーザーが送信した本文がそのまま入ってきます。これを使って「プロンプトに特定の単語が含まれていたら遮断する」みたいな判定ができます。

フック側の応答方法は3パターンあります。

  1. 標準出力にプレーンテキストを出す - Claude のコンテキストに「ユーザーのプロンプトの追加情報」として連結される(さっきの料理ブログ例がこれ)
  2. JSON を出して hookSpecificOutput.additionalContext に文字列を入れる - 連結ではなく「システムからの追加情報」として構造化した形で渡る。プロンプトと混ざらないので意図しない混乱を防ぎやすい
  3. 終了コード2で終わる、または JSON で "decision": "block" を返す - プロンプトが Claude に届かない。reason フィールドの内容がエラーとして表示される

3番目の遮断パターンを使うと、たとえば社内コード名「Project Falcon」が含まれてたら止める、みたいなガードが書けます。

#!/usr/bin/env bash
# .claude/hooks/block-secrets.sh
input=$(cat)
prompt=$(echo "$input" | python3 -c "import sys, json; print(json.load(sys.stdin)['prompt'])")

if echo "$prompt" | grep -qi "project falcon"; then
  echo '{"decision":"block","reason":"社内コード名が含まれてます。一般名で書き直してください"}'
  exit 0
fi
exit 0

これを UserPromptSubmit に登録しておくと、私が「Project Falcon の進捗まとめて」と打った瞬間に「社内コード名が含まれてます」とだけ表示されて、Claude にはプロンプトが届きません。

他のフックとの違いも整理しておきます。

フック 発火タイミング 標準出力の扱い 遮断できるか
SessionStart セッション起動時1回だけ コンテキスト先頭に追加 不可
UserPromptSubmit プロンプト送信ごと(毎ターン) そのプロンプトの追加情報として連結 可(exit 2 / decision block)
PreToolUse ツール実行前 デバッグログ行き(AIに届かない) 可(permissionDecision deny)
PostToolUse ツール実行後 デバッグログ行き 不可
SessionEnd セッション終了時 デバッグログ行き 不可

UserPromptSubmit の独自性は「毎ターン走る」かつ「標準出力が AI に直接届く」かつ「遮断できる」の3つが揃っているところ。SessionStart は起動時1回だけなので会話途中の状態は反映できないし、PreToolUse はツール単位でしか動かないのでプロンプト全体には介入できません。

使いどころ3シナリオ

シナリオ1: 毎回「ドラフト一覧を見て」と書いてた料理ブログ運営

料理ブログを運営してて、「napolitan の続き書いて」「ramen の冒頭直して」と Claude に頼むたびに、その前に「drafts/ フォルダの一覧を見て、書きかけのファイル名を確認してから始めて」と毎回タイプしてた状態。

UserPromptSubmit に ls drafts/ を仕込むだけで、私のプロンプトには本題だけ書けばよくなります。1ターン10秒の節約でも、1日30回プロンプトを投げるなら5分浮きます。

シナリオ2: レシピ記事に栄養情報を自動添付したい

料理ブログで「鶏むね肉のサラダのレシピを書いて」とプロンプトを投げる時、毎回手動で栄養データを調べて貼ってたとします。

UserPromptSubmit に「プロンプトの中から食材名を抜き出して、公式の食材データAPIにアクセスして、栄養情報を JSON で添える」スクリプトを仕込めば、私が単に「鶏むね肉のサラダのレシピを書いて」と送るだけで、Claude は「鶏むね肉100gあたり タンパク質23g 脂質1.9g」みたいな数字を見ながら記事を書き始められます。

ここで使うのは hookSpecificOutput.additionalContext の方が向いてます。プロンプト本文と混ぜたくない構造化データだからです。

シナリオ3: 社内用語・秘密情報の流出ガード

個人ブログでも、たまに業務用のメモを混ぜてプロンプトを書いてしまうことがあります。「Project Falcon の進捗を料理ブログ風にまとめて」みたいに、無意識で社内コード名を Claude に渡してしまうケース。

UserPromptSubmit に「禁止ワードリストに引っかかったら exit 2」のスクリプトを仕込むと、Enter を押した瞬間にローカルで止まり、Claude(とその先のサーバー)には何も送られません。

うっかりミスを構造で防げます。

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

  • SessionStart と勘違いして「起動時1回だけ走る」と思ってしまう。実際はプロンプト送信ごとに毎回走る。重い処理(API 呼び出しでネット往復、巨大ファイル読み込み等)を仕込むと毎ターン数秒待たされてストレスになる。重い前処理は SessionStart 側に置く
  • 標準出力に余計な文字を出すと Claude が混乱する。フックの出力は「ユーザーのプロンプトに連結された追加情報」として渡る扱いなので、デバッグ用の echo "debug: starting hook" みたいな行も Claude には「ユーザーが書いた文字列」として読まれる。デバッグ出力は >&2 で標準エラー側に逃がすか、"suppressOutput": true を付ける
  • 遮断時に reason を空にすると「なぜ止まったか」が分からなくなる{"decision":"block","reason":""} だと画面には何も出ず、ただプロンプトが消えたように見える。必ず人間が読んで意味のある文を入れる
  • プロンプト本文には秘密情報が含まれうる。フック内で echo "$prompt" >> /tmp/log.txt みたいなログ取りをすると、ユーザーがコピペしたAPIキー・パスワード・社内情報がそのままファイルに残る。ログ取るなら本文ではなく送信時刻・文字数だけにする
  • タイムアウトの上限に注意。標準で600秒(10分)まで待つが、外部APIが返ってこない時にここまで待つと体感的に「Claude Code が固まった」と区別がつかない。timeout フィールドで5〜10秒程度に短く設定しておくのが安全
  • 複数フックを設定すると順番に実行される。1つ目のフックが exit 2 で遮断したら、2つ目以降は走らない。「まずバリデーション、次にコンテキスト追加」みたいに並べる時は順序を意識する
  • --bare 起動はフック全スキップ。デバッグ目的で claude --bare したまま「あれ、フック効いてない」と悩むやつ。普通の起動に戻せばいい

関連するフック・設定への動線

UserPromptSubmit と隣り合わせで使うものを並べておきます。フック仕様は同じ settings.json に書くので、組み合わせて使うことが多いです。

書き方

settings.json の hooks.UserPromptSubmit に command 型フックを登録して使う
{
  "hooks": {
    "UserPromptSubmit": [
      { "type": "command", "command": "スクリプトのフルパス" }
    ]
  }
}

やってみるとこうなる

入力

settings.json:
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "type": "command",
        "command": "cd \"$CLAUDE_PROJECT_DIR\" && echo \"[現在のドラフト一覧]\" && ls drafts/"
      }
    ]
  }
}

プロンプト:
> napolitan の続きを書いて

出力例

> napolitan の続きを書いて

⎿  [UserPromptSubmit hook] running...
⎿  [現在のドラフト一覧]
   bibimbap.md
   napolitan.md
   ramen.md

I'll continue writing napolitan.md. Let me first check
the current content of that draft...

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

フック
特定のタイミング(プロンプト送信時・ツール実行前後・セッション開始時など)に自動で割り込んで動かせる小さなプログラム。settings.json に登録して使う
プロンプト
ユーザーが Claude Code に送る指示文。Enter キーで送信した1メッセージ単位
settings.json
Claude Code の設定ファイル。プロジェクトルート直下の <code>.claude/settings.json</code> に置くとそのプロジェクトだけに効く
$CLAUDE_PROJECT_DIR
Claude Code が起動時に自動でセットしてくれる、プロジェクトのルートフォルダのパス
ターミナル
黒い画面で文字のコマンドを打ち込む画面。Windowsだと「コマンドプロンプト」「PowerShell」、Macだと「ターミナル」アプリ
ドラフト
公開前の下書き原稿。料理ブログだと <code>drafts/</code> フォルダに <code>napolitan.md</code> みたいなマークダウンファイルが入ってる想定
標準入力 / 標準出力
プログラムが「外から受け取る入り口」と「外に出す出口」。フックスクリプトは標準入力で JSON を受け取り、標準出力に書いた内容が Claude に渡る
標準エラー
標準出力とは別ルートの「エラー専用の出口」。デバッグログをここに出すと Claude には届かない
終了コード
プログラムが終わる時に返す数字。0 が「成功」。UserPromptSubmit では 2 だけ特別扱いで「プロンプトを遮断」になる
コンテキスト
Claude が応答を組み立てる時に参照する情報のかたまり。ユーザーのプロンプト・過去の会話・フックの出力などが詰まる場所
セッション
<code>claude</code> コマンドを起動してから終了するまでの1回のやり取り全体
API
プログラム同士がデータをやり取りするための窓口。ここでは「食材データを返してくれる外部サービスへのアクセス窓口」の意味
タイムアウト
「これ以上待たない」という制限時間。フックが指定秒数以内に終わらないと強制終了される
--bare
Claude Code をフックなし・カスタム設定なしの素の状態で起動するモード。デバッグ用
hookSpecificOutput.additionalContext
フックの返答 JSON で使えるフィールド。プロンプト本文と混ぜず、システムからの追加情報として構造化した形で Claude に渡せる

関連項目

公式ドキュメント

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

-

← 戻る