How I Auto-Resume a Rate-Limited Claude Code Session with PROGRESS.md and Retries A developer created a 20-line shell script, resume-on-ratelimit.sh, that automatically resumes a Claude Code session when it stalls due to rate limits. The script uses a PROGRESS.md file as a handoff ticket and retries the task with configurable wait times and retry limits, enabling unattended long-running tasks. My previous post, Making costs visible by monitoring output tokens https://zenn.dev/bokuwalily/articles/output-token-sentinel , was about stopping overuse. This time it's the opposite problem: how to keep waking up a session that has stalled — the story of resume-on-ratelimit.sh . I got burned more than once by handing a long task to Claude Code, stepping away from my desk, and coming back to find it had halted on a rate limit. Restarting by hand is tedious, and when the restart is delayed, it forgets the earlier context and starts over. This article is a record of how I solved that problem in 20 lines of shell script. When you run a headless task with claude -p , the process exits non-zero the moment it hits your plan's 5h block or 7d block. Two things go wrong here. The pattern I settled on: use PROGRESS.md as a "handoff ticket" and carry the work forward with --continue retries. When I hand a task to Claude, I include an instruction in the first prompt: "update PROGRESS.md at each work boundary." Claude naturally follows this, continuously writing completed steps, remaining steps, and notes into the file. PROGRESS.md(Claude が自動更新) 完了 - x 依存パッケージの調査 - x ディレクトリ構造の設計 進行中 - 各モジュールのスケルトン生成 次に実行すること モジュールAのテストを先に書く。設計メモは ./design.md 参照。 With this in place, the restart prompt can be a single line: "Read PROGRESS.md and continue the interrupted work" . Claude reads the file and resumes from what's left. Here is the complete script quoted from the real file ~/.claude/scripts/resume-on-ratelimit.sh . bash /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 The whole design fits in a 20-line loop. The claude command returns exit 0 on a clean finish and non-zero otherwise. As it stands, any exit code other than 0 on a rate limit can be used for the check. I do not strictly separate out "this is the rate-limit-specific code." The script's premise is a deliberate simplification: non-zero = a state that should wait and retry . It has the side effect of also retrying tasks that are completely failing, but capping the count with MAX RETRIES prevents an infinite loop. --dangerously-skip-permissions is essential for unattended operation. Without it, a confirmation prompt appears on every tool use and blocks. That said, when you use this flag, narrow allowedTools in the prompt or scope it strictly to trusted, task-specific work. With the defaults MAX RETRIES=20 and WAIT MINUTES=5 , it waits for at most 100 minutes 20 × 5 min . A 7d block resets a week later, so in practice these parameters cover "rate limits within a few hours." They can be overridden via environment variables, so you can tune them to the situation. 10分ごとに最大5回だけ待つ例 WAIT MINUTES=10 MAX RETRIES=5 bash resume-on-ratelimit.sh "重いタスク" When MAX RETRIES is reached, it exits with exit 1 and fires a notification. A parent script or launchd can catch this exit 1 and wire it up to an alert. notify fires a macOS system notification via osascript . notify { osascript -e "display notification \"$1\" with title \"Claude Code\"" 2 /dev/null || true } The || true is there so that even under the script's set -e , a failed notification no notification permission, etc. doesn't kill the process. Notifications fire at three moments: when a rate limit is detected, when a retry succeeds, and when MAX RETRIES is exceeded. Even when you're away from your desk, the Dock bounce lets you notice. autopilot.sh is launched by launchd com.lily.autopilot.plist three times a day: at 2:00, 5:00, and 23:00 .