Last time I wrote about using a probe to decide which jobs to stop first when the quota runs dry. This post is about the problem sitting right next to it: my script could tell when the quota was exhausted, but it treated the limit message as if it were real article body text, then kept retrying a failure that no rewrite could ever fix. From September 13 to 16, every run burned 3 claude -p calls to produce nothing. After the fix, it burns 1 and bails out.
The problem: 4 straight days (9/13–9/16) of 3 failures per run
note2-daily-stock.sh auto-generates product review articles for note2 (bokumolily) using claude -p. For each product it calls claude -p up to 3 passes to produce the body. Each pass is judged by a quality check (article_quality_ok) with criteria like "does the character count exceed MIN_CHARS (7,000)?" and "are there at least 2 affiliate links?"
For 4 days, 9/13 through 9/16, this script failed 3 times in a row with the exact same symptom. The cause: when claude -p hits the Max plan's limit, it returns a one-line limit-reached message (no TITLE: line), and the script treated that as "an ordinary generation failure where the body is too short." The comment in the actual code records exactly what happened:
# 2026-09-16: claude -pがクォータ上限メッセージ(1行・TITLE:行なし)を返すと
# 本文が空のまま「文字数不足」として3回とも空振りし、上限中に3回分のclaude呼び出しを浪費していた
# (note2-daily 9/13-9/16 4日連続で同一症状を実測)。上限メッセージは即検出して1回で中断する。
(note2-daily-stock.sh:565-567)
While the limit is in effect, no amount of rewriting will produce a body. Yet article_quality_ok only ever said "quality NG reason: not enough characters," so the script concluded "maybe one more pass will fix it" and threw away 2 more pointless claude -p calls during the limit window.
Note: A retry loop needs more than the monolithic fact that "it failed." If you don't distinguish "a failure a rewrite can fix" from "a failure a rewrite cannot fix," you'll keep walking into the same wall.
The fix: grep for the limit message and abort after 1 pass
The fix is to grep for the limit-message pattern immediately after each pass's generation, and if it matches, abort right there instead of spending the remaining passes.
for pass in 1 2 3; do
RAW=$(generate_article_raw "$PRODUCT_JSON" "$IMG_COUNT")
if printf '%s' "$RAW" | grep -qiE "hit your (weekly |5-hour |usage |session )?limit|usage limit reached|rate limit|resets? (at |in |on )?[0-9]{1,2}(:[0-9]{2})? *[ap]m"; then
QUOTA_HIT=1
log "ABORT: quota-message検出のため中断 (pass $pass) $SLUG raw先頭=[$(printf '%s' "$RAW" | tr '\n' ' ' | cut -c1-160)]"
break
fi
write_article_file "$RAW" "$OUT"
...
done
(note2-daily-stock.sh:563-572, excerpt)
When QUOTA_HIT is set, the script deletes the half-written output file and any images downloaded along the way, notifies Discord, and exits immediately. Instead of 3 calls, it consumes exactly 1 at that point and retreats.
if [ "$QUOTA_HIT" -eq 1 ]; then
[ -e "$OUT" ] && rm -f "$OUT"
rm -f "$ROOT/${IMG_BASENAME}-p"*.jpg
discord_notify "⚠️ note2-daily: claudeクォータ上限のため中断(3回消費せず中止): $ITEM_NAME"
exit 1
fi
(note2-daily-stock.sh:590-595)
"Not enough characters" and "limit reached" look alike on the surface (the body is nearly empty), but the correct response is completely different. The former might be fixed by a rewrite; the latter won't be fixed no matter how many times you retry until the reset time. Separating those two cases up front with a single grep is the entire fix.
The pitfall I stepped in: the sibling scripts still don't have it
There are 3 other scripts that share the same shape as note2-daily-stock.sh — "have claude -p write the body, retry if the quality check fails": article-daily-stock.sh (the script that writes this very Zenn article series), maker-daily-stock.sh, and series-daily-stock.sh. I grepped all 3, and none of them contained the limit-message detection pattern.
$ grep -n "hit your\|usage limit\|QUOTA_HIT\|rate limit" \
article-daily-stock.sh maker-daily-stock.sh series-daily-stock.sh
(該当なし)
In particular, run_pass() in series-daily-stock.sh has almost exactly the same structure that note2 tripped over.
run_pass(){ # $1=prompt $2=outfile $3=label
local prompt="$1" outfile="$2" label="$3" attempt wait
for attempt in 1 2 3; do
rm -f "$outfile"
timeout 600 "$CLAUDE" ... -p "$prompt" ... > "$outfile" ...
if [ -s "$outfile" ]; then
...
fi
log "WARN: pass $label 空出力(attempt $attempt/3)"
if [ "$attempt" -lt 3 ]; then
wait=$((attempt * 30))
sleep "$wait"
fi
done
(series-daily-stock.sh:254-279, excerpt)
Waiting 30 seconds, then 60 seconds, on "empty output (attempt N/3)" and hammering the same call 3 times for the same reason is essentially the same structure note2 hit on 9/13–9/16. If this script runs during a limit window, it should waste calls in exactly the same way — and it hasn't been fixed yet.
maker-daily-stock.sh only has a single generation pass, so it doesn't fail 3 in a row, but the same dates show up in its comments too.
# 失敗時もキューを1件進める。進めないと同じ題材が先頭に居座り続け、翌日以降も
# 同じ理由で落ち続ける(2026-09-13〜16 に同一題材が7回連続で失敗した実測)。
(maker-daily-stock.sh:412-413)
In other words, the Max plan limit on 9/13–9/16 didn't just hit note2 — it dragged the maker pipeline down with it. On the maker side, the underlying problem of "misdiagnosing the limit message as an incomplete body" is still there, exactly as in note2; it's being held together by a different band-aid: "even on failure, at least advance the topic queue." The same root cause has a different emergency patch in each script, applied whenever someone happened to notice.
- I confused a one-line limit message with a "body too short" quality failure → grep for it right after generation and abort before the quality check
- Judging retries only by "empty/non-empty" or "character count" can't tell failure types apart → branch on the failure reason (retry the fixable ones, exit immediately on the unfixable ones)
- Even when one limit drags down multiple pipelines at once, the fix doesn't propagate → note2 detects the cause, maker advances the queue, article/series are untouched — the responses are still all over the place
- Fixing one script doesn't automatically carry over to sibling scripts that copied the same structure → don't assume "it's fixed" until you've confirmed it with grep
Summary
- For 4 days (9/13–9/16), note2-daily-stock.sh misdiagnosed the limit-reached message as "not enough characters" and burned all 3 pointless
claude -pcalls during the limit window - The fix greps for the limit-message pattern immediately after generation and, on a match, aborts after 1 pass, ahead of the quality check
- During the same 4 days,
maker-daily-stock.shalso failed 7 times in a row on the same topic — the limit hit multiple pipelines simultaneously. But maker's fix is only "advance the queue," and the underlying misdiagnosis remains unfixed there as well as inarticle-daily-stock.shandseries-daily-stock.sh - A retry loop that doesn't distinguish failure types (fixable vs. unfixable) will step in the same hole every time. A lesson learned in one place only pays off once it's propagated to other scripts with a similar structure
Next time I plan to write about whether that propagation itself can be automated — a mechanism to mechanically carry a fix found in one script over to its "structurally similar siblings."
How do your retry loops tell a fixable failure from one that no retry will ever fix?
Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*
Top comments (0)