6 Hours, 15 Jobs, Zero Alerts: A Circuit Breaker for Claude Code's Weekly Quota A developer built a circuit breaker for Claude Code's weekly quota after 15 LaunchAgent jobs silently failed for six hours, costing ¥1.2M/month in lost automation. The tool, claude-quota-guard.py, detects quota-limit errors, blocks further invocations with exit 75, notifies via Discord, and persists state to a JSON file, preventing silent failures. Nothing crashed. Nothing paged me. The 15 LaunchAgent jobs behind my ¥1.2M/month autonomous stack hit the weekly quota ceiling and then spent six hours swallowing errors and pretending to work. I found out the next morning, by checking by hand. When automation breaks, it breaks in one of two ways: the failure that crashes and screams , or the failure that disappears quietly . Claude Code has a weekly usage limit. When you hit it, API calls come back with an error message containing weekly usage limit . That's where the real problem starts: automation scripts running every 5 minutes or every hour under LaunchAgent mostly just emit exit 1 and terminate when they receive that error. launchd records exit 1 as "the job failed," but then tries to run it again on the next cycle as if nothing happened. And it fails again. That loop can run Monday, Tuesday, Wednesday — and nobody notices, because no dashboard exists . Auto-generating social posts, AI-processing thumbnails, ingesting conversation logs into the knowledge base: all of it is gone. In my environment 15 Claude-invoking jobs run in parallel, so hitting the limit skips all of them at once. 6 hours × 15 jobs = 90 jobs' worth of work vanishing in silence. Let me dig into why this is hard to detect automatically. macOS launchd does not notify you by default when a job fails. You can control the retry interval with ThrottleInterval , but there is no way to express "hit the quota → don't run at all until the next limit reset" in a plist. And Claude Code's own exit code varies between 0, 1, and other values depending on the situation, so "just look at the exit code" isn't a simple answer either. What makes it worse is that the error message goes to stderr . LaunchAgent dumps stdout/stderr into logs under /tmp , but almost nobody watches those logs continuously. Very few solo developers have a habit of running grep -r "weekly usage limit" ~/Library/Logs/ first thing in the morning. Back in 2025, when I was a university student earning ¥100K a month, my daily routine included discovering "huh, why didn't yesterday's post go up?" through manual checks. Even after juggling gigs up to ¥600K a month, the same problem kept recurring. Then I was laid off and went back to zero, spent half a year rebuilding my Claude Code autonomous environment from scratch, and got to today's ¥1.2M/month. One thing I learned along the way: if you leave "not knowing you're down" unaddressed, what disappears isn't the revenue — it's the trust. The core of the mechanism is three things: the moment it fires, stop, notify, and persist . When you hit the quota, immediately block every subsequent Claude invocation exit 75 , notify Discord, and persist the circuit state to a JSON file. Instead of going to check manually, you let the environment scream at you. The idea is the same as an electrical circuit breaker. The instant excess current flows, the breaker trips. Once things recover, you reset it by hand. It prevents jobs from "hammering pointlessly and piling up errors," and it stops in a way that's observable from the outside. LaunchAgent 定期ジョブ群 × 15本 │ │ CLAUDE=~/.claude/scripts/claude-quota-guard.py ▼ claude-quota-guard.py │ ├─ CLOSED ─▶ ~/.local/bin/claude 本物 を実行 │ │ │ stdout + stderr の末尾 128KB をスキャン │ │ │ "weekly usage limit" 等 7パターンに一致? │ │ │ YES ──┤ │ ▼ │ open until = now + 21600s 6時間 │ reason = "quota-message" │ ~/.claude/state/claude-quota-circuit.json に原子書き込み │ ~/.discord/notify.sh "alerts" へ即時通知 │ └─ OPEN ──▶ exit 75 で即ブロック(直接呼び出し時) exit 0 でスキップ(--job モード時) 復旧フロー: 制限リセット後 ─▶ claude-quota-guard.py --reset ─▶ CLOSED に戻る または open until を過ぎると自動で CLOSED(normalize expired) The circuit state is persisted to ~/.claude/state/claude-quota-circuit.json with atomic writes. So that the file isn't corrupted when multiple jobs read and write the state simultaneously, updates take a file lock with fcntl.LOCK EX first, then swap the file in atomically with os.replace . There are seven strings that claude-quota-guard.py treats as a quota hit. QUOTA PATTERNS = r"weekly ?:usage ?limit", r"usage limit", r"rate limit", r"quota ?:exceeded|limit|reached ", r" ?:you ?:'ve| have ?hit your limit", r"limit reached", r"resets? ?:at|in|on|tomorrow ", Matching is case-insensitive re.IGNORECASE , and the scan target is the last 131,072 bytes 128KB of Claude's stdout and stderr combined. combined = result.stdout + b"\n" + result.stderr -131072: .decode "utf-8", errors="replace" record claude result result.returncode, combined There's a clear reason for slicing off the tail: it skips past the normal early-run logs of long-running jobs and efficiently inspects only the tail, where errors are most likely. A side benefit is not having to expand a huge stdout entirely into memory. Besides quota-message detection, there's one more trigger. Three consecutive exit 1s within 10 minutes forces OPEN. cooldown = int os.environ.get "CLAUDE GUARD COOLDOWN SECONDS", "21600" window = int os.environ.get "CLAUDE GUARD FAILURE WINDOW SECONDS", "600" threshold = int os.environ.get "CLAUDE GUARD FAILURE THRESHOLD", "3" The defaults are cooldown=21600 6 hours , window=600 10 minutes , and threshold=3 3 times . Claude's weekly limit often resets in the early morning Japan time, and the rule of thumb that the limit is likely lifted six hours later is where these values come from. They can be overridden via environment variables, so you can tune them to your own reset timing. A reason field is recorded per trigger: "quota-message" for quota-message detection, "repeated-exit-1" for consecutive failures. That distinction matters later, when deciding how to recover. When called while the circuit is OPEN, run claude immediately returns exit 75. php EXIT CIRCUIT OPEN = 75 def run claude arguments: list str - int: status = circuit status if status "is open" : print "CLAUDE QUOTA CIRCUIT OPEN " f"reason={status 'reason' } remaining={status 'remaining seconds' }s", file=sys.stderr, return EXIT CIRCUIT OPEN 以下、本物の claude を実行する処理 Exit 75 corresponds to the POSIX convention EX TEMPFAIL temporary failure , meaning "not now, but you can try later." In a launchd plist you can set