# Rate Limits Cost Me a Whole Night of Work — Here's the 46-Line Script That Fixed It

> Source: <https://dev.to/bokuwalily/rate-limits-cost-me-a-whole-night-of-work-heres-the-46-line-script-that-fixed-it-7o3>
> Published: 2026-08-23 05:00:06+00:00

Every heavy Claude Code user eventually hits the wall: the 5-hour rate limit block.

I'm Lily. I lost my income overnight to a company-initiated layoff, rebuilt my setup starting that same month, and got back to ¥1.2M/month in revenue within six months.

To most users, a rate limit block looks like a simple "pause." Grab a coffee, wait it out. The reality is far more destructive. Your Claude Code session dies during the block, and **thousands of tokens' worth of accumulated working context get wiped**. Which files you read and how far, why you picked that architecture, what the next step was — all of it evaporates.

When you come back, Claude greets you from a blank slate: "What would you like to do?"

Back in university, I grew a ¥100K/month income into ¥600K/month by stacking multiple gigs, and one thing became clear along the way: **systems** scale, not **skills**. Skills only grow in proportion to the hours you personally work; systems keep running while you sleep. The same mindset applies to Claude Code: **the right answer isn't "I manage the limit," it's "the environment gets past the limit."**

`resume-on-ratelimit.sh`

is that idea in script form. It watches the exit code of the Claude Code process, and when it detects an abnormal exit caused by a rate limit, it waits a configured number of minutes and then auto-resumes with `claude --continue`

, inheriting the previous session. It retries up to 20 times, which works out to **5 minutes × 20 = 100 minutes of fully unattended retrying**.

You need to understand why `--continue`

alone isn't enough.

`--continue`

is a flag that means "carry over the immediately preceding session." If the session is alive, conversation history is restored. But when the session has been fully severed during a 5-hour block, what `--continue`

can carry over is a **record of the last exchange** — not the **context of intent**, meaning "what I was about to do next."

Even after resuming the conversation, Claude won't say "I'm in the middle of this task, so next I'll do X." Because the only one who knew that was "the Claude inside the working session," and that instance is already gone.

That's exactly why **you need PROGRESS.md**.

PROGRESS.md is a file where you continuously write task progress in human-readable text. If you commit to updating "where I am," "what's done," and "next action" at every step, the Claude that resumes can grasp "what I was doing" instantly just by reading that file. The script's retry prompt is hardcoded to `"PROGRESS.mdを読んで中断した作業を続けて。"`

("Read PROGRESS.md and continue the interrupted work.") — line 27. That's a design that presupposes PROGRESS.md exists.

When you actually run this setup, **the work is finished when you wake up, without a human doing anything during the block**. Check the time Claude stopped and you'll sometimes find a log showing it hit the rate limit around 2 AM, retried three times in 5-minute increments at 2:05 and 2:10, resumed on the third attempt, and kept running straight through to 6 AM. Stack up enough experiences like that and the idea of "removing the ceiling on how much you can work" really sinks in.

Writing code, writing articles, running a scraper — whatever the task, the biggest bottleneck when using Claude Code is "you being in front of the computer."

That's what `resume-on-ratelimit.sh`

solves. Even when you're not at the screen, even when the rate limit hits, **the process keeps going on its own**. The reason automation revenue makes up a growing share of that ¥1.2M/month is that I stacked up these "runs without human hands" systems one at a time.

Current Claude Code lets you skip all permission prompts with the `--dangerously-skip-permissions`

flag. Without this flag, on resume Claude asks "may I write to this file?" and the process stalls with nobody there to answer. This flag is mandatory for unattended continuation (used on both line 23 and line 27).

Let's look at the script's structure from above first, then go through each part of the code.

