{"slug": "how-i-auto-resume-a-rate-limited-claude-code-session-with-progress-md-and", "title": "How I Auto-Resume a Rate-Limited Claude Code Session with PROGRESS.md and Retries", "summary": "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.", "body_md": "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`\n\n.\n\nI 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.\n\nWhen you run a headless task with `claude -p`\n\n, the process exits non-zero the moment it hits your plan's 5h block or 7d block. Two things go wrong here.\n\nThe pattern I settled on: **use PROGRESS.md as a \"handoff ticket\" and carry the work forward with --continue retries.**\n\nWhen 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.\n\n```\n# PROGRESS.md（Claude が自動更新）\n\n## 完了\n- [x] 依存パッケージの調査\n- [x] ディレクトリ構造の設計\n\n## 進行中\n- [ ] 各モジュールのスケルトン生成\n\n## 次に実行すること\nモジュールAのテストを先に書く。設計メモは ./design.md 参照。\n```\n\nWith this in place, the restart prompt can be a single line: `\"Read PROGRESS.md and continue the interrupted work\"`\n\n. Claude reads the file and resumes from what's left.\n\nHere is the complete script quoted from the real file (`~/.claude/scripts/resume-on-ratelimit.sh`\n\n).\n\n``` bash\n#!/usr/bin/env bash\n# レートリミットで止まったら自動で再開するラッパー\n# 使い方: bash resume-on-ratelimit.sh [追加の指示]\n#         bash resume-on-ratelimit.sh \"PROGRESS.mdを読んで作業を再開して\"\n\nset -euo pipefail\n\nWAIT_MINUTES=${WAIT_MINUTES:-5}\nMAX_RETRIES=${MAX_RETRIES:-20}\nTASK=\"${1:-PROGRESS.mdを読んで中断した作業を続けて。作業済みなら何もしない。}\"\nRETRY=0\n\nnotify() {\n  osascript -e \"display notification \\\"$1\\\" with title \\\"Claude Code\\\"\" 2>/dev/null || true\n}\n\necho \"[$(date '+%H:%M')] 起動: $TASK\"\n\nwhile [ $RETRY -lt $MAX_RETRIES ]; do\n  if [ $RETRY -eq 0 ]; then\n    claude --dangerously-skip-permissions --continue -p \"$TASK\"\n    EXIT=$?\n  else\n    echo \"[$(date '+%H:%M')] リトライ $RETRY / $MAX_RETRIES\"\n    claude --dangerously-skip-permissions --continue -p \"PROGRESS.mdを読んで中断した作業を続けて。\"\n    EXIT=$?\n  fi\n\n  if [ $EXIT -eq 0 ]; then\n    echo \"[$(date '+%H:%M')] 完了\"\n    notify \"Claude Code: 作業完了\"\n    exit 0\n  fi\n\n  RETRY=$((RETRY + 1))\n  echo \"[$(date '+%H:%M')] レートリミット検出 (exit: $EXIT)。${WAIT_MINUTES}分後にリトライ...\"\n  notify \"Claude Code: レートリミット。${WAIT_MINUTES}分後に再開します\"\n  sleep $((WAIT_MINUTES * 60))\ndone\n\necho \"最大リトライ回数に達しました\"\nnotify \"Claude Code: 最大リトライ超過。手動確認してください\"\nexit 1\n```\n\nThe whole design fits in a 20-line loop.\n\nThe `claude`\n\ncommand 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.\"\n\nThe 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`\n\nprevents an infinite loop.\n\n`--dangerously-skip-permissions`\n\nis 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`\n\nin the prompt or scope it strictly to trusted, task-specific work.\n\nWith the defaults `MAX_RETRIES=20`\n\nand `WAIT_MINUTES=5`\n\n, 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.\"\n\nThey can be overridden via environment variables, so you can tune them to the situation.\n\n```\n# 10分ごとに最大5回だけ待つ例\nWAIT_MINUTES=10 MAX_RETRIES=5 bash resume-on-ratelimit.sh \"重いタスク\"\n```\n\nWhen `MAX_RETRIES`\n\nis reached, it exits with `exit 1`\n\nand fires a notification. A parent script or launchd can catch this `exit 1`\n\nand wire it up to an alert.\n\n`notify()`\n\nfires a macOS system notification via `osascript`\n\n.\n\n```\nnotify() {\n  osascript -e \"display notification \\\"$1\\\" with title \\\"Claude Code\\\"\" 2>/dev/null || true\n}\n```\n\nThe `|| true`\n\nis there so that even under the script's `set -e`\n\n, a failed notification (no notification permission, etc.) doesn't kill the process.\n\nNotifications fire at three moments: when a rate limit is detected, when a retry succeeds, and when `MAX_RETRIES`\n\nis exceeded. Even when you're away from your desk, the Dock bounce lets you notice.\n\n`autopilot.sh`\n\nis launched by launchd (`com.lily.autopilot.plist`\n\n) three times a day: at **2:00, 5:00, and 23:00**.\n\n```\n<key>StartCalendarInterval</key>\n<array>\n  <dict><key>Hour</key><integer>2</integer><key>Minute</key><integer>0</integer></dict>\n  <dict><key>Hour</key><integer>5</integer><key>Minute</key><integer>0</integer></dict>\n  <dict><key>Hour</key><integer>23</integer><key>Minute</key><integer>0</integer></dict>\n</array>\n```\n\n2 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`\n\n, which I **use as a wrapper when I manually run a long task**.\n\n`autopilot.sh`\n\nis designed to stop autonomously after one Phase (up to 40 turns), so it doesn't need a retry loop, but `resume-on-ratelimit.sh`\n\nis for the case where the user wants to \"keep it running until this process finishes.\"\n\n`--continue`\n\nstarts a new session`--continue`\n\ntogether with it.`set -euo pipefail`\n\nwith `|| true`\n\n`EXIT=$?`\n\n, the exit code gets overwritten. If you modify the script, be careful about `resume-on-ratelimit.sh`\n\n, 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`\n\n.`--continue`\n\n, a loop you can build in 20 lines`osascript`\n\n`|| true`\n\n, so a failed notification doesn't kill the process under `set -e`\n\nNext 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](https://zenn.dev/bokuwalily/articles/context-slimming).\n\n*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.\n\nFollow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*", "url": "https://wpnews.pro/news/how-i-auto-resume-a-rate-limited-claude-code-session-with-progress-md-and", "canonical_source": "https://dev.to/bokuwalily/how-i-auto-resume-a-rate-limited-claude-code-session-with-progressmd-and-retries-5cl5", "published_at": "2026-07-17 05:00:05+00:00", "updated_at": "2026-07-17 05:31:12.193823+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "large-language-models", "mlops"], "entities": ["Claude Code", "PROGRESS.md", "resume-on-ratelimit.sh"], "alternates": {"html": "https://wpnews.pro/news/how-i-auto-resume-a-rate-limited-claude-code-session-with-progress-md-and", "markdown": "https://wpnews.pro/news/how-i-auto-resume-a-rate-limited-claude-code-session-with-progress-md-and.md", "text": "https://wpnews.pro/news/how-i-auto-resume-a-rate-limited-claude-code-session-with-progress-md-and.txt", "jsonld": "https://wpnews.pro/news/how-i-auto-resume-a-rate-limited-claude-code-session-with-progress-md-and.jsonld"}}