cd /news/developer-tools/how-i-auto-resume-a-rate-limited-cla… · home topics developer-tools article
[ARTICLE · art-63190] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

How I Auto-Resume a Rate-Limited Claude Code Session with PROGRESS.md and Retries

A developer created a 20-line shell script that automatically resumes rate-limited Claude Code sessions using a PROGRESS.md handoff file and retries. The script waits 5 minutes between up to 20 retries, carrying forward interrupted work by instructing Claude to read PROGRESS.md and continue.

read5 min views50 publishedJul 17, 2026

My previous post, Making costs visible by monitoring output tokens, 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.


## 完了
- [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

).

#!/usr/bin/env bash

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, narrowallowedTools

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.

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.

<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 AM and 11 PM are the "set a long task running" hours; 5 AM is meant to "make progress before I wake up." What covers the gaps outside these launch windows is resume-on-ratelimit.sh

, which I use as a wrapper when I manually run a long task.

autopilot.sh

is designed to stop autonomously after one Phase (up to 40 turns), so it doesn't need a retry loop, but resume-on-ratelimit.sh

is for the case where the user wants to "keep it running until this process finishes."

--continue

starts a new session--continue

together with it.set -euo pipefail

with || true

EXIT=$?

, the exit code gets overwritten. If you modify the script, be careful about resume-on-ratelimit.sh

, being a shell sleep loop, resumes after waking even if it was asleep. If you need the autopilot side, configure wake on schedule with pmset

.--continue

, a loop you can build in 20 linesosascript

|| true

, so a failed notification doesn't kill the process under set -e

Next time I'll write about a problem that ironically emerged because this automation foundation grew — the story of how skills and agents multiplied until context injection hit 228 KB, and the audit that trimmed it to 48 KB.

*Written by Lily — I ship iOS apps and automate my content stack with Claude Code.

Follow along: Portfolio · X · GitHub*

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-i-auto-resume-a-…] indexed:0 read:5min 2026-07-17 ·