結論
expect コマンドで claude をラップし、API Error: 500 が出たら自動で「続けてください」を送信する。これだけで、会話コンテキストを失わずに自動リカバリできる。実装は記事の後半にあるので、先に動かしたい人はそちらへ。
はじめに
Claude Code を使っていると、こんな経験はないだろうか。
長時間タスクを依頼して席を外し、戻ってみたら何も終わっていない。ターミナルには API Error: 500 のメッセージが出たまま、Claude は止まっている。
これはアメリカ時間の夜間(日本時間の昼間)にサーバー負荷が高くなりやすく、確率的に 500 エラーが発生するためだ。Claude Code はエラー時に応答を止めるが、UI 上は「考え中」に見えるためユーザーが気づきにくい。
この記事では、この問題に対してどう向き合うかの設計思想と、ターミナル派向けの自動リカバリ実装を紹介する。
研修現場で見えた「止まり待ち」問題
プログラミング研修の講師をしていると、受講生が Claude Code を使ってコードを書いているうちに、これと似た状況を頻繁に目にする。
授業中に「このコードをリファクタリングして」と Claude に依頼して演習の別の説明に集中し、数分後に戻ると画面が止まっている。エラーメッセージに気づかないまま「Claude が遅い」と感じて待ち続けるか、ブラウザをリロードして会話ごと消してしまうか。どちらにせよ時間を無駄にする。
Claude Code のターミナル版を使いこなしている受講生はまだ少ないが、これから増えるはずだ。「止まったことに気づく仕組み」と「止まっても自動で再開する仕組み」は、AI を使った開発スタイルの基礎体力として身につけておいて損はない。
問題の整理
まず「何が困るか」を整理する。
- API Error 500 は確率的に発生する(サーバー側の問題なので根本解決できない)
- 止まっても UI 上は気づきにくい(「考え中」に見える)
- 離席中・就寝中に長時間タスクを依頼した場合、気づかず作業が宙ぶらりんになる
「確率的に発生する」というのがポイントで、毎回起きるわけではない。だから「自分の操作が悪かったのかも」と思いがちだが、サーバー側の一時障害なので待っても自然回復することが多い。問題は、その「待ち」の間に人間が何もできないことだ。
平日昼間(日本時間)はアメリカのサーバーが夜間メンテ明けで負荷が低い。逆に日本の昼食後〜午後にかけての時間帯は、アメリカ東海岸の夜間に重なり負荷が上がりやすい。Claude Code をヘビーに使っているなら、この時間帯に 500 エラーが増えることを経験で知っているはずだ。
検討した選択肢
1. Stop hook で通知
Claude Code には Stop イベントの hook がある。止まった瞬間にデスクトップ通知を出すことはできる。
"hooks": {
"Stop": [{
"type": "command",
"command": "osascript -e 'display notification \"Claude が停止\" with title \"Claude Code\" sound name \"Funk\"'"
}]
}
限界は「気づく」だけという点だ。離席中・就寝中に通知が来ても意味はない。「知ること」と「対処できること」は別の話で、この手段は前者しか解決しない。
2. watchdog でプロセス再起動
launchd や cron でプロセスを監視して、死んでいたら再起動する案。
問題はプロセスを再起動しても会話コンテキストが消えることだ。「何をすべきか知らない Claude」が立ち上がるだけで、タスクは最初からやり直しになる。コンテキストを復元するためのプロンプトを用意しておく手もあるが、その管理コストが馬鹿にならない。
3. サブエージェントに委譲
長時間タスクをサブエージェントに任せて、メインセッションへの影響を減らす案。
サブエージェントは起動のたびにトークン注入コストが発生する。長時間タスク全体を委譲するのは非現実的で、そもそも「サブエージェントが 500 で止まったらどうするか」という問題は残る。
4. expect でターミナル出力を監視してキック ← 採用
expect コマンドを使って claude をラップし、API Error: 500 が出たら自動で「続けてください」を送信する。
会話コンテキストを保持したまま再開できる点が決め手だ。プロセスは同じまま、エラー文字列を検知したら入力を自動送信するだけなので、Claude は「タスクの途中」から再開できる。
実装
設計上の判断
トリガーを 500 に限定する理由:
| エラーコード | 意味 | キックすべきか |
|---|---|---|
| 400 | リクエスト側の問題 | ❌ 繰り返しても無意味 |
| 429 | Rate limit | ❌ キックすると悪化 |
| 500 | サーバー側の一時障害 | ✅ 時間を置いてリトライで回復する可能性が高い |
キック制御の考え方:
- キック間隔 60 秒: サーバー負荷が高い時に連打しない
- 連続エラー上限 3 回: 「キックしたのに 500 が続いた回数」をカウント。3 回で停止
- 正常応答が来たらカウントリセット
60 秒インターバルは「サーバーが一時的に詰まっているなら少し待てば回復する」という経験則から来ている。30 秒にすると回復前に連打してしまうことがあった。3 回上限は「それ以上続くなら違う種類の問題」という判断だ。
コード
#!/usr/bin/env expect
# claude-watchdog.sh — API Error 500 を検知して自動キックする expect ラッパー
set kick_interval 60
set max_consecutive_errors 3
set kick_message "続けてください"
set consecutive_errors 0
set last_kick_time 0
set timeout -1
# CLAUDE_CMD 環境変数でコマンドを切り替え可能(テスト用)
set claude_cmd "claude"
if {[info exists env(CLAUDE_CMD)]} {
set claude_cmd $env(CLAUDE_CMD)
}
if {[info exists argv] && [llength $argv] > 0} {
eval spawn $claude_cmd $argv
} else {
spawn $claude_cmd
}
proc try_kick {} {
global kick_interval max_consecutive_errors kick_message
global consecutive_errors last_kick_time
set now [clock seconds]
set elapsed [expr {$now - $last_kick_time}]
if {$elapsed < $kick_interval} {
send_error "\[watchdog\] キック間隔未満 (${elapsed}s) — スキップ\n"
return
}
incr consecutive_errors
if {$consecutive_errors > $max_consecutive_errors} {
send_error "\[watchdog\] 連続エラー上限超過 — キック停止\n"
exit 1
}
send_error "\[watchdog\] 500 検知 (連続 ${consecutive_errors}/${max_consecutive_errors}) — キック\n"
set last_kick_time $now
send "$kick_message\r"
}
expect_background {
-re {API Error: 500[^\r\n]*\r?\n} {
try_kick
exp_continue
}
-re {[^\r\n]+\r?\n} {
if {$consecutive_errors > 0} {
send_error "\[watchdog\] 正常応答 — カウントリセット\n"
set consecutive_errors 0
}
exp_continue
}
}
interact
テスト方法
本番環境に負荷をかけずにテストするためのモックスクリプト。
#!/bin/bash
# mock-claude.sh — テスト用モック
INTERVAL=${FAST:+10}
INTERVAL=${INTERVAL:-65}
echo "作業中..."
sleep 2
echo 'API Error: 500 {"type":"error"} · check status.claude.com'
sleep $INTERVAL
echo "作業中(キック受信後)..."
sleep 2
echo 'API Error: 500 {"type":"error"} · check status.claude.com'
sleep $INTERVAL
echo "完了"
実行:
# FAST=1 で 10 秒間隔(通常 65 秒の代わり)
FAST=1 CLAUDE_CMD=./mock-claude.sh ./claude-watchdog.sh
使い方
# ~/.zshrc に追加
alias claude='~/.claude/scripts/claude-watchdog.sh'
重要な制約:VSCode 拡張には効かない
ここが最も重要な点だ。
この watchdog はターミナルで claude コマンドを叩いた場合にのみ有効。
VSCode の Claude Code 拡張は独自のプロセスで動いており、~/.zshrc のエイリアスは適用されない。VSCode 拡張が 500 で止まった場合、この仕組みは無関係だ。
研修で受講生に伝えるとき、ここを最初に確認する。「VSCode で Claude を使っています」という場合は、この記事の手順は使えない。ターミナル版を使っているかどうかで、できることが大きく変わる。
VSCode 拡張ユーザーへの現実的な答えは今のところない。Anthropic 側の改善待ちというのが正直なところ。止まったら手動で「続けてください」と送るしかない。
まとめ
| 手段 | 効果 | 限界 |
|---|---|---|
| Stop hook 通知 | 止まったことに気づける | 離席中は無意味 |
| プロセス再起動 | プロセスは復活する | コンテキストが消える |
| expect watchdog | コンテキスト保持で自動再開 | ターミナル派のみ有効 |
| VSCode 拡張 | — | 外から介入できない |
500 エラーはサーバー側の問題なので根本解決はできない。できることは「止まった時の損失を最小化する」設計を積み重ねることだ。
ターミナルで claude を使っている人には expect watchdog が有効な選択肢になる。VSCode 拡張ユーザーは、今のところ手動再送信が唯一の対策だ。
研修現場で AI ツールを教えるときに気づいたのは、「ツールが止まった理由」を理解しているかどうかで、受講生の対処が大きく変わるという点だ。「500 はサーバー側の問題。待てば回復することが多い。自分の操作は悪くない」と知っているだけで、無駄にやり直すことが減る。
この記事で紹介した watchdog は仕組みとして役立つが、「なぜこの設計か」を理解した上で使うと、自分のユースケースに合わせてパラメータを調整できる。60 秒インターバルを変えたいなら kick_interval を、上限回数を変えたいなら max_consecutive_errors を調整すればいい。
