Claude Code の hooks が受け取る stdin JSON の構造:共通項目とイベント別の項目
hooks のコマンドは標準入力で JSON を受け取ります。全イベント共通の項目と、PreToolUse・Stop・SessionStart などイベント別の項目を一覧にし、jq で値を取り出す実例を示します。
hooks でスクリプトを実行する設定は書けたものの、そのスクリプトの中で「どのファイルが編集されたのか」「どのコマンドが実行されようとしているのか」をどう知るのかが分かりません。
答えは 標準入力に流れてくる JSON です。コマンド型のフックは stdin から JSON を受け取り、HTTP 型のフックは同じ JSON を Content-Type: application/json の POST ボディとして受け取ります。この記事では、その JSON の構造を共通項目とイベント別項目に分けて整理します。
KEY POINT
この記事で分かること
- 全イベント共通で入る項目と、それぞれの意味
- PreToolUse・Stop・SessionStart などイベント別に追加される項目
jqで値を取り出す実例と、JSON にない情報の取得方法
全イベント共通の項目
どのイベントでも、次の項目がイベント固有の項目に加えて入ります。
{
"session_id": "abc123",
"prompt_id": "550e8400-e29b-41d4-a716-446655440000",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"scratchpad_dir": "/tmp/claude-1000/-home-user-my-project/abc123/scratchpad",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"effort": {
"level": "medium"
}
}
| 項目 | 内容 |
|---|---|
session_id | 現在のセッションの識別子 |
prompt_id | ユーザープロンプトを識別する UUID(最初の入力までは入らない。v2.1.196 以降) |
transcript_path | 会話 JSON のパス(現在のターンに追いついていない場合がある) |
cwd | フック実行時のカレントディレクトリ |
scratchpad_dir | セッションのスクラッチパッドのパス(利用できない場合は入らない。v2.1.257 以降) |
permission_mode | default / plan / acceptEdits / auto / dontAsk / bypassPermissions |
effort | level を持つオブジェクト。値は low / medium / high / xhigh / max |
hook_event_name | 発火したイベント名 |
agent_id | サブエージェントの識別子(サブエージェントのコンテキストのみ) |
agent_type | Explore のようなエージェント名(サブエージェント時、または --agent 使用時) |
hook_event_name が入るため、1 つのスクリプトを複数のイベントで共用して、中で分岐する書き方ができます。
transcript_path は最新とは限らない
公式ドキュメントには、transcript_path が現在のターンに追いついていない場合がある(may lag current turn)と明記されています。進行中のやりとりを前提にした処理には使わないでください。
イベント別に追加される項目
PreToolUse / PostToolUse
もっともよく使うイベントです。PreToolUse には次の 3 項目が追加されます。
{
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite",
"timeout": 120000,
"run_in_background": false
},
"tool_use_id": "toolu_01ABC123..."
}
tool_input の中身はツールごとに異なります。Bash なら command、Edit や Write なら file_path が入ります。
PostToolUse は PreToolUse と同じ構造に tool_output が加わります。
{
"tool_output": {
"text": "Test results...",
"error": null
}
}
PostToolUseFailure も同じ形で、error にエラーの種類(例: TimeoutError)が入ります。
その他のイベント
| イベント | 追加される項目 | 値の例 |
|---|---|---|
UserPromptSubmit | user_input | プロンプトの本文 |
Stop | last_assistant_message、stop_reason | end_turn |
SubagentStop | agent_type、agent_id、last_assistant_message、stop_reason | code-reviewer |
SessionStart | how、model | startup / resume / clear / compact / fork |
SessionEnd | why、model | clear / resume / logout / prompt_input_exit / other |
PreCompact / PostCompact | what | manual / auto |
Notification | type、metadata | permission_prompt / idle_prompt / auth_success |
SessionStart の項目名は how、SessionEnd は why、PreCompact は what と、イベントごとに名前が違います。ここは推測せず一覧で確認してください。なお model は「常に入るわけではない」と公式ドキュメントに書かれています。
これらの値は matcher でも絞り込めます。どのイベントで何が照合されるかは hooks の matcher に書ける値一覧 にまとめています。
jq で値を取り出す
もっとも短い書き方は、コマンドに直接 jq をつなぐ形です。公式ドキュメントも、編集後に Prettier をかける例をこの形で示しています。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
スクリプトファイルにする場合は、まず cat で全体を受け取ってから取り出します。公式ドキュメントの保護ファイル用スクリプトは次の形です。
#!/usr/bin/env bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Windows のバックスラッシュ区切りを正規化して、下のパターンに一致させる
FILE_PATH="${FILE_PATH//\\//}"
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
ポイントは 2 つあります。
// emptyを付ける。file_pathを持たないツールで発火した場合に、jqがnullという文字列を返すのを防げます- stdin は 1 回しか読めない。
INPUT=$(cat)で変数に入れてから、必要な項目を何度でも取り出します
終了コード 2 は処理をブロックします。意味の違いは hooks の終了コードの意味と使い分け を参照してください。
用語解説
stdin(標準入力): プロセスに外部からデータを流し込む経路です。cat で受け取るとファイルのように扱えます。一度読み切ると再読み込みできないため、変数に保持するのが定石です。
JSON に入らない情報は環境変数から取る
パスの起点など、JSON に含まれない情報は環境変数で受け取ります。フックのプロセスは親の環境を継承したうえで、次の変数が追加されます。
| 環境変数 | 内容 |
|---|---|
$CLAUDE_PROJECT_DIR | セッションを開始したプロジェクトのルート |
$CLAUDE_PLUGIN_ROOT | プラグインのインストール先(プラグインのフック用) |
$CLAUDE_PLUGIN_DATA | プラグインの永続データ用ディレクトリ |
$CLAUDE_EFFORT | 現在の effort(low / medium / high / xhigh / max) |
$CLAUDE_CODE_REMOTE | リモートの Web 環境では true |
$CLAUDE_CODE_BRIDGE_SESSION_ID | Remote Control のセッション ID(v2.1.199 以降、接続中のみ) |
JSON の cwd は「フック実行時のカレントディレクトリ」なので、サブディレクトリで作業していると変わります。スクリプトの場所を固定して参照したいときは $CLAUDE_PROJECT_DIR を使ってください。
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
}
具体的な lint・format の組み方は hooks で lint と format を自動実行する にまとめています。
まとめ
- コマンド型のフックは stdin から JSON を受け取る。HTTP 型は同じ JSON を POST ボディで受け取る
- 共通項目は
session_id、transcript_path、cwd、permission_mode、hook_event_name、effortなど - ツールのイベントでは
tool_name、tool_input、tool_use_idが入り、PostToolUseではtool_outputが加わる - 項目名はイベントごとに違う(
SessionStartはhow、SessionEndはwhy、PreCompactはwhat) - スクリプトでは
INPUT=$(cat)で保持し、jq -r '... // empty'で取り出す - JSON にないプロジェクトルートは
$CLAUDE_PROJECT_DIRから取得する
よくある質問
- hooks のスクリプトはどうやって入力を受け取りますか?
- コマンド型のフックは標準入力(stdin)から JSON を受け取ります。HTTP 型のフックは同じ JSON を POST のリクエストボディとして受け取ります。
- 編集されたファイルのパスはどこに入っていますか?
- PreToolUse と PostToolUse の `tool_input` の中です。Edit や Write なら `.tool_input.file_path` で取り出せます。
- どのイベントでも使える項目はありますか?
- あります。`session_id`、`transcript_path`、`cwd`、`permission_mode`、`hook_event_name` などは全イベント共通で入ります。
- プロジェクトのルートディレクトリはどう取得しますか?
- JSON の `cwd` はフック実行時のカレントディレクトリです。セッションを開始したプロジェクトルートは環境変数 `$CLAUDE_PROJECT_DIR` で取得します。
参考にした一次情報
この記事は公式ドキュメントを基に AI が下書きを作成し、運営者が内容を確認して公開しています。誤りを見つけた場合は お問い合わせからお知らせください。