← ブログに戻る

Claude Code の auto mode で Bash だけが止まる — safety classifier 障害の調査と「故障時だけ fail-open」hook 案

この記事を含む総合ガイド Claude Code 実戦運用ガイド

ある日、Claude Code の auto mode (自動承認モード) で作業していると、Bash ツールだけが断続的に実行できなくなりました。エラーはこうです。

Error: <model> is temporarily unavailable, so auto mode cannot determine
the safety of Bash right now. Wait briefly and then try this action again.
If it keeps failing, continue with other tasks that don't require this
action and come back to it later. Note: reading files, searching code,
and other read-only operations do not require the classifier and can
still be used.

ファイルの読み書きは動く。コード検索も動く。でも git status ひとつ打てない。リトライすると通ることもあれば、数分間まったく通らないこともある。この記事は、その原因を調べた記録と、「障害のときだけ挙動を変える」hook の設計案です。

何が起きていたのか

auto mode では、Claude が実行しようとするコマンドの安全性を判定する仕組みが 2 段構えになっています。

1 段目は settings.json の permission rules。Bash(git status) のような決定論的なルールにマッチすれば、そこで許可・拒否が決まります。

2 段目が今回の主役、サーバー側の safety classifier です。ルールで決まらなかったコマンドは、Anthropic 側で動く判定用モデルに送られ、安全なら自動許可、危険ならブロックされます。auto mode が「rm -rf を勝手に実行しない」でいられるのは、この判定があるからです。

問題は、この classifier が落ちたときの挙動です。判定できない場合、Claude Code は fail-closed — つまり安全側に倒して、対象ツールを全部ブロックします。「判定不能なら通さない」は安全設計としては正しい。ただ、classifier は上流のモデル可用性に依存しているため、ピーク時間帯に障害がバーストすると、その間 Bash がまるごと使えなくなります。

エラーメッセージの最後にある「read-only operations do not require the classifier」がヒントでした。Read や Grep のような読み取り専用ツールは classifier を経由しない設計なので、障害中も普通に動きます。副作用のあるツールだけが判定対象で、だから Bash だけが狙い撃ちで止まって見えたわけです。

観察できた範囲

その日のセッションログ (transcript) を後から grep してみると、事実関係はこうでした。

  • ブロックされたのは Bash と、一瞬ですが Write も。副作用のあるツールが判定対象という設計と一致します
  • Read・Edit・サブエージェント経由の Grep は終日無傷
  • auto mode を解除すると即座に回復。通常の許可プロンプト (人間が承認する方式) に戻るので、classifier の出番がなくなるためです

つまりローカル環境の問題ではなく、サーバー側の可用性障害でした。ローカルで直せるものは何もありません。

既知の障害だった

同じエラーメッセージで検索すると、公式リポジトリに報告が複数ありました。

  • #74949 (OPEN): ピーク時間帯に障害がバーストし、fail-closed が複合コマンドをほぼ全部塞ぐという報告。今回の体験そのものです
  • #68437 (CLOSED): 通常の生成は動くのに classifier だけ「temporarily unavailable」になる報告

興味深いのは #74949 の指摘で、&&| を含む複合コマンドは permission rules で静的に評価しきれないため、allow ルールをどれだけ書いても classifier 行きになるという点です。「よく使うコマンドを allow に登録しておけば障害を回避できる」という素朴な対策は、単発コマンドにしか効きません。私の使い方だと cd hoge && npm test のような複合コマンドが大半なので、これは効き目が薄い。

手元でできる対策を並べる

調べた範囲で、ユーザー側の選択肢は 3 つでした。

対策効果制約
permissions.allow に頻用コマンドを登録マッチすれば classifier を経由しない複合コマンドは静的評価できず classifier 行き
障害時だけ auto mode を手動解除確実に回復する気づいて切り替える手間。障害は断続的なので何度も往復する
PreToolUse hook で自前判定を返すhook が allow を返せば classifier に到達しない常時バイパスになる

3 つ目の hook 方式は一見きれいな解決に見えます。PreToolUse hook は tool 実行前に任意のスクリプトを走らせ、JSON で判定を返せる仕組みです。permissionDecision: "allow" を返せば permission flow 全体をバイパスして即実行、"deny" でブロック、"ask" で人間にエスカレート、何も返さなければ通常フローに進みます。

危険パターンの denylist だけ ask にして、それ以外を allow にするスクリプトを書けば、実質「ローカル classifier」になり、サーバー障害の影響を受けません。

ただしこれには根本的な問題があります。classifier が生きているときも素通りになることです。Anthropic 側の判定モデルは、手書きの denylist よりずっと文脈を読める。平常時にその判定を捨ててまで障害対策をするのは、本末転倒に感じました。

欲しいのは「平常時は classifier、障害時だけ自前判定」という条件分岐です。

「故障時だけ fail-open」にする方法はあるか

条件分岐を作るには、hook が「いま classifier が落ちている」と知る必要があります。classifier の稼働状態を照会する API はありません。詰みかけたのですが、間接的な検知手段がひとつありました。

hook は stdin で transcript_path を受け取ります。 これはセッションの会話ログ (JSONL) のパスで、classifier 障害でツールがブロックされると、その エラーメッセージ自体が transcript に記録されます。実際、冒頭のエラー文は当日の transcript から grep で取り出したものです。

