PostToolUseFailure hookでツール失敗時だけ動くリカバリを書く
PostToolUseFailureフックはツール呼び出しが失敗したときだけ発火します。エラー情報の受け取り方とPostToolUseとの使い分け、通知・リカバリ実装の例をまとめます。
PostToolUseFailureフックは、実行を開始したツール呼び出しが失敗したときだけ発火するフックです。ツールが例外を投げた場合や、MCPツールがエラー結果を返した場合が対象で、失敗をログに残す・アラートを送る・Claudeへ修正のヒントを渡すといった用途に使います。成功時にしか発火しないPostToolUseと役割が分かれているため、失敗系の処理をこちらに寄せると条件分岐が減ります。
どんな失敗で発火し、どんな失敗では発火しないか
matcherはツール名で絞り込め、値はPreToolUseと同じです。「失敗」の定義は狭く、実行を開始したツールが途中で失敗した場合に限られます。
PostToolUseFailureの発火範囲
発火する
- ツールが例外を投げた
- MCPツールがエラー結果を返した
- Bashのコマンドが0以外のコードで終わった
- コマンドがタイムアウトした
発火しない
- 存在しないツール名を呼んだ
- スキーマや個別バリデーションに通らなかった
- 権限が拒否された
ツール名の誤りとバリデーション失敗はtool_use_errorとして結果が返り、PreToolUseも発火しません。権限拒否はPreToolUseだけが発火する非対称な扱いです。実行中のツールをキャンセルした場合も発火せず、中断メッセージがツールの結果として返ります。
権限拒否そのものを捕まえたいならPermissionDeniedフックを使います。ただしこのフックはauto modeでの拒否に限って発火し、手動での拒否ダイアログ操作・PreToolUseフックによるブロック・denyルールへの一致は対象外です。
PostToolUseとPostToolUseFailureの使い分け
同じ「ツール呼び出しの後」に発火する2つのフックですが、成功と失敗で対象が分かれ、返せるフィールドとexit code 2の効果も違います。
| フック | 発火条件 | 主な用途 | exit code 2の効果 |
|---|---|---|---|
| PostToolUse | 発火条件ツールが正常に完了した直後 | 主な用途整形・テスト・正規化、結果の書き換え | exit code 2の効果stderrをClaudeに見せる(ツールは既に実行済み) |
| PostToolUseFailure | 発火条件ツールが失敗した直後 | 主な用途ログ記録、アラート送信、修正ヒントの注入 | exit code 2の効果stderrをClaudeに見せる(ツールは既に失敗済み) |
PostToolUseの節には次の書き方があります。matcherを省くか"*"にして全ツールの成功後に動かし、失敗するツール呼び出しには同じhookをPostToolUseFailure側にも追加します。git status --porcelainのように差分を自分で調べるスクリプトなら、両方のイベントに同じハンドラを紐づけるだけで済みます。
どちらのイベントも「もう起きてしまったこと」を扱うため、exit code 2はツール呼び出し自体をブロックできません。ブロックしたいならPreToolUse側に判定を置きます。PostToolUse単体でのmatcher設計やフォーマット・記録・チェックの実装例はPostToolUse hookでツール実行後の後処理を自動化するにまとめています。
受け取るJSON — errorフィールドの読み方
PostToolUseFailureはPostToolUseと同じtool_name・tool_inputに加えて、失敗情報をトップレベルのフィールドで受け取ります。
{
"hook_event_name": "PostToolUseFailure",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite"
},
"tool_use_id": "toolu_01ABC123...",
"error": "Exit code 1\nError: Cannot find module 'express'",
"is_interrupt": false,
"duration_ms": 4187
}| フィールド | 内容 |
|---|---|
error | 内容何が起きたかを表す文字列。形式はツールごとに異なる |
is_interrupt | 内容任意のbool。ツールがエラーを返したのではなく中断として失敗が届いた場合にtrue。実行中のツールをキャンセルした場合はこのフック自体が発火せず、中断メッセージはツールの結果として返る |
duration_ms | 内容任意。ツールの実行時間(ミリ秒)。権限確認やPreToolUseフックにかかった時間は含まない |
errorの中身はClaudeが受け取る失敗結果とほぼ同じ文字列で、ツールと失敗の種類によって形式が変わります。Bash・PowerShellでは、読み取れる形が次の3つに分かれます。
Bash・PowerShellのerrorの3つの形
Exit code N で始まる
コマンドが実行されて終了コードを返した場合です。1行目が
Exit code Nで、続けて標準出力と標準エラーが1つのブロックに混ざって入ります。Exit code行のない裸のメッセージ
シェルプロセス自体を起動できなかった場合に届くことがあります。終了コードで分岐するハンドラはこの形を取りこぼします。
Claude Codeが足した行
Command timed out after 2m 0sのように、Claude Code自身が行を挿入することがあります。長い文字列は... [N characters truncated] ...を境に中略されます。
Claude Codeの公式ドキュメントは、フックのキーをtool_name・is_interrupt・先頭のExit code N行の3つに絞るよう案内しています。残りは表示用テキストなので、正規表現で本文を厳密に読むほど、別のツールや将来の書式変更で壊れやすくなります。
duration_msはツールそのものの実行時間です。設定したタイムアウト値と見比べると、処理が長すぎて打ち切られたのかどうかを切り分ける材料になります。
実際のerror文字列を見てから実装する
正規表現を書く前に、実物を見ておくと手戻りが減ります。claude --debug-file <path>を指定すると、フックの実行結果を含む詳細ログがファイルに書き出されます。claude --helpでは「Write debug logs to a specific file path」と説明され、デバッグモードも同時に有効になります。
パース処理を書く前の下ごしらえ
- 1
デバッグログ付きで起動する
claude --debug-file /tmp/hook-debug.log - 2
意図的にツールを失敗させる
存在しないモジュールを読むテストを走らせるなど、確実に失敗する操作をClaudeに頼みます。
- 3
errorの先頭行と改行位置を見る
ログで
PostToolUseFailureの実行記録を探し、出力やerrorの内容が読み取れるか確かめます。入力がログに出ない場合は、フックスクリプト内でstdinを一時ファイルへ書き出して、Exit code行の有無と出力の始まりを見ます。 - 4
想定外の書式なら、キーを3点に戻す
分岐の条件を前述の3点だけに戻し、書式の揺れに引きずられない形にします。
非同期で通知だけ飛ばす
テストの完了を待たずにClaudeの作業を続けさせつつ、失敗だけ人間に知らせたい場合はasync: trueを付けます。この指定はtype: "command"のフックでだけ使え、非同期フックはdecisionのような制御フィールドを返しても効果がありません。
{
"hooks": {
"PostToolUseFailure": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/notify-failure.sh",
"async": true
}
]
}
]
}
}非同期フックのプロセスが終わると、JSON出力のadditionalContextとsystemMessageが次のターンでClaudeに渡されます。同期フックのsystemMessageと違い、どちらもユーザーには表示されません。
claude -pで非対話実行しているときは、終了時点で動いている非同期フックが強制終了されます。通知を確実に届けたい無人実行では、この点に注意が必要です。
同期のままadditionalContextでClaudeに次の一手を渡したいのか、非同期で人間側への通知だけを飛ばしたいのかによってasyncの要否が変わります。両方が要る場合は、同期ハンドラと非同期ハンドラを別々に登録すれば、Claudeへのフィードバックと人間への通知を独立に設計できます。
MCPツールの失敗も同じ形で届く
MCPサーバーのツールがエラー結果を返した場合も、PostToolUseFailureは組み込みツールと同じ形で発火します。matcherもmcp__<server>__<tool>という命名規則に従います。そのためmcp__github__.*のように特定のMCPサーバー由来の失敗だけを狙って処理するハンドラも書けます。
MCPツールの場合、入力にはmcp_serverオブジェクトも加わります。中身はサーバー名のnameと、サーバー定義の出どころを示すsourceです。sourceにはplugin・sdkや、user・projectのような設定スコープが入ります。
このフィールドにはClaude Code v2.1.274以降が必要です。信頼の判断はmcp__<server>__の接頭辞やnameではなく、sourceに基づけるのが公式の指針です。
error文字列の書式はサーバー実装に依存するので、Bash/PowerShellのようなExit code N始まりを前提にした処理はそのままでは流用できません。MCPツール用のハンドラは、キーにできる範囲をtool_nameとis_interrupt程度に絞るのが無難です。
ifとハンドラの種類で絞り込む
matcherはツール名までしか絞れません。ifフィールドに権限ルールの書式("Bash(npm test*)"など)を書くと、ツール呼び出しがそのパターンに一致したときだけコマンドが動きます。ifが評価されるのは、PreToolUse・PostToolUse・PostToolUseFailure・PermissionRequest・PermissionDeniedの5つだけです。それ以外のイベントに付けたフックは一度も動きません。
{
"hooks": {
"PostToolUseFailure": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(npm test*)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-failure-hint.sh"
}
]
}
]
}
}このイベントで使えるハンドラはcommandだけではありません。http・mcp_tool・prompt・agentを含む5種類に対応しています。
promptとagentのフックがok: falseを返すと、その理由は失敗したツールのエラーとしてClaudeに返り、ターンは続きます。PostToolUseは既定でターンが終わり、continueOnBlockで継続を選ぶ形なので、ここは動きが違います。
errorに含まれる情報の扱い
error文字列にはコマンドの標準出力・標準エラーがそのまま含まれるため、環境変数の値やAPIキーの断片がエラーメッセージに混ざっているケースがあります。Slack通知や外部ログサービスへerrorを転送するフックを書くときは、転送前にマスキング処理を挟むか、転送する範囲をExit code N行など安全な部分だけに絞ります。
共通入力フィールドのpermission_modeからは、失敗が起きたセッションがどのモードで動いていたかを読み取れます。bypassPermissionsやautoで無人実行していたときの失敗と、defaultで人が画面を見ているときの失敗とでは、通知の緊急度が変わります。無人実行中の失敗だけデスクトップ通知を出し、人が見ているセッションではadditionalContextだけに留める、といった条件分岐に使えます。
実装例:テスト失敗時だけ修正のヒントを注入する
npm testが失敗したときだけ、失敗したテストファイル名を抜き出してClaudeに伝えるスクリプトです。ユーザーによる中断(is_interrupt: true)は対象から除外します。
#!/bin/bash
# .claude/hooks/test-failure-hint.sh
input=$(cat)
tool_name=$(jq -r '.tool_name' <<<"$input")
command=$(jq -r '.tool_input.command // empty' <<<"$input")
is_interrupt=$(jq -r '.is_interrupt // false' <<<"$input")
error=$(jq -r '.error // empty' <<<"$input")
if [[ "$tool_name" != "Bash" \
|| "$command" != npm\ test* \
|| "$is_interrupt" == "true" ]]; then
exit 0
fi
failing_file=$(grep -oE '[A-Za-z0-9_/-]+\.test\.[jt]sx?' <<<"$error" | head -1)
if [[ -n "$failing_file" ]]; then
jq -n --arg f "$failing_file" '
{
hookSpecificOutput: {
hookEventName: "PostToolUseFailure",
additionalContext: (
"失敗したテストファイルは " + $f + " です。"
+ "関連する実装ファイルとテストの期待値を"
+ "突き合わせてから再実行してください。"
)
}
}'
else
exit 0
fi設定側はBashツールにmatcherを絞って紐づけます。
{
"hooks": {
"PostToolUseFailure": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-failure-hint.sh"
}
]
}
]
}
}additionalContextは失敗結果のすぐ横にシステムリマインダーとして挿入され、Claudeは次のモデル呼び出しでこれを読みます。チャット欄には表示されないため、ユーザー向けの通知が必要な場合は別途systemMessageやterminalSequenceを組み合わせます。
落とし穴
exit 0のstderrはClaudeに届かない
PostToolUseFailureがexit 0で終わると、stderrはデバッグログにだけ記録され、Claudeにも画面にも表示されません。失敗の警告をClaudeの目に触れさせたいならexit 2で終える必要があります。ツールは既に失敗しているためexit 2でも呼び出し自体は取り消されず、単にstderrが確実にClaudeへ渡る効果だけが働きます。
additionalContextは1万文字で切り替わる
additionalContext・systemMessage・プレーンなstdoutは、10,000文字を超えるとセッションディレクトリのファイルに書き出されます。Claudeには先頭2,000文字までのプレビューとファイルパスだけが渡り、ファイルを読むよう促す動作はありません。
ログ全文をそのままadditionalContextに流すと、肝心の部分がプレビューから外れかねません。失敗箇所だけを抜き出して渡すほうが確実です。
よくある質問
PostToolUseFailureでツールの再実行を強制できますか
直接はできません。additionalContextでClaudeに次の一手を示すことはできますが、ツールを自動で再実行する制御フィールドはこのイベントにはありません。PermissionDeniedのretry: trueのような専用フィールドとは別物です。
まとめ
PostToolUseFailureは、成功時の後処理を担うPostToolUseと分けておくことで、失敗専用のロジックを条件分岐なしで書けるフックです。ツールは既に失敗しているため呼び出し自体は止められず、Claudeへ情報を届ける手段は、exit 2のstderrかadditionalContextです。
通知先や緊急度をpermission_modeで出し分ける設計にしておくと、無人実行と対話実行の両方で無駄な通知を出さずに済みます。フック全体の仕様はClaude Code Hooks完全ガイドにあります。他の発火タイミングとの組み合わせ方はHooks実例カタログ、最初の1個を動かす手順はClaude Code Hooksの設定方法です。