はじめに / 対象と前提
Claude Code の hook 機能はいくつか種類があるが、今回扱う UserPromptSubmit は使っている人がまだ少ない印象がある。「ユーザーがプロンプトを送信した瞬間」に割り込めるフックで、危険な指示をブロックしたり、逆に毎回自動で前提情報を差し込んだりできる。
対象読者は以下を想定。
- Claude Code を業務で日常的に使っている
-
.claude/settings.jsonの hooks 設定を一度は触ったことがある(PreToolUseやPostToolUseは知っている) - チームで Claude Code を使わせる際に、危険な指示や機密情報の書き込みを事前にガードしたい
検証環境は以下。
- Claude Code v2.x 系(hooks 設定フォーマットは 2025 年末以降のもの)
- macOS / zsh
- Python 3.13(hook スクリプトの実装に使用)
TL;DR
-
UserPromptSubmitはユーザーの入力がモデルに渡る前に発火する唯一の hook で、入力の拒否と追加コンテキストの注入の両方ができる - ブロックしたい場合は exit code 2 + stderr にメッセージ、コンテキストを足したい場合は stdout に JSON で
additionalContextを返す、という 2 系統の使い分けが肝 - 他の hook(
PreToolUseなど)と違い、tool_nameが存在しないので matcher の書き方を間違えると設定自体が無視される
手順 / 動かし方
1. hook スクリプトを用意する
標準入力から JSON が渡ってくるので、prompt フィールドを見て判定する。
#!/usr/bin/env python3
import json
import re
import sys
data = json.load(sys.stdin)
prompt = data.get("prompt", "")
DANGEROUS_PATTERNS = [
r"rm\s+-rf\s+/",
r"git\s+push\s+.*--force",
r"\.env\b.*(cat|print|show)",
]
for pattern in DANGEROUS_PATTERNS:
if re.search(pattern, prompt, re.IGNORECASE):
print(f"危険な操作の可能性があるプロンプトを検知: {pattern}", file=sys.stderr)
sys.exit(2) # ブロック。Claude には渡らない
# ブロックしない場合は、前提情報を追加コンテキストとして注入
output = {
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "このリポジトリは本番DBに直結しているため、破壊的操作の前は必ず確認を取ること。",
}
}
print(json.dumps(output))
sys.exit(0)
2. settings.json に登録する
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/guard_prompt.py"
}
]
}
]
}
}
PreToolUse と違って matcher にツール名を指定する概念がないので、配列の中に hooks だけを持つオブジェクトを 1 つ置けば全プロンプトに適用される。
3. 動作確認
危険パターンに該当するプロンプトを投げてみると、Claude 側の応答が生成される前にブロックされ、stderr の内容がターミナルに表示されて処理が中断する。該当しないプロンプトでは、additionalContext に書いた文字列がシステム側の文脈として自然に付与され、Claude の回答にそれを踏まえた注意書きが混ざるようになる。実際に「本番DBを触る系の依頼」を送ってみて、確認を挟むようになったことを目視で確認した。
ハマりどころ
1. exit code とJSON出力を混同して両方書いてしまう
ブロックは exit code 2、コンテキスト注入は stdout の JSON、と経路が別なのに、同じスクリプト内で「ブロックもしたいし通常時はコンテキストも足したい」を素朴に書くと、ブロック時に stdout の JSON も出力してしまい、Claude Code 側のパースでエラーログが出ることがあった。ブロック分岐では sys.exit(2) の前に余計な stdout を書かないよう、早期リターンで分離するのが安全。
2. additionalContext が思ったタイミングで効かない
additionalContext はその回のプロンプト処理には効くが、次のターン以降は毎回 hook が再実行されない限り引き継がれない。「一度注入したら以降もずっと有効」と誤解して、2 ターン目で情報が消えたように見えて焦った。毎ターン発火する hook である以上、必要な情報は毎回付け直される前提で設計する必要がある。
3. 他の hook との実行順序でタイムアウトを食う
SessionStart で重い初期化処理をする hook を別途仕込んでいる場合、UserPromptSubmit はそれとは独立に毎回同期的に実行される。プロンプト検査に外部 API 呼び出しなどを挟むと、体感のレスポンスが毎回そのぶん遅くなる。hook のタイムアウトはデフォルトで短めに設定されているため、重い処理を書くとタイムアウトでフック自体が失敗し、結果的にブロックもコンテキスト注入も効かないまま素通りしてしまう。外部通信を伴う判定は避け、正規表現などのローカル処理に留めるのが無難だった。
背景・補足
PreToolUse はツール実行そのものをガードする hook なので「Bash でこのコマンドを実行しようとしている」段階で止められるが、UserPromptSubmit はそれよりさらに手前、Claude が何も考える前の入力の時点で介入できる。プロンプトインジェクション対策や、チーム利用時のガードレールとして使うなら、ツール実行を待たずに弾ける分こちらの方が早い段階で防御できる。両方を組み合わせて多層防御にするのが実務的には現実的だと感じた。
まとめ
-
UserPromptSubmitは入力そのものを検査できる唯一の hook で、ブロックと追加コンテキスト注入の 2 系統を使い分ける - ブロックは exit code 2 + stderr、コンテキスト注入は stdout の JSON、経路を混ぜると事故る
- additionalContext は毎ターン再注入される前提で設計する
- 判定処理は重くしすぎるとタイムアウトで素通りするので、ローカルで完結させるのが安全