つまり hook はこう動けます。

  1. 平常時: transcript にエラーの痕跡なし → 何も返さない。通常フロー (classifier) に進む。挙動変化ゼロ
  2. transcript の直近 10 分に「cannot determine the safety」エラーを発見 → 障害モード。denylist に該当しなければ allow を返す
  3. 10 分間エラーが出なければ、自動的に平常時の動作に戻る

障害の検知が「一度ブロックされたこと」に依存するので、最初の 1 発は必ず失敗します。ただ Claude はブロックされたコマンドをリトライするので、実運用では「2 回目から通る」挙動になります。fail-closed の初弾だけ食らって、以降のバーストは自前判定でしのぐ、という折衷案です。

設計スケッチ

未検証の設計段階ですが、hook スクリプトの骨格はこうなります。

#!/bin/bash
# classifier-outage-fallback.sh — PreToolUse (matcher: Bash)
set -euo pipefail
input=$(cat)
transcript=$(jq -r '.transcript_path' <<<"$input")
cmd=$(jq -r '.tool_input.command // empty' <<<"$input")

outage() {
  # transcript の直近行に 10 分以内の classifier 停止エラーがあるか
  tail -n 400 "$transcript" 2>/dev/null \
    | jq -c 'select(.timestamp? and
        ((.timestamp | fromdateiso8601) > (now - 600)))' 2>/dev/null \
    | grep -q 'cannot determine the safety'
}

dangerous() {
  grep -Eq 'rm +-rf|--force|--no-verify|reset +--hard|-fd?D' <<<"$cmd"
}

if ! outage; then
  exit 0  # 平常時: 判定を返さず classifier に委ねる
fi

if dangerous; then
  jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse",
    permissionDecision: "ask",
    permissionDecisionReason: "classifier outage: 危険パターンのため手動確認"}}'
else
  jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse",
    permissionDecision: "allow",
    permissionDecisionReason: "classifier outage: denylist 非該当のため暫定許可"}}'
fi

登録は settings.json に書きます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/classifier-outage-fallback.sh"
          }
        ]
      }
    ]
  }
}

導入するなら、まず判定ログだけ残して介入しない warn-only で数日走らせ、誤判定がないことを見てから有効化する段階導入を考えています。

弱点も先に書いておく

この設計には自覚している穴が 3 つあります。

エラー文言へのマッチが脆い。 検知は「cannot determine the safety」という文字列頼みなので、Claude Code のバージョンアップで文言が変わると障害を検知できなくなります。ただしその場合は単に平常動作 (fail-closed) に戻るだけで、危険側に倒れることはありません。壊れ方が安全なのは救いです。

transcript の書き込みは非同期。 公式ドキュメントに「transcript ファイルは非同期に書かれ、メモリ上の会話より遅れることがある」と明記されています。直近のエラーがまだファイルに反映されていない瞬間があり得るので、検知は数秒〜十数秒遅れる可能性があります。障害バーストは分単位で続くので実害は小さいと見ていますが、初弾に加えて 2 発目も食らうケースはありそうです。

denylist の判定品質がそのまま安全性になる。 障害モード中は手書きの正規表現が classifier の代役です。rm -rf--force のような明白なパターンは拾えますが、文脈依存の危険 (たとえば重要ファイルへの上書きリダイレクト) までは見抜けません。障害中だけの暫定運用と割り切り、denylist は保守的に太らせておく前提です。

導入はまだしない

ここまで設計しておいてなんですが、この hook はまだ導入していません。理由は単純で、障害の頻度が今後も続くか分からないからです。

サーバー側の問題はサーバー側で直るのが一番よくて、issue #68437 が CLOSED になっているように、改善は進んでいるようです。まずは CLI を最新版に更新して数日様子を見る。それでも障害に日常的にぶつかるようなら、warn-only から段階導入する。対策の導入自体にもコスト (メンテナンス、denylist の保守、バージョンアップ追従) がかかるので、発生頻度を見てから払うか決めます。

障害に一日付き合わされた腹いせに設計まで済ませてしまいましたが、一番の学びは仕組みの側にありました。auto mode の安全判定はサーバー側のモデルに依存していて、fail-closed で守られている。読み取り系ツールが judgment-free なのも、ブロック時のエラーメッセージに回避のヒントが書いてあるのも、知ってから見ると筋の通った設計です。落ちなければ、ですが。

まとめ

  • auto mode の Bash ブロックは、サーバー側 safety classifier の障害 + fail-closed 設計が原因。ローカルでは直せない
  • 読み取り専用ツールは classifier を経由しないため障害中も動く。auto mode を解除すれば人間承認に戻り回復する
  • allow ルールでの回避は複合コマンドに効かない (#74949)
  • PreToolUse hook + transcript 監視で「障害時だけ fail-open」の条件分岐は作れる。平常時の挙動は不変
  • ただし導入は CLI 更新後の再発観察を見てから。対策にもコストはかかる
ハーネス・エンジニアリング 関連書籍 ハーネス・エンジニアリング ハーネスエンジニアリング 入門 | AGENTS.md 設計・hooks 実装・AIエージェント運用の体系書 書籍ページを見る →