Claude Code の hooks の入出力仕様、stdin の JSON の構造と終了コード 0・2・その他の意味
hooks のコマンドは標準入力で JSON を受け取り、終了コードで続行・ブロック・警告を Claude Code に伝えます。全イベント共通の項目とイベント別の項目、jq で値を取り出す実例、終了コード 2 で標準エラー出力が Claude に渡る仕組み、JSON 出力による細かい制御を 1 本にまとめます。
目次
hooks でスクリプトを実行する設定は書けたものの、そのスクリプトの中で「どのファイルが編集されたのか」をどう知るのか、そして lint が失敗したことをどう Claude に伝えるのかが分かりません。この 2 つは hooks の「入力」と「出力」の仕様で、どちらも押さえないと、lint が失敗しても Claude が気付かない、あるいは警告のつもりが処理を止めてしまう、という食い違いが起きます。
結論として、入力は標準入力に流れてくる JSON、出力は終了コードです。コマンド型のフックは stdin から JSON を受け取り、HTTP 型のフックは同じ JSON を Content-Type: application/json の POST ボディとして受け取ります。終了コードは 0 が成功、2 がブロック(標準エラー出力が Claude に渡る)、それ以外が警告(ユーザーに表示、処理は続行)です。
KEY POINT
この記事で分かること
- 全イベント共通で入る JSON の項目と、PreToolUse・Stop・SessionStart などイベント別に追加される項目
jqで値を取り出す実例と、JSON にない情報の取得方法- 終了コード 0 / 2 / その他の挙動の違いと、イベントごとの「ブロック」の意味
- 標準出力の 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 は同じ構造に tool_output が加わり、PostToolUseFailure も同じ形で error にエラーの種類(例: TimeoutError)が入ります。
{
"tool_output": {
"text": "Test results...",
"error": null
}
}
その他のイベント
| イベント | 追加される項目 | 値の例 |
|---|---|---|
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) で変数に入れてから必要な項目を何度でも取り出します。最後の exit 2 が次の節で説明する「ブロック」です。
用語解説
stdin(標準入力): プロセスに外部からデータを流し込む経路です。cat で受け取るとファイルのように扱えます。一度読み切ると再読み込みできないため、変数に保持するのが定石です。
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"
}
出力、終了コードの意味
スクリプトの終了コードは、Claude Code に「続けてよいか」「止めるか」「注意だけするか」を伝える信号です。
| 終了コード | 扱い | 標準出力 | 標準エラー出力 |
|---|---|---|---|
0 | 成功 | ユーザーに表示される(一部のイベントでは Claude のコンテキストに追加) | 表示されない |
2 | ブロック | 無視される | Claude に渡され、Claude はその内容に従って対応する |
その他(1 など) | 非ブロックのエラー | JSON の形なら解釈され、それ以外は無視される | 先頭行が Failed with non-blocking status code: 付きでユーザーに表示される。処理は続行 |
「Claude に何かを伝えたい」ときは 2、「人間にだけ知らせたい」ときは 1 です。終了コード 0 で標準出力が Claude のコンテキストに追加されるのは UserPromptSubmit、UserPromptExpansion、SessionStart、PostModelSwitch です。PreToolUse で 0 を返してもツール呼び出しが承認されるわけではなく、通常の権限フローがそのまま適用されます。
イベントごとの「ブロック」の意味
| イベント | ブロックできるか | 終了コード 2 の効果 |
|---|---|---|
PreToolUse | できる | ツールの実行を中止する。標準エラー出力の内容が Claude に渡る |
PostToolUse | 実行済みの操作は取り消せない | 標準エラー出力の内容が Claude に渡り、修正を促す |
UserPromptSubmit | できる | プロンプトを Claude に渡さず消去し、標準エラー出力をユーザーに表示 |
UserPromptExpansion | できる | 展開を中止する |
Stop / SubagentStop | できる | 停止を止め、Claude に作業を続けさせる(標準エラー出力が指示になる) |
PermissionRequest | できない | 2 は無視され、権限フローはそのまま進む。拒否は後述の decision オブジェクトで行う |
SessionStart など | できない | 標準エラー出力をユーザーに表示して続行 |
ConfigChange や Elicitation のようにメッセージが表面に出ないイベントもあります。ブロックに頼る前に、公式リファレンスのイベント別の表を確認してください。
用語解説
PostToolUse で「ブロック」しても取り消せない: PostToolUse は実行後に走るため、ファイルは既に書き換わっています。2 を返す目的は「取り消し」ではなく、「lint エラーがあるから直して」と Claude に伝えることです。
使い分けの例
PreToolUse で危険なコマンドを止める(2)。完全版と限界は PreToolUse フックで rm -rf を止める を参照してください。
#!/usr/bin/env bash
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push --force'; then
echo "このコマンドは禁止されています: $cmd" >&2
exit 2
fi
exit 0
PostToolUse で lint 失敗を Claude に直させる(2)。
#!/usr/bin/env bash
file=$(jq -r '.tool_input.file_path // empty')
[[ "$file" == *.ts ]] || exit 0
if ! npx eslint "$file"; then
echo "ESLint がエラーを報告しました。修正してください: $file" >&2
exit 2
fi
exit 0
人間にだけ知らせる(1)。Claude の処理は止まらず、ユーザーの画面に警告だけが出ます。
#!/usr/bin/env bash
file=$(jq -r '.tool_input.file_path // empty')
if [[ "$file" == */migrations/* ]]; then
echo "注意: マイグレーションファイルが変更されました: $file" >&2
exit 1
fi
exit 0
出力、JSON で細かく制御する
終了コードでは表現できない制御は、終了コード 0 で標準出力に JSON を書きます。使うフィールドはイベントごとに違う点が間違えやすいところです。
PreToolUse は hookSpecificOutput.permissionDecision を使います。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "破壊的なコマンドをフックで拒否しました"
}
}
PostToolUse と Stop はトップレベルの decision: "block" と reason、PermissionRequest は hookSpecificOutput.decision.behavior を使います。書こうとしているイベントの decision control の表を公式リファレンスで確認してください。
標準出力に余計なものを出さない
Claude Code が標準出力を JSON として解釈するのは、{ で始まり } で終わるときだけです。デバッグ用の echo が 1 つ混ざるだけで解釈に失敗し、しかも静かに失敗します。非対話の sh -c が読み込むシェルの設定ファイルに無条件の echo があっても同じことが起きます。デバッグ出力は標準エラー出力に書いてください。
関連する記事
hooks の設定ファイルの書き方と、lint / format の自動実行の全体像は親記事の hooks で lint と format を自動実行する を参照してください。作業完了を通知する Stop フックの実例は Stop フックで作業完了をデスクトップ通知する にあります。
まとめ
- コマンド型のフックは stdin から JSON を受け取る。共通項目は
session_id、cwd、permission_mode、hook_event_name、effortなど。ツールのイベントではtool_name、tool_input、tool_use_idが入る - 項目名はイベントごとに違う(
SessionStartはhow、SessionEndはwhy、PreCompactはwhat) - スクリプトでは
INPUT=$(cat)で保持し、jq -r '... // empty'で取り出す。プロジェクトルートは$CLAUDE_PROJECT_DIR - 終了コード 0 は成功、2 はブロック(標準エラー出力が Claude に渡る)、その他は警告(ユーザーに表示)。PostToolUse の 2 は取り消しではなく修正指示
PermissionRequestは 2 を無視する。JSON 制御のフィールドはイベントごとに違い、標準出力には JSON 以外を出さない
よくある質問
- hooks のスクリプトはどうやって入力を受け取りますか?
- コマンド型のフックは標準入力(stdin)から JSON を受け取ります。HTTP 型のフックは同じ JSON を POST のリクエストボディとして受け取ります。
- 編集されたファイルのパスはどこに入っていますか?
- PreToolUse と PostToolUse の `tool_input` の中です。Edit や Write なら `.tool_input.file_path` で取り出せます。
- hooks で終了コード 1 を返すとどうなりますか?
- 0 と 2 以外の終了コードは「非ブロックのエラー」として扱われ、標準エラー出力の先頭行がユーザーに表示されますが、Claude の処理は続行されます。
- PostToolUse で終了コード 2 を返しても操作は取り消せますか?
- 取り消せません。PostToolUse はツール実行後に走るため、2 を返しても実行済みの操作は残ります。標準エラー出力の内容が Claude に渡され、修正を促す形になります。
- プロジェクトのルートディレクトリはどう取得しますか?
- JSON の `cwd` はフック実行時のカレントディレクトリです。セッションを開始したプロジェクトルートは環境変数 `$CLAUDE_PROJECT_DIR` で取得します。
読者からの質問と回答
Q. 複数のフックから additionalContext を返していますが、Claude に届く順序が毎回変わります。登録した順にはならないのでしょうか。
順序は保証されていません。 公式ドキュメントが保証しているのは、値がすべて届くことだけです。「When several hooks return additionalContext for the same event, Claude receives all of the values」とあり、順序については何も書かれていません。
順序が安定しない理由は並列実行です。ドキュメントは「When an event fires, its matching hooks run in parallel」と明記しています。
| 事項 | 公式ドキュメントの記述 |
|---|---|
| 実行 | 同じイベントに一致するフックは並列に走る |
| 同時実行数 | 下記のとおり記述が分かれている |
additionalContext | 複数のフックが返した値はすべて Claude に届く |
| 順序 | 記載なし |
同時実行数は記述が分かれている
2026 年 11 月 1 日時点のフックのドキュメントは「If more than 12 of them are prompt or agent hooks, 12 of those run at once and the rest start as running ones finish」と書いています。一方 v2.1.312 の CHANGELOG は「Changed prompt and agent hooks to run at most 24 at a time per session, sub-agents included, plus at most 12 that have waited longer than their event's timeout; a timeout under 1 second now counts as 1 second」としています。どちらが現在の値かは文書からは判断できません。いずれにしても並列で走る点は変わらないため、順序に依存しない作りにしてください。
順序に意味を持たせたい場合は、1 つのフックで文字列を組み立てて返すのが確実です。複数のフックの出力を結合して順番を決めるのではなく、順番を決める責任を 1 か所に寄せます。
セッション全体に効かせたい固定の指示なら、フックではなく起動時のフラグが向いています。
# 既定のシステムプロンプトに追記する
claude --append-system-prompt-file ./extra-rules.txt
1 つの値が 10,000 文字を超えるとファイルに退避される
additionalContext、systemMessage、initialUserMessage と素の標準出力は、それぞれ 10,000 文字で上限に達します。超えるとセッションディレクトリのファイルに保存され、Claude にはそのパスと先頭 2,000 文字までのプレビューが渡されます。公式ドキュメントは「Claude Code doesn't ask Claude to read the file」と書いているため、必ず読ませたい内容は上限内に収めてください。この上限を引き上げる設定や環境変数はありません。
挿入される位置はイベントごとに決まっています。SessionStart と SubagentStart は会話の先頭、UserPromptSubmit は送信したプロンプトの隣、PreToolUse と PostToolUse はツール結果の隣、Stop と SubagentStop はターンの終わりです。
なお、同じハンドラを複数の設定ファイルに定義しても実行は 1 回です(プラグインやスキルが持つ同じハンドラは別扱い)。フックの入出力の仕様は Claude Code のフックの入出力仕様 にまとめてあります。
参考にした一次情報
この記事は公式ドキュメントを基に AI が下書きを作成し、運営者が内容を確認して公開しています。誤りを見つけた場合は お問い合わせからお知らせください。