Claude Code の hooks の入出力仕様、stdin の JSON の構造と終了コード 0・2・その他の意味

Claude Code 公開: 更新:

hooks のコマンドは標準入力で JSON を受け取り、終了コードで続行・ブロック・警告を Claude Code に伝えます。全イベント共通の項目とイベント別の項目、jq で値を取り出す実例、終了コード 2 で標準エラー出力が Claude に渡る仕組み、JSON 出力による細かい制御を 1 本にまとめます。

検証日 2026年10月10日 仕様変更が早い分野です。最新の公式ドキュメントも併せてご確認ください。 記事の作り方と検証手順
目次
  1. 入力、全イベント共通の項目
  2. 入力、イベント別に追加される項目
    1. PreToolUse / PostToolUse
    2. その他のイベント
  3. jq で値を取り出す
  4. JSON に入らない情報は環境変数から取る
  5. 出力、終了コードの意味
    1. イベントごとの「ブロック」の意味
    2. 使い分けの例
  6. 出力、JSON で細かく制御する
  7. 関連する記事
  8. まとめ

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_modedefault / plan / acceptEdits / auto / dontAsk / bypassPermissions
effortlevel を持つオブジェクト。値は low / medium / high / xhigh / max
hook_event_name発火したイベント名
agent_idサブエージェントの識別子(サブエージェントのコンテキストのみ)
agent_typeExplore のようなエージェント名(サブエージェント時、または --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
  }
}

その他のイベント

イベント追加される項目値の例
UserPromptSubmituser_inputプロンプトの本文
Stoplast_assistant_message、stop_reasonend_turn
SubagentStopagent_type、agent_id、last_assistant_message、stop_reasoncode-reviewer
SessionStarthow、modelstartup / resume / clear / compact / fork
SessionEndwhy、modelclear / resume / logout / prompt_input_exit / other
PreCompact / PostCompactwhatmanual / auto
Notificationtype、metadatapermission_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_IDRemote 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 に届く順序が毎回変わります。登録した順にはならないのでしょうか。

編集部の回答質問の出典: GitHub anthropics/claude-code Issue #105799

順序は保証されていません。 公式ドキュメントが保証しているのは、値がすべて届くことだけです。「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 が下書きを作成し、運営者が内容を確認して公開しています。誤りを見つけた場合は お問い合わせからお知らせください。