PostToolUseFailure hookでツール失敗時だけ動くリカバリを書く
PostToolUseFailureフックはツール呼び出しが失敗したときだけ発火します。エラー情報の受け取り方とPostToolUseとの使い分け、通知・リカバリ実装の例をまとめます。
PostToolUseFailureフックは、実行を開始したツール呼び出しが失敗したときだけ発火するフックです。ツールが例外を投げた場合や、MCPツールがエラー結果を返した場合が対象で、失敗をログに残す・アラートを送る・Claudeへ修正のヒントを渡すといった用途に使います。成功時にしか発火しないPostToolUseと役割が分かれているため、失敗系の処理をこちらに寄せると条件分岐が減ります。
PostToolUseFailureフックが発火するタイミング
matcherはツール名で絞り込め、値はPreToolUseと同じです。「失敗」の定義は狭く、実行を開始したツールが途中で失敗した場合に限られます。次のケースはこのイベントの対象外です。
- 存在しないツール名を呼んだ場合
- ツールのスキーマ検証や個別バリデーションで弾かれた場合
- 権限が拒否された場合
これらは実行が始まる前にtool_use_errorとして結果が返るため、PreToolUseもPostToolUseFailureもどちらも発火しません。権限拒否についてはPreToolUseは発火しますがPostToolUseFailureは発火しない、という非対称な扱いになっている点に注意します。権限拒否そのものを捕まえたいなら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に見せる(ツールは既に失敗済み) |
公式ドキュメントも、matcherを"*"にして全ツール完了後にPostToolUseを発火させ、失敗時の処理は同じhookをPostToolUseFailure側にも登録する構成を推奨しています。成功・失敗どちらでもgit status --porcelainのように差分を自分で調べるスクリプトなら、両方のイベントに同じハンドラを紐づけるだけで済みます。
どちらのイベントも「もう起きてしまったこと」を扱うため、exit code 2はツール呼び出し自体をブロックできません。ブロックしたいならPreToolUse側に判定を置きます。
受け取る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の場合は1行目がExit code N、続けて標準出力と標準エラーが1つのブロックとして混在します。シェルプロセス自体を起動できなかった場合はExit code行のない裸のメッセージになることもあります。10,000文字を超える文字列は... [N characters truncated] ...という表示で中略され、Command timed out after 2m 0sのようにClaude Code自身が行を挿入することもあります。
このためerrorをパースするときは、tool_name・is_interrupt・先頭のExit code N行の3つだけをキーにし、それ以外は表示用テキストとして扱うのが安全です。安定したフォーマットとして固定されているのはこの3点だけです。
duration_msで失敗の種類を見分ける
duration_msは権限確認やPreToolUseフックの待ち時間を含まない、ツールそのものの実行時間です。Exit code N行がなくduration_msが極端に短い場合は、Claude Codeがシェルプロセス自体を起動できなかった失敗である可能性が高く、依存コマンドの不在やパス設定のミスを疑う手がかりになります。一方、duration_msが設定したタイムアウト値に近ければ、実際に処理が長すぎて打ち切られたと判断できます。1つのerror文字列だけでは区別しにくい失敗の種類を、duration_msと組み合わせて切り分けられる点がこのフックの実用上の利点です。
デバッグ:実際のerror文字列を確認してから実装する
errorの書式はツールと失敗の種類ごとに変わるため、正規表現を書く前に実物を見ておくと手戻りが減ります。claude --debugで起動するかclaude --debug-file <path>を指定すると、フックが受け取った入力とフックが返した出力を含む詳細ログがファイルに書き出されます。意図的に失敗を起こしてログを確認し、errorの先頭行と改行位置を確かめてからパース処理を書くのが確実です。想定と違う書式に遭遇したら、その場で正規表現を厳密にするのではなく、キーにする範囲をtool_name・is_interrupt・Exit code N行の3点まで戻すほうが変更に強くなります。
非同期で通知だけ飛ばす
テストの完了を待たずにClaudeの作業を続けさせつつ、失敗だけ人間に知らせたい場合はasync: trueを付けます。非同期フックはdecisionのような制御フィールドを返しても効果がなくなる代わりに、terminalSequenceのようなデスクトップ通知を出す副作用系のフィールドはそのまま動作します。
{
"hooks": {
"PostToolUseFailure": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/notify-failure.sh",
"async": true
}
]
}
]
}
}同期のままadditionalContextでClaudeに次の一手を渡したいのか、非同期で人間側への通知だけを飛ばしたいのかによってasyncの要否が変わります。両方が要る場合は、同期ハンドラと非同期ハンドラを別々に登録すれば、Claudeへのフィードバックと人間への通知を独立に設計できます。
errorに含まれる情報の扱い
error文字列にはコマンドの標準出力・標準エラーがそのまま含まれるため、環境変数の値やAPIキーの断片がエラーメッセージに混ざっているケースがあります。Slack通知や外部ログサービスへerrorを転送するフックを書くときは、転送前にマスキング処理を挟むか、転送する範囲をExit code N行など安全な部分だけに絞ります。フック自体はユーザーの権限でシェルコマンドを実行するため、この種の情報漏えいはフックスクリプトのレビューで防ぐ以外に手立てがありません。
permission_modeでヘッドレス実行かどうかを判定する
共通入力フィールドのpermission_modeから、失敗が起きたセッションがどのモードで動いていたかを読み取れます。bypassPermissionsやautoで無人実行していたときの失敗と、defaultで人が画面を見ているときの失敗とでは、通知の緊急度が変わります。無人実行中の失敗だけデスクトップ通知を出し、人が見ているセッションではadditionalContextだけに留める、といった条件分岐に使えます。
MCPツールの失敗も同じ形で届く
MCPサーバーのツールがエラー結果を返した場合も、PostToolUseFailureは組み込みツールと同じ形で発火します。matcherもmcp__<server>__<tool>という命名規則にそのまま従うため、mcp__github__.*のように特定のMCPサーバー由来の失敗だけを狙って処理するハンドラも書けます。ただしerror文字列の書式はサーバー実装に依存し、Bash/PowerShellのような「Exit code N」始まりを前提にした処理はそのままでは流用できません。MCPツール用のハンドラは、キーにできる範囲をtool_nameとis_interrupt程度に絞るのが無難です。
実装例:テスト失敗時だけ修正のヒントを注入する
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へ渡る効果だけが働きます。
権限拒否は対象外
tool_use_errorとして返る検証エラーと権限拒否は、どちらも実行開始前に弾かれるためPostToolUseFailureは発火しません。権限拒否のログを取りたい場合はPermissionDeniedフックを別に登録しますが、このフックもauto modeでの拒否に限って発火し、手動拒否・PreToolUseによるブロック・denyルール一致は対象外です。
errorの書式をハードコードしない
ツール・失敗の種類ごとにerrorの形が変わるため、正規表現で厳密にパースしすぎると別のツールや将来のフォーマット変更で壊れます。tool_name・is_interrupt・Exit code N行の3点に絞って判定するのが安全です。
よくある質問
権限拒否で失敗したときもPostToolUseFailureは発火しますか
発火しません。権限拒否はPreToolUseは発火させますがPostToolUseFailureは発火させない非対称な扱いです。権限拒否そのものを捕まえたいならPermissionDeniedフックを使いますが、auto modeでの拒否に限って発火し、手動拒否は対象外です。
PostToolUseFailureでツールの再実行を強制できますか
直接はできません。additionalContextでClaudeに次の一手を示すことはできますが、ツールを自動で再実行する制御フィールドはこのイベントにはありません。PermissionDeniedのretry: trueのような専用フィールドとは別物です。
is_interruptがtrueになるのはどんなときですか
ツールの失敗が、ツール自身が返したエラーではなく中断として届いた場合です。ユーザーが実行中のツールをキャンセルした操作そのものはこのフックを発火させず、中断メッセージはツールの結果として返ります。
タイムアウトしたコマンドもこのフックで捕まりますか
捕まります。errorフィールドにCommand timed out after 2m 0sのようにClaude Code自身が挿入した行が含まれる形で届きます。
まとめ
PostToolUseFailureは「失敗したときだけ」に処理を絞れるフックです。成功時の後処理を担うPostToolUseと役割を分けておけば、ログ記録やアラート送信、修正ヒントの注入といった失敗専用のロジックを条件分岐なしで書けます。ツールは既に失敗しているためこのイベントで呼び出し自体は止められず、exit 2でstderrを確実にClaudeへ渡すかadditionalContextで情報を足すかの2択になる点を押さえておくと実装がぶれません。
エラーの内容次第で通知先や緊急度を出し分ける設計にしておくと、無人実行と対話実行の両方で無駄な通知を出さずに済み、後から見直すときの調査コストも小さく抑えられます。フック全体の仕様はClaude Code Hooks完全ガイド、他の発火タイミングとの組み合わせ方はHooks実例カタログ、最初の1個を動かす手順はClaude Code Hooksの設定方法をあわせて参照してください。