0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

レートリミットで止まった Claude Code を PROGRESS.md+自動リトライで復活させる

0
Posted at

前作 出力トークン監視でコストを可視化した話 では「使いすぎを止める」を作りました。今回は逆向き ―― 止まってしまったセッションを自動で起こし続けるための resume-on-ratelimit.sh の話です。

「長時間タスクをClaude Codeに任せて席を外したら、レートリミットで中断していた」を何度か踏みました。手動で再開するのは面倒で、かつ再開のタイミングが遅れると前の文脈を忘れてやり直しになります。この記事は、その問題をシェルスクリプト20行で解いた設計の記録です。

困りごと:長時間タスクがレートリミットで途中停止する

claude -p でヘッドレスタスクを流すと、プラン枠の5hブロックや7dブロックに触れた瞬間にプロセスが非ゼロで終了します。このとき困るのが2点。

  1. タスクが「どこまで終わったか」が消える ―― 単純に再実行すると先頭からやり直す
  2. 気づかない ―― ターミナルを見ていなければ「止まっていた」こと自体を知らない

解法として採用したのが、PROGRESS.md を「中断票」として使い、--continue リトライで引き継ぐパターンです。

PROGRESS.md を中断票にする

タスクを Claude に渡すとき、最初のプロンプトに「作業の区切りごとに PROGRESS.md を更新して」と指示します。Claude は自然にこれを守り、完了したステップ・残りステップ・メモをファイルに書き続けます。

# PROGRESS.md(Claude が自動更新)

## 完了
- [x] 依存パッケージの調査
- [x] ディレクトリ構造の設計

## 進行中
- [ ] 各モジュールのスケルトン生成

## 次に実行すること
モジュールAのテストを先に書く。設計メモは ./design.md 参照。

これがあると、再開時のプロンプトを "PROGRESS.mdを読んで中断した作業を続けて" という1行で済ませられます。Claude はファイルを読んで、やり残しから再開します。

resume-on-ratelimit.sh の全体

実ファイル(~/.claude/scripts/resume-on-ratelimit.sh)から全量を引用します。

#!/usr/bin/env bash
# レートリミットで止まったら自動で再開するラッパー
# 使い方: bash resume-on-ratelimit.sh [追加の指示]
#         bash resume-on-ratelimit.sh "PROGRESS.mdを読んで作業を再開して"

set -euo pipefail

WAIT_MINUTES=${WAIT_MINUTES:-5}
MAX_RETRIES=${MAX_RETRIES:-20}
TASK="${1:-PROGRESS.mdを読んで中断した作業を続けて。作業済みなら何もしない。}"
RETRY=0

notify() {
  osascript -e "display notification \"$1\" with title \"Claude Code\"" 2>/dev/null || true
}

echo "[$(date '+%H:%M')] 起動: $TASK"

while [ $RETRY -lt $MAX_RETRIES ]; do
  if [ $RETRY -eq 0 ]; then
    claude --dangerously-skip-permissions --continue -p "$TASK"
    EXIT=$?
  else
    echo "[$(date '+%H:%M')] リトライ $RETRY / $MAX_RETRIES"
    claude --dangerously-skip-permissions --continue -p "PROGRESS.mdを読んで中断した作業を続けて。"
    EXIT=$?
  fi

  if [ $EXIT -eq 0 ]; then
    echo "[$(date '+%H:%M')] 完了"
    notify "Claude Code: 作業完了"
    exit 0
  fi

  RETRY=$((RETRY + 1))
  echo "[$(date '+%H:%M')] レートリミット検出 (exit: $EXIT)。${WAIT_MINUTES}分後にリトライ..."
  notify "Claude Code: レートリミット。${WAIT_MINUTES}分後に再開します"
  sleep $((WAIT_MINUTES * 60))
done

echo "最大リトライ回数に達しました"
notify "Claude Code: 最大リトライ超過。手動確認してください"
exit 1

20行のループで構成全体が把握できます。

exit コードがレートリミット検出の唯一の口

claude コマンドは正常終了なら exit 0、それ以外なら非ゼロで返ります。現状、レートリミット時の exit コードは 0 以外であれば何でも判定に使えます。厳密に「これがレートリミット専用コード」という分離はしていません。

このスクリプトの前提は「非ゼロ = 待って再試行すべき状態」という割り切りです。完全に失敗しているタスクもリトライしてしまう副作用がありますが、MAX_RETRIES で上限を決めることで無限ループは防ぎます。

