調査日: 2026-04-02
対象: ~/.claude/scripts/rate-limit-recovery.sh(StopFailure hook)
rate-limitに引っかかった作業セッションが自動復帰できていない問題を調査・修正した。 4つの根本原因を特定し、すべて修正済み。
at デーモンが動いてない(最大の原因)| 項目 | 内容 |
|---|---|
| 症状 | リカバリスクリプトが予定時刻に実行されない |
| メカニズム | at コマンド自体は成功しジョブ登録される → atrun(実行デーモン)が停止中で永遠に実行されない |
| 影響 | nohup+sleepフォールバックは at がエラーの時だけ発動 → at が成功扱いなのでフォールバックされない |
| 証拠 | atq に 04/01 22:15 のジョブが04/02 12:51時点でまだ残存 |
修正: at を完全廃止。常に nohup+sleep を使用。
設計判断 — なぜ launchd ではなく nohup+sleep か:
launchd + ワンショットスクリプトは ADR-012 で ntfy通知爆撃インシデントを起こした前科がある(KeepAlive誤設定リスク)caffeinate + ロックなし運用のため、スリープ・ログアウトによるプロセス消失リスクなしnohup+sleep は前提条件(常時稼働Mac)下では最もシンプルで確実な選択| 項目 | 内容 |
|---|---|
| 症状 | 実際は45分〜1時間で済むのに5時間待ちにフォールバック |
| メカニズム | last_assistant_message に "You've hit your limit · resets 11pm (Asia/Tokyo)" が入っているが、スクリプトはこの値を読んでいなかった |
| 旧動作 | capture-pane の Xh(@XXam/pm) パターンのみ → マッチしない → デフォルト5h |
| 証拠 | ログ: Default 5h. Recovery at 03:15(実際のresetは11pm = 23:00、わずか45分後) |
修正: 復帰時刻の抽出を多段フォールバック構成に変更。構造化データ(resets_at)を最優先とし、自由文パースは2番手に降格。日付跨ぎ判定(算出時刻が過去なら翌日)も追加。
| 項目 | 内容 |
|---|---|
| 症状 | 新しいrate-limitが検知されても「already pending」で弾かれる |
| メカニズム | 原因1で実行されなかったリカバリのpending flagが残存 → 同セッションの新規リカバリをブロック |
| 証拠 | 04/02 09:55: Recovery already pending for main (pending_main_1775049336_75582) — 11時間前のフラグ |
修正: pending flagを拡張。PID・予定時刻・セッションID・execスクリプトパスをメタデータとして保存。有効判定は「PID生存 AND 予定時刻が未来」の場合のみ — それ以外(PID死亡 OR 予定時刻過去)は全てstaleとして自動クリーンアップ。
| 項目 | 内容 |
|---|---|
| 症状 | 同時に2つのリカバリスクリプトが生成される |
| メカニズム | サブエージェントのStopFailure(agent_id付き)も親と同時に発火 |
| 証拠 | 04/01 22:15:36 に2つのhook: keys差分に agent_id, agent_type の有無 |
修正:
agent_id が存在するStopFailureは即座にスキップ(サブエージェント除外)pending_${SESSION}) の mkdir で排他制御。同一セッションの2つのhookが同時に走っても、先に mkdir した方だけが通過し、後続は即座にスキップ| ファイル | 変更内容 |
|---|---|
~/.claude/scripts/rate-limit-recovery.sh |
メインhookスクリプト(全面改修) |
~/.claude/scripts/rate-limit-recovery-template.sh |
リカバリ実行テンプレート(新規) |
~/.claude/scripts/test-rate-limit-recovery.sh |
テストスイート(新規、21テスト) |
旧: メインスクリプト内でヒアドキュメントでリカバリスクリプトを生成(変数展開が複雑)
新: テンプレートファイル + BSD sed 置換でリカバリスクリプトを生成(保守性向上)
|(パス中の / との衝突回避)& \ のエスケープ不要resets_at フィールド(構造化データ — 最も信頼性が高い)last_assistant_message の resets XXam/pm パース(自由文、表記揺れリスクあり)Xh(@XXam/pm) ステータスバー表示※ 4b/4cでは日付跨ぎ判定あり(算出時刻が過去なら翌日と判断)
PASS: 'resets 11pm (Asia/Tokyo)' → 23:00
PASS: 'resets 11am (Asia/Tokyo)' → 11:00
PASS: 'resets 3am (Asia/Tokyo)' → 03:00
PASS: 'resets 12am (Asia/Tokyo)' → 00:00
PASS: 'resets 12pm (Asia/Tokyo)' → 12:00
PASS: resets_at + last_msg → resets_at used first
PASS: no resets_at → last_msg fallback
PASS: capture-pane pattern extraction works ('3h(@11pm)')
PASS: no resets_at + no last_msg + no capture-pane → default
PASS: first mkdir succeeds (lock acquired)
PASS: second mkdir blocked (race condition prevented)
PASS: PID生存 + 予定時刻が未来 → 既存リカバリ有効、新規をブロック
PASS: PID死亡 + 予定時刻が過去 → stale判定、クリーンアップ後に新規実行
PASS: PID死亡 + 予定時刻が未来 → stale判定(プロセスが異常終了した場合)
残存プレースホルダー: 0
生成スクリプトシンタックス: OK
Main script: OK
Template: OK
PASS: /bin/bash (3.2) での構文チェック通過
PASS: BSD sed デリミタ | での置換正常動作
PASS: date -j -f (BSD date) でのパース正常動作
PASS: stat -f %m (BSD stat) でのタイムスタンプ取得正常動作
次回rate-limitが発生した時に以下を確認:
ログ確認: tail -f ~/.claude/logs/rate-limit-recovery.log
Reset from JSON resets_at: または Reset from last_assistant_message: が出ること(4a/4bが機能してる)Scheduled via nohup+sleep が出ること(atではなくnohup)Ignoring subagent StopFailure が出ること(race condition対策)プロセス確認: cat ~/.claude/rate-limit-recovery/pending_*/pid でPIDを確認し、ps -p <PID> -o pid,ppid,etime,command= で生存確認
復帰時刻: 通知の復帰予定時刻が妥当か(5h固定ではなく、actual resetに近い値)
自動復帰: 予定時刻にntfy通知が来るか + セッションが再開されるか
# 1. 待機中のsleepプロセスを停止
for f in ~/.claude/rate-limit-recovery/pending_*/pid; do
pid=$(cat "$f" 2>/dev/null) && kill "$pid" 2>/dev/null
done
# 2. pending flagとexecスクリプトを全削除
rm -rf ~/.claude/rate-limit-recovery/pending_* ~/.claude/rate-limit-recovery/exec_*
settings.json の hooks.StopFailure からこのスクリプトのエントリを削除し、セッションを再起動。
ntfyの送信失敗はリカバリ処理をブロックしない(&>/dev/null & でバックグラウンド実行)。通知未着でもリカバリは実行される。ログ(~/.claude/logs/rate-limit-recovery.log)で確認可能。
at デーモン (atrun) を有効化すれば at も使えるようになるが、nohup+sleepで十分なので放置launchd LaunchAgent への移行を再検討| Round | 結果 | 指摘数 | 主な指摘 |
|---|---|---|---|
| R1 | NG | MAJOR×5, MINOR×3 | launchd不採用の根拠不足、優先順位逆転、stale判定が時間固定、BSD sed互換未記載、テスト不足、race condition冪等性、ps grep雑、ロールバック手順なし |
| R2 | NG | CRITICAL×1, WARNING×2 | mkdir排他がセッション固定名ではなく毎回一意(race condition未解決)、テストファイル不在、旧コメント残存 |
| R3 | NG | WARNING×1, INFO×1 | ntfy通知がロック取得前(二重通知リスク)、stale判定のレポート表現不一致 |
| R4 | NG | WARNING×1 | レポートにcapture-paneテストPASSと記載するも実テスト不在 |
| R5 | LGTM | なし | 全指摘解消。実装・テスト・レポートの三点が整合 |