``` bash
$ bash resume-on-ratelimit.sh "PROGRESS.mdを読んで作業を再開して"
        │
        ▼
   RETRY=0 の判定
        │
        ▼ (RETRY=0)
claude --dangerously-skip-permissions \
       --continue -p "$TASK"           ← 初回: 引数のタスク文を使用
        │
        ├─ exit 0 ──→ ✅ 完了ログ + macOS通知 → exit 0
        │
        └─ exit ≠ 0
              │
              ▼
         RETRY++ (1へ)
         "レートリミット検出。5分後にリトライ..." をログ出力
         macOS通知: "レートリミット。5分後に再開します"
         sleep 300   (= 5 × 60秒)
              │
              ▼ (RETRY=1以降)
claude --dangerously-skip-permissions \
       --continue \
       -p "PROGRESS.mdを読んで中断した作業を続けて。"  ← 2回目以降は固定文
              │
              ├─ exit 0 ──→ ✅ 完了
              │
              └─ exit ≠ 0
                    │
                    ▼
               RETRY < 20 なら再びsleep→リトライ
               RETRY = 20 なら ❌ 最大リトライ超過 + 通知 → exit 1
```

**Using different prompts for the first run and for retries** is the core design decision of this script.

The first run uses `$TASK`

(the instruction passed as an argument). For example, you can hand it a concrete task like `"ECサイトのスクレイパーを完成させて"`

("Finish the e-commerce site scraper"). From the second attempt onward, the argument is ignored and the prompt is fixed to `"PROGRESS.mdを読んで中断した作業を続けて。"`

(line 27). Why fix it? Because **the Claude that resumes is already partway through the task**. Sending the initial instruction "finish the e-commerce site scraper" again risks Claude "trying to start over from scratch." Wording that makes it read PROGRESS.md and do "the continuation" preserves continuity of intent.

``` 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() {
  # macOS通知
  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
    # 初回は --continue でセッションを引き継ぐ
    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
```

46 lines. Zero dependencies. No installation. Copy it, `chmod +x`

, done.

`set -euo pipefail`

(line 8)

`-e`

exits immediately on command failure, `-u`

treats references to undefined variables as errors, and `-o pipefail`

propagates errors from the middle of a pipe. Remove these three and you get the "an error happened but it kept going anyway" problem. Rate limit detection is done via exit codes, so a broken exit-code regime causes false detections.

`WAIT_MINUTES=${WAIT_MINUTES:-5}`

(line 8)

Overridable via environment variable. `WAIT_MINUTES=10 bash resume-on-ratelimit.sh`

switches it to a 10-minute wait. It's a design that lets you change behavior without editing the script, which is handy when calling it from launchd or cron.

`MAX_RETRIES=${MAX_RETRIES:-20}`

(line 9)

Default 20 attempts × 5 minutes = up to 100 minutes of automatic recovery attempts. If it hasn't come back after 100 minutes, it's likely a genuine error or a different problem (network outage, full disk, etc.), so at that point it returns `exit 1`

and notifies the human.

**The default value of TASK="${1:-...}" (line 10)**

When run without arguments, the default is `"PROGRESS.mdを読んで中断した作業を続けて。作業済みなら何もしない。"`

("Read PROGRESS.md and continue the interrupted work. If it's already done, do nothing."). That trailing "if it's already done, do nothing" matters — it prevents duplicate work if you re-run while PROGRESS.md is already in a completed state.

**The notify() function (lines 13–16)**

A native macOS notification via `osascript`

. It swallows errors with `2>/dev/null || true`

so the whole script doesn't die on Linux or in environments where notifications are disabled. This is the design judgment that notifications are an optional feature in this script, and a notification failure must not stop the main processing.

**Exit code check (lines 31–34)**

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

Claude Code returns `exit 0`

on normal completion and non-zero on rate limits or abnormal termination. This script treats "non-zero = rate limit" and retries without question. Strictly speaking, other errors (auth failure, file I/O errors) can also return non-zero. But in realistic operation, when you run long sessions overnight, the errors you run into are overwhelmingly rate limits, so this simplification causes no practical problems.

**Timestamped loop logging (line 26)**

