StopFailureフックでAPIエラー終了時のフォールバックを書く
ターンがAPIエラーで終わったときだけ発火するStopFailureフック。decision controlを持たない仕様と、エラー種別ごとの復旧レシピを解説します。
StopFailureフックとは
StopFailureは、ターンがレート制限や認証エラーなどのAPIエラーで終わったときに発火するフックです。正常にターンを終えたときはStopが発火し、APIエラーで終わったときはStopの代わりにStopFailureが発火します。ユーザーが割り込んで止めたときはStopは発火しません。
最大の特徴は、ブロックも継続もできないことです。Claude Codeはこのフックの出力とexit codeを無視します。唯一の例外はデスクトップ通知用のterminalSequenceフィールドで、これだけは処理されます。StopFailureはセッションの流れに介入する仕組みではなく、記録・通知・復旧アクションの起点として使うフックです。
このイベントはv2.1.78で追加されました。成功終了と失敗終了で別々の処理を持てるので、返答本文かエラー文字列かを自分で見分けるロジックは要りません。
Stopとの違い
同じ「ターン終了」でも、見ている対象が逆
Stop
Claudeがどう答えたかを見るフックです。decision: "block"で継続をブロックでき、テスト通過までの継続や/goalの基盤になります。last_assistant_messageにはClaudeの会話出力の本文が入ります。
StopFailure
Claudeが答えられなかったことを見るフックです。decision controlはなく、出力もexit codeも無視されます。last_assistant_messageにはAPIエラー文字列(例:"API Error: Rate limit reached")が入ります。
同じフィールド名でも中身の意味が正反対なので、Stop用のスクリプトを書き回すと、エラー文を「Claudeの返答」として扱う通知が出来上がります。
エラー種別とmatcherの落とし穴
StopFailureはerrorフィールドの値でmatcherを絞り込めます。取りうる値は12種類です。
errorの値 | 意味 |
|---|---|
rate_limit | 意味レート制限に達した |
overloaded | 意味APIが過負荷状態 |
authentication_failed | 意味認証に失敗した |
oauth_org_not_allowed | 意味OAuth組織の許可設定に引っかかった |
account_on_hold | 意味アカウントが保留状態 |
billing_error | 意味課金上のエラー |
invalid_request | 意味リクエストが不正 |
model_not_found | 意味指定モデルが見つからない |
server_error | 意味サーバー側のエラー |
max_output_tokens | 意味出力トークンの上限に達した |
cloud_credential_error | 意味クラウドの認証情報を読み込めなかった |
unknown | 意味上記以外 |
cloud_credential_errorをmatcherに使うには、v2.1.267以降が必要です。v2.1.267より前の版は、認証情報の読み込み失敗をserver_errorかunknownとして報告します。
入力JSONにはerrorのほかに、"429 Too Many Requests"のようなerror_detailsが入ることがあります。errorが同じでも詳細で出し分けたいときは、スクリプト側でerror_detailsを読みます。
ここで、他の大半のイベントとは違う仕様があります。StopFailureとFileChangedだけは、matcherの完全一致に使える文字が英数字・アンダースコア・|に限られます。ハイフン、スペース、カンマを混ぜると、matcherは正規表現として評価される側に回り、区切りとして働くのは|だけです。
"rate_limit|overloaded"は2つの値を完全一致で拾えます。"rate_limit, overloaded"ではカンマが区切りにならず、意図どおりに動きません。他のイベントのmatcherは|でも,でも区切れるので、コピーしてカンマ区切りで書くと、ここでだけ壊れます。
復旧レシピ — エラー種別ごとに対応を分ける
エラーの性質で必要なアクションは変わります。rate_limitとoverloadedは時間が経てば解消しうるエラーです。authentication_failedとbilling_errorは、設定や契約を直さない限り同じ失敗を繰り返します。matcherで振り分けて、別々のスクリプトを割り当てます。
#!/bin/bash
# authentication_failed / billing_error 向け: 対応者に即通知
input=$(cat)
error=$(echo "$input" | jq -r '.error')
message=$(echo "$input" | jq -r '.last_assistant_message // "unknown error"')
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg text "🚨 [$error] セッションが停止しました: $message" '{text: $text}')"
seq=$(printf '\033]777;notify;%s;%s\007' "Claude Code" "$error で停止")
jq -nc --arg seq "$seq" '{terminalSequence: $seq}'settings.json側はmatcherでこのスクリプトの対象を絞ります。
{
"hooks": {
"StopFailure": [
{
"matcher": "authentication_failed|billing_error",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify-hard-failure.sh"
}
]
}
]
}
}スクリプトの末尾2行がterminalSequenceを返す部分です。Claude Codeがこの文字列を端末へ書き出すのは、対話セッションでインターフェースが画面に出ている間だけです。-pフラグの非対話モードとAgent SDKでは、このフィールドは無視されます。人が張り付かないCIやスケジュール実行では、通知を届ける役はSlackへのcurlが担います。
通知スクリプトをasync: trueにすると、通知の送信中もClaudeの実行を待たせずに済みます。asyncはtype: "command"のフックだけで使える設定です。StopFailureにはdecision controlが元から無いので、非同期にしても失うものはありません。
一過性のエラー側は通知を抑えます。rate_limitやoverloadedのたびにSlackへアラートを飛ばすと、通知が日常のノイズになります。ログへの追記だけにして、同じエラーが短時間に続いたときだけ通知する設計が現実的です。
#!/bin/bash
# rate_limit / overloaded 向け: ログにだけ残す
input=$(cat)
error=$(echo "$input" | jq -r '.error')
echo "$(date -Iseconds) StopFailure: $error" >> "$CLAUDE_PROJECT_DIR/.claude/logs/stopfailure.log"
exit 0このスクリプトにサンプルの入力JSONを流すと、ログに1行追記されます。次の出力は、CLAUDE_PROJECT_DIRを一時ディレクトリにしてlog.shを実行した結果です。
echo '{"hook_event_name":"StopFailure","error":"rate_limit","error_details":"429 Too Many Requests","last_assistant_message":"API Error: Rate limit reached"}' | bash log.sh
cat .claude/logs/stopfailure.log2026-10-03T10:56:08+09:00 StopFailure: rate_limit通知側のスクリプトでlast_assistant_messageが無い(またはnullの)入力を流すと、messageはjqの//の右側にフォールバックしてunknown errorになります。terminalSequenceのJSONでは、制御文字が\u001bと\u0007にエスケープされます。
{"terminalSequence":"\u001b]777;notify;Claude Code;billing_error で停止\u0007"}ログ用をmatcher: "rate_limit|overloaded"に、通知用をmatcher: "authentication_failed|billing_error"に割り当てれば、1つのイベントの中で温度感の違う2系統の対応が共存します。
/goalはエラーの種類で動きが変わる
/goalコマンドは、セッション単位のプロンプト型Stopフックのラッパーです。継続の判断はStop側で行うので、StopFailureの範囲外にあります。ただしターンがAPIエラーで失敗したときの扱いは、/goalの公式ページにエラーの種類別で書かれています。
/goal中にターンが失敗したとき
ゴールが解除される
直さない限り消えないエラーです。認証失敗(Claude Codeが自分で認証情報を管理している場合)、クレジット残高の枯渇、自動コンパクションで解消できないコンテキスト溢れ、利用できないモデルの4種です。
Goal cleared after an unrecoverable errorという警告が出て、/goalの再設定が要ります。自動で再試行される
過負荷のサーバーや接続の切断のように、自然に解消しやすい失敗です。対話セッションのv2.1.269以降では
Goal still activeという通知が出て、3回の自動再試行のあとに一時停止へ切り替わります。一時停止される
再試行しても同じ結果になる失敗です。APIのレート制限、claude.aiの利用上限、フックがターンを終わらせたケースが当たり、
Goal pausedという通知が出ます。
デスクトップアプリやクラウドセッションのように、ホスト側が認証情報を管理している場合、認証失敗でもゴールは解除されません。ホストが自動でアクセスを復旧するためです。
Stopフックで自前のリトライ処理を書くときも、この区分が設計の目安になります。再試行して意味のあるエラーと、人が直すまで止まるエラーを分けずにループを組むと、後者で空回りします。StopFailureは、その分岐点に種別ごとのログと通知を足せる場所です。
Stop hookでテスト駆動開発を締めるレシピは「テストが通るまでタスクを完了させない」という成功パス側の設計です。StopFailureのレシピは、その手前で足が止まる失敗パス側を担います。
unknownと汎用ハンドラーの二重発火に注意する
errorがunknownのケースを拾いたいときは、matcherを省略した汎用ハンドラーを置きたくなります。matcherを省略したハンドラーはフィルタなしなので、StopFailureが起きるたびに毎回実行されます。
特定のerror値に絞ったハンドラーと汎用ハンドラーを両方登録すると、rate_limitのような値でも両方が発火し、通知が二重に飛びます。想定外のエラーだけを拾いたいなら、汎用ハンドラーのスクリプトの先頭でerrorフィールドを見て、個別ハンドラーが担当する値なら早期returnします。
タイムアウトの既定値
コマンドフックのタイムアウトは、command・http・mcp_toolの各タイプで既定600秒です。StopFailureもこの既定に従います。通知先のAPIが多少もたついても、途中で打ち切られる心配はほとんどありません。
ただし同期実行のまま重い処理(外部システムへのチケット起票など)を書くと、フックが終わるまでClaudeの実行が待たされます。Notificationフックを使った軽い通知と、StopFailureでの重い復旧アクションとで、同期・非同期を使い分けます。
よくあるつまずき
- exit 2やJSON出力でブロックしようとする:
StopFailureは出力とexit codeを無視するので、decisionやcontinueをどう書いても何も起きません。ロジックは通知や記録の副作用だけに絞ります - matcherにカンマやハイフンを使う: 区切りは
|だけです。カンマ・スペース・ハイフンを含めると正規表現として評価され、他のイベントで通っていた書き方が壊れます last_assistant_messageを会話ログとして扱う: このフィールドの中身はAPIエラー文字列であって、Claudeの返答ではありません。Stop用のテンプレートを流用すると意味の通らない通知になります- 割り込みで止めたときは
Stopが発火しない: 正常終了用のフックが動かないので、割り込みを検知する用途には使えません。中断の検知が必要なら別の仕組みを組みます terminalSequence以外のフィールドで何か制御できると期待する: 出力はterminalSequence以外すべて無視されます。-pとAgent SDKではterminalSequenceも効きません
最小構成から入れる
ログから通知へ広げる順番
- 1
ログだけ書く
errorの値をログへ追記するだけのハンドラーを1つ登録します。通知は付けません。 - 2
error値の分布を見る
数日分のログから、
rate_limit・overloaded・authentication_failedのどれが実際に出ているかを数えます。 - 3
対応が要る値だけ通知する
人の手が要る値を
|でつないだmatcherに切り出し、通知用のスクリプトを割り当てます。
StopFailureはブロックできない代わりに、仕分け(ログだけか、通知までか)をmatcherに任せられるフックです。
よくある質問
StopFailureでリトライを自動化できますか
フック自体にはターンをやり直す手段がありません。decision controlを持たないため、できるのは通知や記録などの副作用だけです。自動リトライが必要なら、フックの外側でclaude -pを再実行するような仕組みを組みます。
Notificationフックとの違いは何ですか
Notificationは、権限プロンプトやアイドル状態など、Claude Codeがユーザーの注意を必要とする場面で発火します。StopFailureはAPIエラーでターンが終わったときだけの専用イベントなので、対象が狭い分、エラー種別ごとの分岐がしやすくなります。
サブエージェントのAPIエラーでも発火しますか
フックのリファレンスのStopFailureの節には、サブエージェント内で発火するかどうかの記述がありません。サブエージェントの終了はSubagentStopが担い、その入力はagent_id・agent_type・agent_transcript_path・last_assistant_messageなどです。StopFailureのerrorに当たる項目はありません。サブエージェントの失敗を拾いたいときは、last_assistant_messageやagent_transcript_pathの中身を読む設計になります。