--dangerously-skip-permissions は無人運用で必須のフラグです。これを外すとツール使用のたびに確認プロンプトが出てブロックします。ただし、このフラグを使う場合はプロンプトで allowedTools を絞るか、信頼できるタスク専用にスコープを限定してください。

MAX_RETRIES ガード

デフォルトの MAX_RETRIES=20WAIT_MINUTES=5 で計算すると、最大 100分(20回×5分) 待ち続けます。7dブロックのリセットは1週間後なので、このパラメータで実用上は「数時間以内のレートリミット」をカバーできます。

環境変数で上書き可能なので、状況に合わせて調整できます。

# 10分ごとに最大5回だけ待つ例
WAIT_MINUTES=10 MAX_RETRIES=5 bash resume-on-ratelimit.sh "重いタスク"

MAX_RETRIES に達すると exit 1 で終了し、通知を出します。上位スクリプトやlaunchdでこの exit 1 を拾ってアラートに繋げることができます。

macOS 通知との組み合わせ

notify()osascript 経由で macOS のシステム通知を出します。

notify() {
  osascript -e "display notification \"$1\" with title \"Claude Code\"" 2>/dev/null || true
}

|| true を付けているのはスクリプトの set -e 環境でもNotificationが失敗(通知権限なし等)してもプロセスが落ちないようにするためです。

通知は3タイミングで出ます:レートリミット検出時・リトライ成功時・MAX_RETRIES 超過時。席を外していても Dock のバウンスで気づけます。

autopilot.sh との連携:launchd で3回起動

autopilot.sh は launchd(com.lily.autopilot.plist)から毎日2:00・5:00・23:00の3回起動します。

<key>StartCalendarInterval</key>
<array>
  <dict><key>Hour</key><integer>2</integer><key>Minute</key><integer>0</integer></dict>
  <dict><key>Hour</key><integer>5</integer><key>Minute</key><integer>0</integer></dict>
  <dict><key>Hour</key><integer>23</integer><key>Minute</key><integer>0</integer></dict>
</array>

深夜2時と23時は「長時間タスクをかけておく」時間帯、朝5時は「朝起きる前に進めておく」想定です。この起動間隔の外側をカバーするのが resume-on-ratelimit.sh で、手動で長時間タスクを流す時にラッパーとして使います

autopilot.sh 側は1 Phase(ターン数最大40)で自律的に止まる設計なのでリトライループは不要ですが、resume-on-ratelimit.sh はユーザーが「この処理が終わるまで回し続けてほしい」場面で使います。

踏んだ落とし穴

  • --continue なしで再実行すると新規セッションになる → 前のコンテキストが消え、PROGRESS.mdを読んでも「どのファイルを触っていたか」の記憶がない。必ず --continue をセットで使う
  • PROGRESS.mdを更新させる指示を忘れると中断票が空になる → 再開プロンプトが「作業済みなら何もしない」で空振りして終わる。初回プロンプトに必ず「区切りごとにPROGRESS.mdを更新」を入れる
  • 非ゼロ exit = レートリミットではないケースがある → 文法エラーや設定ミスで落ちた場合もリトライし続ける。MAX_RETRIESを超えたら手動確認が必要
  • set -euo pipefail|| true の組み合わせに注意EXIT=$? を取る前に別のコマンドが挟まると exit code が上書きされる。スクリプトを改変する場合は変数の取得タイミングに気をつける
  • macOS sleep中は launchd ジョブがスキップされるresume-on-ratelimit.sh の方はシェルのsleepループなので、スリープ中でも復帰後に再開する。autopilot側が必要なら pmset でwake on scheduleを設定する

まとめ

  • PROGRESS.md を中断票にする:Claude に「区切りごとに更新して」と指示するだけで、再開プロンプトが1行で済む
  • レートリミット検出は exit コード判定で十分:非ゼロ → 待つ → --continue で再開、のループを20行で組める
  • MAX_RETRIES ガードは必須:完全に詰まったタスクを無限にリトライしないために上限を設ける
  • macOS通知は osascript で1行|| true を忘れずに、set -e 環境でも通知失敗でプロセスが落ちない
  • launchd の autopilot とは役割が違う:autopilot は定時・自律改善ループ、resume-on-ratelimit は「今このタスクが終わるまで」の有人ジョブのラッパー

次回は、この自動化基盤が育ってきたことで逆に生まれた問題 ―― スキルとエージェントが増えすぎてコンテキスト注入が228KBになった話と、それを48KBに削った監査 を書きます。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?