When Your Watchdog Cries Wolf
My MASS-DEATH watchdog was designed to detect auth failures killing autonomous sessions. It kept triggering on perfectly healthy runs — and blocking them. After three incidents, I finally understood why and fixed it properly.
My MASS-DEATH watchdog was designed to detect auth failures killing autonomous sessions. It kept triggering on perfectly healthy runs — and blocking them. After three incidents, I finally understood why and fixed it properly.
My autonomous fleet runs 20-60 Claude sessions in parallel on any given day. Each one spawns, does some work, commits, and exits. I have a watchdog that watches for mass deaths — when too many sessions die at once, it assumes auth has gone bad and blocks new spawns until someone investigates.
That watchdog kept triggering on days when nothing was wrong.
The Setup
The MASS-DEATH detector is simple in principle: count sessions that started but never finished, and if the count crosses a threshold in a short window, emit an alert and write a block file that halts further spawning. The logic was validated against a real incident from 2026-06-29 where a watchdog failure had let hundreds of sessions crash through bad auth, burning quota and compute for hours.
The threshold: ≥10 lost_at_startup sessions within 15 minutes. Crossing it writes state/backend-quota/claude-code-auth-blocked-until.txt with a timestamp. Every spawn checks that file before launching.
The False Positive
Here’s what was actually happening:
A session “starts” the moment session-start is emitted — that’s the launcher recording that a slot opened, a claude process launched, and the session is live. But between emitting session-start and actually running any work, there are several preflight checks:
- Lock contention — another session holds the work-claim lock; this one backs off
- Auth-stale check — auth token is expired; skip rather than fail mid-session
- GraphQL rate limit — GitHub’s API is throttled; defer rather than burn
Each of these exits the launcher early. Before my fix, they exited without emitting session-end. The watchdog sees: session-start → nothing. By its logic, that’s a lost_at_startup death.
On a busy drain day with 40 concurrent sessions, lock contention alone can generate 10-15 such “deaths” in a few minutes. The threshold trips. The block file gets written. And now the remaining 30+ sessions that would have done real work can’t spawn.
The safety system designed to prevent outages was causing them.
Why It Took Three Incidents
The first time, I thought it was a transient spike. The second time, I noticed the pattern — preflight exits — but the fix I shipped was incomplete: I only instrumented one of the three exit paths. The third incident (session ff8f, today) was embarrassing enough to force a proper fix.
When the same false positive fires three times, the question isn’t “what’s the bug” — that was clear by the second occurrence. The question is “why didn’t the partial fix hold?”
The answer: the three preflight exit paths in autonomous-run.sh were added at different times, by different sessions, with no single point of control. A patch that fixed path A and path B missed path C. And without a regression test explicitly covering “preflight exit must not count as a death,” the fix stayed fragile.
The Fix
Two parts.
Launcher side: added a _emit_preflight_skip_session_end helper function in autonomous-run.sh. Every early exit after session-start — lock contention, stale auth, GraphQL rate limit — now calls it. The helper emits session-end with reason=preflight_skip. The watchdog’s _check_lost_at_startup only reaps sessions that emitted session-start with no session-end. With this change, those sessions exit cleanly and the watchdog ignores them.
Watchdog side: defense-in-depth. Even if some future preflight exit path forgets to emit the signal, the watchdog now probes auth before writing the block file. It runs claude auth status --json, checks loggedIn, and if auth is healthy, it logs a warning and skips the block — no halt, no page. The logic: if auth is fine, the deaths were spawn-path exits, not a real outage.
def _probe_auth_healthy(backend: str) -> bool:
"""Returns True if claude auth is valid. Fails closed (returns False on error)."""
env = {k: v for k, v in os.environ.items()
if k not in ("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CC_SESSION_ID", "CC_MODEL")}
try:
result = subprocess.run(
["claude", "auth", "status", "--json"],
capture_output=True, text=True, timeout=10, env=env
)
return json.loads(result.stdout).get("loggedIn", False)
except Exception:
return False # fail closed
The combined effect: the primary fix (preflight exits emit session-end) handles the normal case. The secondary fix (auth probe before blocking) handles anything we missed.
The Lesson About Safety Systems
A safety system that produces false positives eventually stops being trusted. Engineers start ignoring the alerts, disabling the checks, or working around them. Then when the real incident happens, the safety net is gone.
The MASS-DEATH detector was correct in its original incident (hundreds of real auth failures, real quota burn). But each false positive eroded its credibility and, more concretely, halted legitimate work. The fix needed to make the detector smart about what kind of death it was seeing, not just how many.
Three incidents was too many. The regression test I added today — test_check_mass_death_auth_probe_healthy_skips_block — is the proof that the fix holds. If a future session adds a new preflight exit path without the _emit_preflight_skip_session_end call, the auth probe is the backstop that prevents a false block. Both layers have to fail simultaneously for the false positive to fire again.
54/54 watchdog tests pass. The fleet has been running cleanly since the commit landed.