ankuro.dev
← ブログ一覧に戻る
【Claude Code中級編 #10】StopFailureとPermissionDenied——エラー・拒否への対処
2026-03-31#Claude Code#AI#Hooks#エラー処理#自動化

【Claude Code中級編 #10】StopFailureとPermissionDenied——エラー・拒否への対処

Claude Code Hooksには7つのイベントがある。そのうち「エラーや拒否に対処する」専用のイベントが2つある——StopFailurePermissionDenied

この記事でわかること:

  • Stop / StopFailure / PermissionDenied の3つの違い
  • StopFailure のinputデータの形式と使い方(実測済み)
  • PermissionDenied が発火する条件とauto modeの仕組み

3つのStop系イベントの違い

イベント 発火タイミング 用途
Stop Claudeが正常に応答を終了したとき サマリー生成・完了通知
StopFailure APIエラーでターンが終了したとき エラーログ・Slack通知・リトライ起動
PermissionDenied auto mode分類器がツールを拒否したとき 拒否の記録・retry制御

判断基準:

正常終了後に何かしたい       → Stop
APIエラー時に自動で対処したい → StopFailure
auto modeの拒否に割り込みたい → PermissionDenied

StopFailure——APIエラー時に自動で対処する

StopFailureStop と対になるイベント。Claudeが正常に応答を返せずAPIエラーでターンが終了したとき発火する。

inputに含まれるデータ(実測値)

{
  "session_id": "fbcff89f-d402-4f8a-81c0-5b5e527a2916",
  "transcript_path": "/Users/yourname/.claude/projects/.../session.jsonl",
  "cwd": "/path/to/project",
  "hook_event_name": "StopFailure",
  "error": "invalid_request",
  "last_assistant_message": "There's an issue with the selected model..."
}

Stop との差分:

フィールド Stop StopFailure
hook_event_name "Stop" "StopFailure"
error なし あり(エラー種別)
stop_hook_active false なし
last_assistant_message あり あり

error フィールドに入る値は "invalid_request""api_error" など、APIが返したエラー種別。

典型的な用途

① エラーログを残す

#!/usr/bin/env python3
import json, sys, os
from datetime import datetime

input_data = json.loads(sys.stdin.read())
error = input_data.get("error", "unknown")
message = input_data.get("last_assistant_message", "")
cwd = input_data.get("cwd", "")

log_path = os.path.expanduser("~/.claude/stop_failure.log")
with open(log_path, "a") as f:
    f.write(f"{datetime.now():%Y-%m-%d %H:%M:%S}  error={error}  cwd={cwd}\n")
    if message:
        f.write(f"  last_message: {message[:100]}\n")

② Slack通知を送る

#!/usr/bin/env python3
import json, sys, os, urllib.request

input_data = json.loads(sys.stdin.read())
error = input_data.get("error", "unknown")
cwd = input_data.get("cwd", "")

webhook_url = os.environ.get("SLACK_WEBHOOK_URL", "")
if webhook_url:
    payload = json.dumps({
        "text": f":warning: Claude Code APIエラー\nerror: `{error}`\ncwd: `{cwd}`"
    }).encode()
    req = urllib.request.Request(webhook_url, data=payload,
                                  headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req)

settings.jsonの設定

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/on_stop.py"
          }
        ]
      }
    ],
    "StopFailure": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/on_stop_failure.py"
          }
        ]
      }
    ]
  }
}

StopStopFailure は別イベントなので、両方設定しても干渉しない。


PermissionDenied——auto mode分類器の拒否に割り込む

PermissionDenied--permission-mode auto(auto mode)専用のフック(v2.1.88で追加)。

auto modeとは

Claude Codeをエージェント・CI/CDパイプラインなどで非対話的に動かすとき、ツール実行のたびにユーザーへの確認ダイアログが出ると処理が止まる。--permission-mode auto を指定すると、auto mode分類器がツール実行の可否を自動で判定する。

# auto modeで実行
claude -p "リリース手順を実行して" --permission-mode auto

分類器は各ツール呼び出しを評価し、allow(自動承認)・soft_deny(確認が必要)・実行拒否のいずれかを返す。

PermissionDeniedが発火するとき

Claudeがツール呼び出しを試みる
  ↓
auto mode分類器が評価
  ↓
分類器が拒否 → PermissionDeniedフックが発火
  ↓
フックが処理(ログ記録・条件判定など)
  ↓
retry: true を返すとClaudeが再試行できる

retry: truePermissionDenied 固有の返り値。フックがこれを返すと、Claudeが別の方法でアプローチし直す。

スクリプト例

inputデータの詳細構造はauto modeが有効な環境での確認が必要。まずはログ記録から始めるのが安全。

#!/usr/bin/env python3
import json, sys, os
from datetime import datetime

input_data = json.loads(sys.stdin.read())

# inputの内容をすべてログに記録して構造を確認する
log_path = os.path.expanduser("~/.claude/permission_denied.log")
with open(log_path, "a") as f:
    f.write(f"{datetime.now():%Y-%m-%d %H:%M:%S}\n")
    f.write(json.dumps(input_data, ensure_ascii=False, indent=2))
    f.write("\n\n")

# retry: true を返すとClaudeが再試行する(条件はinput確認後に実装)
# print(json.dumps({"retry": True}))
sys.exit(0)

注意:auto modeのアカウント要件

--permission-mode auto はすべての環境で有効ではない。実際の動作確認では、tengu_auto_mode_config.enabled === "disabled" というcircuit breakerによって無効化されていることが確認された。

auto mode disabled: tengu_auto_mode_config.enabled === "disabled" (circuit breaker)
canEnterAuto=false

auto modeが有効でない環境では、PermissionDenied フックは発火しない。claude auto-mode config コマンドで現在の設定を確認できる。

claude auto-mode config     # 現在の設定を確認
claude auto-mode defaults   # デフォルトルールを確認

まとめ

やりたいこと 使うもの
APIエラーをSlackに通知する StopFailure + Webhook呼び出し
エラー発生をログに残す StopFailure + ファイル書き込み
正常終了とエラー終了を使い分ける Stop + StopFailure を別々に設定
auto modeの拒否をハンドリングする PermissionDenied + retry: true

設計の原則:

  1. StopFailureStop の対になるイベント——正常終了とエラー終了で処理を分けられる
  2. StopFailure のinputには error フィールドにエラー種別が入る
  3. PermissionDenied--permission-mode auto が有効な環境でのみ発火する
  4. retry: truePermissionDenied 専用の返り値——Claudeに再試行させたいときに使う

関連記事


第9回:Hook設定を最適化する——ifフィールドでgit操作・危険コマンドだけ反応させる第11回:コンテキスト管理の技術——トークンを無駄にしない