```
echo "[$(date '+%H:%M')] リトライ $RETRY / $MAX_RETRIES"
```

This is what pays off when you check the next morning. Looking at the terminal log leaves you a timeline like `[02:05] リトライ 1 / 20`

→ `[02:10] リトライ 2 / 20`

→ `[02:15] 完了`

, so you can see at a glance "what time it hit the block and what time it recovered." Redirect the log to a file to save it and you can analyze it later.

```
bash resume-on-ratelimit.sh "スクレイパーを完成させて" >> ~/logs/claude-session.log 2>&1
```

Once these logs pile up, your own Claude usage patterns become visible (which hours you're most likely to hit blocks, which tasks turn into long sessions). Once you can see the data, you can improve it.

From here I dig into the deeper design question of "why write it this way." Line-by-line explanation was covered in the previous chapter, so here I'll narrow in on **structural intent** and **the conventions for writing PROGRESS.md**.

```
RETRY=0

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))
  ...
done
```

Starting from `RETRY=0`

and judging the exit condition with `$RETRY -lt $MAX_RETRIES`

(less than) is intentional design.

If you made it `$RETRY -le $MAX_RETRIES`

(less than or equal), you'd actually get `MAX_RETRIES + 1`

attempts. With `MAX_RETRIES=20`

, that's 21. When the number and the actual behavior diverge, checking "how many times did it retry" in the logs becomes annoying. Writing it with `-lt`

establishes the intuitive correspondence ** MAX_RETRIES=20 → exactly 20 attempts**.

There's another important point. The increment equivalent to `RETRY++`

is only executed **after the success check**.

```
試行1(RETRY=0) → 失敗 → RETRY=1, sleep
試行2(RETRY=1) → 失敗 → RETRY=2, sleep
...
試行20(RETRY=19) → 失敗 → RETRY=20, sleep → ループ条件 20<20 が偽 → 脱出 → exit 1
```

Even if attempt 2 succeeds, `RETRY`

stays at 2 and it does `exit 0`

. `RETRY`

is a label for "which attempt are we on," and it isn't used for judging success or failure. This simple separation of roles makes reading the logs during debugging easier.

`EXIT=$?`

Is Captured Immediately

```
claude --dangerously-skip-permissions --continue -p "$TASK"
EXIT=$?
```

`$?`

is the shell's special variable holding "the exit code of the most recently executed command." **It gets overwritten the instant the next command runs.**

Write it like this and it breaks:

```
# NG: echo が $? を上書きする
claude --dangerously-skip-permissions --continue -p "$TASK"
echo "claudeが終わりました"  # この echo が $? を 0 にする
if [ $? -eq 0 ]; then ...  # 常に 0 になってしまう
```

The rule that `EXIT=$?`

goes on the very next line after the command is Bash basics, but under `set -euo pipefail`

there's an additional consideration. With `set -e`

in effect, the behavior "exit immediately if a command returns non-zero" kicks in. In this script, rather than `EXIT=$?`

executing right after `claude`

returns non-zero, the command is executed inside the evaluation context of the `while`

loop, which suppresses `set -e`

's instant-death trigger.

This is less an intentional design than **a byproduct of bash behavior**, but as a result it works safely. If you wanted to design it intentionally, you could write `set +e; claude ...; EXIT=$?; set -e`

to explicitly disable `e`

temporarily. My own script prioritizes simplicity and stays as is.

By the script's design, the Claude on a retry always goes to read PROGRESS.md (because the line 27 prompt is a fixed string). Which means **if PROGRESS.md isn't written, or its contents are vague, the Claude that resumes is left at a loss**.

Here's the minimal format I arrived at:

```
# PROGRESS

## 現在地
スクレイパーのページネーション処理を実装中。
`~/dev/scraper/scraper.py` の `fetch_page()` 関数、101行目まで書いた。
次のステップ: `next_page_url` の抽出ロジックを追加する。

## 完了済み
- [x] 認証トークンの取得 (`auth.py`)
- [x] 1ページ目の商品一覧取得
- [x] 商品データのCSV書き出し

## 次のアクション（最重要）
1. `fetch_page()` に `next_page_url` 抽出を追加
2. ループで全ページ取得
3. 重複URLの除外

## 注意事項
- APIレートリミットは1秒1リクエスト。`time.sleep(1)` 必須
- 認証トークンは `~/.env` の `API_TOKEN`
```

The key is **writing the "next action" section in the most concrete terms**. "Continue the implementation" isn't enough — unless you write down "what to do at which line of which file," the resumed Claude hesitates at the very first step. Include the file path and function name and Claude will go open that file first, which is smooth.

The timing for updating PROGRESS.md is "every time you complete one thing." If you try to batch two or three updates together, the update can lag behind at the moment you get blocked and leave a stale state behind. Overwriting frequently is the safe approach.

`WAIT_MINUTES`

and `MAX_RETRIES`

can be overridden by environment variables (lines 8–9). This design pays off **when calling from launchd (macOS's scheduled-execution daemon)**.

When calling the script from a launchd plist, arguments go in the `ProgramArguments`

array, but environment variables go in the `EnvironmentVariables`

section.

```
<key>EnvironmentVariables</key>
<dict>
  <key>WAIT_MINUTES</key>
  <string>10</string>
  <key>MAX_RETRIES</key>
  <string>12</string>
  <key>HOME</key>
  <string>/Users/（あなたのユーザー名）</string>
</dict>
```

You can change the wait to 10 minutes and max retries to 12 without touching the script itself. "For long overnight tasks, `WAIT_MINUTES=10`

puts less load on the API; for short tasks, `WAIT_MINUTES=3`

retries quickly" — that split is one config line away.

Some things you only learn by running it. Here I record the moments when "it should work in theory" broke, in the order of symptom → cause → fix.

**Symptom**: Blocked at 2 AM, auto-resumed, but when I woke up it had stopped after writing "please tell me what to start with."

**Cause**: I hadn't created PROGRESS.md for that task. The retry prompt is `"PROGRESS.mdを読んで中断した作業を続けて。"`

, but when the file doesn't exist, Claude asks the human back: "I can't find the file. What work should I continue?" `--dangerously-skip-permissions`

is a flag that skips permission dialogs, but it can't prevent a stop when Claude internally decides "I lack the information to make the next judgment."

**Fix**: I made it a rule to always prepare PROGRESS.md before launching any task. Adding `[ -f PROGRESS.md ] || echo "PROGRESS.mdがありません" && exit 1`

before the script launch is also effective, but I judged the habit of "write PROGRESS.md first, then start the script" to be more reliable, and that's still what I do.

**Symptom**: It works when run manually from the terminal, but running it via a launchd timer fails at the first line with `command not found: claude`

and exits with code 127.

**Cause**: The shell launchd starts doesn't read the user's `.zshrc`

or `.bash_profile`

, so `PATH`

is bare. Claude Code was installed via nvm, so the binary only exists at `~/.nvm/versions/node/v24.13.0/bin/`

. That location isn't in launchd's PATH, hence "what's claude?"

**Fix**: Explicitly write PATH in the plist's `EnvironmentVariables`

.

```
<key>PATH</key>
<string>/Users/（あなたのユーザー名）/.nvm/versions/node/v24.13.0/bin:/usr/local/bin:/usr/bin:/bin</string>
```

Alternatively, you can add `export PATH="$HOME/.nvm/versions/node/v24.13.0/bin:$PATH"`

at the top of the script. Writing it on the script side removes the launchd dependency, so it's more portable. However, the path changes when you bump the Node.js version, so you have to update it each time. Which one you choose is a tradeoff, but I use the launchd-side plist approach, because it's self-contained in plist changes and keeps management in one place.

**Symptom**: Launching the script produced an error like `claude: invalid option -- 't'`

and it died instantly.

**Cause**: When I copy-pasted the task string for the argument, tab characters used for indentation slipped in. In the shell, tabs can be treated as word-splitting delimiters during argument expansion, so `$TASK`

in `-p "$TASK"`

was being split on tabs and passed to claude as multiple arguments.

**Fix**: Always wrap the task string in double quotes as `"$TASK"`

when passing it (already done inside the script), and don't include tabs in strings passed as arguments. When passing from the command line, rather than using `$'...'`

notation or heredocs, a simple one-line string with no newlines or tabs is more reliable.

By adopting the practice of writing complex instructions in PROGRESS.md and keeping the script argument to a simple string like `"PROGRESS.mdを読んで作業を再開して"`

, this problem essentially stopped happening. The principle of **keep complex information in files, keep arguments simple** also matches the overall design of the script.

**Symptom**: I woke up, the terminal window was closed, and I had absolutely no idea whether the work completed, whether it kept retrying, or how many hours it ran.

**Cause**: I hadn't saved the script's output to a file, and I hadn't specified launchd's StandardOutPath. Terminal session logs disappear when you close the window (depending on terminal settings).

**Fix**:

When launching from the terminal, add a redirect.

```
bash ~/scripts/resume-on-ratelimit.sh "スクレイパーを完成させて" \
  >> ~/logs/claude-$(date '+%Y%m%d').log 2>&1
```

When launching from launchd, add this to the plist.

```
<key>StandardOutPath</key>
<string>/Users/（あなたのユーザー名）/logs/claude-resume.log</string>
<key>StandardErrorPath</key>
<string>/Users/（あなたのユーザー名）/logs/claude-resume-error.log</string>
```

Ever since logs started being kept, my first action in the morning became "check the log."

```
[02:03] 起動: スクレイパーを完成させて
[02:47] レートリミット検出 (exit: 1)。5分後にリトライ...
[02:52] リトライ 1 / 20
[03:37] レートリミット検出 (exit: 1)。5分後にリトライ...
[03:42] リトライ 2 / 20
[04:21] 完了
```

From this log you can read off the fact that "it was blocked twice, around 2 AM and in the 3 o'clock hour, and completed on the third attempt in the 4 o'clock hour." As logs accumulate, a pattern like "my Claude usage tends to get blocked between 2 and 4 AM" also becomes visible. Next time you can make a data-based improvement: start at 1 AM to give yourself more headroom.

`--continue`

didn't carry over "the previous session"
**Symptom**: The resumed Claude was supposedly using the `--continue`

flag, yet it started with a fresh greeting: "Hello. What can I help you with?"

**Cause**: Claude Code's `--continue`

carries over "the last session." But "the last session" is **tied to the current directory**. If the current directory differed between when it got blocked and when it resumed (e.g., launchd's WorkingDirectory differing from the directory of a manual run), `--continue`

references a different session (or a nonexistent one) and starts as a new conversation.

**Fix**: Always specify `WorkingDirectory`

in the launchd plist.

```
<key>WorkingDirectory</key>
<string>/Users/（あなたのユーザー名）/dev/（プロジェクト名）</string>
```

For manual runs, I made it a rule to `cd`

into the project directory before launching the script. And what keeps this problem from being catastrophic is **the existence of PROGRESS.md**. Even if `--continue`

fails and it becomes a new session, as long as PROGRESS.md exists, the prompt `"PROGRESS.mdを読んで中断した作業を続けて"`

lets Claude acquire the correct context. `--continue`

is ultimately "an aid for faster recovery"; the real workhorse for context restoration is PROGRESS.md. Once I understood this, I stopped panicking even in situations where `--continue`

doesn't work.

The previous chapter covered five sticking points. Here I'll line up the rest of the landmines in one go. If you spot an item you've already done, deal with it on the spot.

**Closing the terminal window wiped out every process**

When you quit the terminal app, `SIGHUP`

goes out to all of its child processes. `resume-on-ratelimit.sh`

dies the instant it receives that. This is the situation where you launch at 2 AM, crawl into bed, and wake up to "nothing changed." There are two solutions; pick one. Creating a tmux session with `tmux new -s claude`

and launching the script inside it is easier to manage. The next morning, `tmux attach -t claude`

puts you right back into the continuing log. If tmux isn't installed in your environment, launching in the background with `nohup bash ~/scripts/resume-on-ratelimit.sh "タスク" >> ~/logs/claude-$(date '+%Y%m%d').log 2>&1 &`

gets you an equivalent effect.

**The Mac went to sleep and the sleep command stopped**

macOS enters system sleep once idle time exceeds a threshold. When that happens, the count of a running `sleep 300`

(5 minutes × 60 seconds) freezes. Because the count resumes after waking, I experienced "retry in 5 minutes" turning into "retry in 2 hours." The countermeasure is to prefix it: `caffeinate -i bash ~/scripts/resume-on-ratelimit.sh "タスク"`

. `caffeinate -i`

suppresses system sleep only while the command is running, so it releases automatically when the script ends. When launching from launchd it's unnecessary, since launchd itself is designed to coexist with sleep.

**Launching multiple projects at once crossed the wires on --continue**

`claude --continue`

carries over "the most recently launched session." If you run project A and project B concurrently in separate tabs, "the last session" gets overwritten the instant one of the Claudes exits. If the other one then launches a retry right after, you get the worst-case scenario: it inherits a session from a different project. I actually experienced a session that was in the middle of scraper development picking up landing-page-production context and starting to output HTML. When running several in parallel, either prepare a dedicated plist per project in launchd and separate the WorkingDirectory, or design it to run serially. If you absolutely must launch concurrently, handle it by cramming the full task into the `TASK`

argument and eliminating the dependency on PROGRESS.md.**It logged "完了" with exit 0, but the work was only half done**

`exit 0 = complete`

(the check on line 31). But `exit 0`

isn't only what Claude returns "when it finished the task." It's also `exit 0`

when it cuts off the conversation with "the instructions are ambiguous and I can't make a judgment, so I'm ending here" or "there are items requiring confirmation. I'm waiting for your response." I had a situation where the log said `[03:14] 完了`

at 3:14 AM, and when I woke up, four items remained in PROGRESS.md's "next action." The countermeasure is two-layered. ① Append `"完了したらPROGRESS.mdの最終行にCOMPLETED: [完了日時]と書いてください"`

("When done, write COMPLETED: [completion timestamp] on the last line of PROGRESS.md") to the end of the TASK argument. ② For the morning check, don't rely only on the `[HH:MM] 完了`

log — verify the `COMPLETED:`

record with `tail -1 PROGRESS.md`

. These two prevent "exit 0 false positives" for practical purposes.**PROGRESS.md bloated and squeezed the context**

Run the same project for a week and the "completed" section reaches several hundred lines. Claude reads this file in full on every resume, so the bigger the file gets, the more context is consumed and the more the accuracy of subsequent work drops. In practice, I feel a slight drop in post-resume work accuracy once `wc -l PROGRESS.md`

goes past 400 lines. Resolve it by resetting weekly with `mv PROGRESS.md PROGRESS_archive_$(date '+%Y%m%d').md`

and rewriting a new PROGRESS.md with only the currently in-progress portion. You almost never re-read the archives, so just saving and leaving them is enough.

**macOS notifications never arrived once**

`osascript -e "display notification..."`

goes through the macOS Notification Center. They won't arrive if the terminal app's notification permission is off, or if Focus mode is enabled. Line 15 of the script swallows errors with `2>/dev/null || true`

, so the script body doesn't stop even if a notification fails. If you rely on notifications operationally, go to System Settings → Notifications → and turn the terminal app's notification permission "on." Whether the "レートリミット。5分後に再開します" notification arrives lets you confirm in real time that the script is working as intended.

**A single quote slipped into the TASK argument and caused a shell error**

If a copy-pasted task string contains a Japanese-style '（single quote）' or an English `'`

, the shell's argument interpretation breaks. Even when the argument is wrapped in double quotes, like `bash resume-on-ratelimit.sh "ユーザーの'入力'を"`

, single quotes inside double quotes can cause problems depending on the shell. The safest design is to **keep arguments short and simple, and write all complex instructions in PROGRESS.md**. Keep the argument to symbol-free Japanese like `"PROGRESS.mdを読んで作業を再開して"`

, and avoid writing code or commands into the argument. The default value on line 10 of the script follows this design too.

**I didn't check the current directory before launching**

`--continue`

carries over the session tied to the current directory (detailed in the previous chapter). On top of that, since PROGRESS.md is referenced by a path relative to the current directory, launching while you forgot to `cd`

produces "PROGRESS.md not found." Running `~/scripts/resume-on-ratelimit.sh`

while not in `~/dev/プロジェクト/`

is a recipe for accidents. Adopt either the habit of checking `pwd`

before launching the script, or adding `cd ~/dev/プロジェクト名 || exit 1`

at the top of the script.

These are the rules that stuck after 3+ months of production use. You don't need to adopt all of them at once — starting with "just #1 and #3" and adding the rest in order as you hit problems is the realistic approach.

**1. Always verify PROGRESS.md exists before launching**

This is the base. Using this script on a task with no PROGRESS.md just means Claude stops on the second attempt onward (the fixed prompt on line 27). Putting your launch command in the following form prevents launching without the file.

```
[ -f PROGRESS.md ] && bash ~/scripts/resume-on-ratelimit.sh "$1" || echo "PROGRESS.mdがありません"
```

Alternatively, just fixing the habit of "write PROGRESS.md immediately before launching the script" is sufficient.

**2. Write PROGRESS.md's "next action" down to file path, function name, and line number**

Not "continue the scraper" but "add `next_page_url`

extraction logic at line 101 of the `fetch_page()`

function in `~/dev/scraper/scraper.py`

." Claude reads this file right after resuming and decides its first action. Given a file path it opens that file; given a function name it looks for it. The more information per line, the closer the hesitation time before the first action gets to zero.

**3. Save logs to date-stamped files and always check them the next morning**

```
bash ~/scripts/resume-on-ratelimit.sh "タスク" \
  >> ~/logs/claude-$(date '+%Y%m%d').log 2>&1
```

This leaves a timeline like `[02:47] レートリミット検出 (exit: 1)。5分後にリトライ...`

→ `[02:52] リトライ 1 / 20`

→ `[04:21] 完了`

. As logs pile up, patterns emerge — "blocks tend to happen between 2 and 4 AM," "this type of task takes 3 hours" — and you can apply them to your next launch plan. Improvement without data is guesswork.

**4. When using launchd, state PATH, HOME, and WorkingDirectory explicitly in the plist**

The shell launchd starts doesn't read `.zshrc`

. Missing just these three triggers `command not found`

and "the session isn't carried over" problems simultaneously. Include nvm's bin directory in PATH, and set HOME to the user's home directory. Writing the project's absolute path in WorkingDirectory makes `--continue`

's session binding work correctly too.

**5. Protect long terminal runs with tmux**

```
tmux new -s claude
# セッション内で起動
bash ~/scripts/resume-on-ratelimit.sh "タスク" >> ~/logs/claude-$(date '+%Y%m%d').log 2>&1
# Ctrl+b d でデタッチして就寝
# 翌朝
tmux attach -t claude
```

Even if you close the terminal window, the tmux session persists as long as the server is alive. Re-attach in the morning and the real-time tail of the log is right there.

**6. Combine with caffeinate -i to prevent Mac sleep**

Mandatory if you're running overnight by any method other than launchd.

```
caffeinate -i bash ~/scripts/resume-on-ratelimit.sh "タスク" \
  >> ~/logs/claude-$(date '+%Y%m%d').log 2>&1
```

When the script exits, `caffeinate`

exits automatically too, so you never have the accident of "leaving sleep prevention switched on."

**7. Use 5–10 minutes for WAIT_MINUTES; never go below 2**

A rate limit is a block against "API usage within a given time window." During the block period, it keeps failing no matter how many times you retry. Even if you retry rapidly at 2-minute intervals, resumption doesn't come any sooner — you just accumulate failure logs. For long overnight tasks, specifying `WAIT_MINUTES=10 bash ~/scripts/resume-on-ratelimit.sh "タスク"`

flattens the peaks of API usage and reduces block frequency the next day. For short daytime tasks, the 5-minute default is plenty.

**8. Make it a convention to have the completion sign written into PROGRESS.md**

Append `"完了したらPROGRESS.mdの最終行にCOMPLETED: [完了日時]と記録してください"`

to the end of the TASK argument. Then the morning check is one command: `tail -1 PROGRESS.md`

. If you use `exit 0`

alone as evidence of completion, you'll be slow to notice when Claude cut the conversation short. Cross-checking PROGRESS.md's record against the log's `[HH:MM] 完了`

is the shortest verification route.

**9. Manage parallel projects with launchd + dedicated plists**

Launching multiple tabs manually at the same time causes `--continue`

crosstalk. If you prepare a launchd plist per project and `launchctl load`

them, the WorkingDirectory is independent, so no crosstalk. Standardizing the plist naming convention as `com.自分の名前.プロジェクト名.plist`

lets you list managed tasks with `launchctl list | grep 自分の名前`

.

**10. Archive PROGRESS.md weekly to keep it thin**

Archive a week's accumulation with `mv PROGRESS.md PROGRESS_archive_$(date '+%Y%m%d').md`

and rewrite a new `PROGRESS.md`

with only the in-progress portion. Aim for under 50 lines. Past 400 lines, post-resume work accuracy noticeably drops. You almost never re-read the archives, so just dropping them into `~/dev/プロジェクト名/archive/`

is enough.

**11. Set MAX_RETRIES and WAIT_MINUTES by working backward from your completion deadline**

If you have a constraint like "I want the task done by 6 AM," do this calculation in advance.

`MAX_RETRIES=5`

`WAIT_MINUTES=5`

(25 minutes of automatic recovery grace in total)The default 20 × 5 minutes = 100 minutes is generous headroom designed for long tasks, but for a short task it means "wait 100 minutes, then give up." Build the habit of adjusting the numbers to the nature of the task.

**12. Auto-delete logs after 30 days**

Clean up old logs once a month with cron.

```
# crontab -e で追加
0 3 1 * * find ~/logs/claude-*.log -mtime +30 -delete
```

Thirty days of accumulated logs adds up to tens of MB to a few GB. You're essentially never going to look back at logs older than 30 days for improvement purposes, so auto-deletion is fine. Add the single line above with `crontab -e`

and it runs automatically at 3 AM on the 1st of every month.

`resume-on-ratelimit.sh`

is 46 lines. Two environment variables, one function, one while loop. Zero dependencies — copy it and `chmod +x`

and it runs.

Even so, having this script versus not having it fundamentally changes the shape of your work. Because the premise that "nothing progresses unless you're in front of the computer" collapses.

The first thing I decided in the month I got laid off and went to zero was: "don't increase the amount I move, increase the amount the system moves." Prepare PROGRESS.md, launch the script, sleep. Check the log the next morning, write the next task. The share of the ¥1.2M/month that comes from Claude Code-related automation revenue is still growing, and that's not because I had special skills — it's the result of stacking up "small but reliably working systems" one at a time.

This script is one of the entrances to that. Run it first, look at the logs, fix where you got stuck. The value is in that repetition.

I've put the full picture of the system, the breakdown of the ¥1.2M/month, and the 30-day procedure into a paid note.

📕 [Claude Code自律環境で、実際どう稼ぐか ― 仕組み・実例・始め方・サポート](https://note.com/bokuwalily/n/n849b3a07784a)

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

Follow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*
