Day 32 of 37 · Monday · Learning

Headless auth — why four mornings died silentlyCapability

Your morning plan died five days in a row with one repeated line: 'OAuth session expired and could not be refreshed.' Today you learn what that sentence actually means, why no amount of retrying could fix it, and why the alert wall stayed quiet instead of growing by five.

Catch-up progress
32/37
Why this matters to you

This is not hypothetical — it is the exact failure that killed plan-day from Jul 22 through Jul 26 (logs/plan-day.log lines ~1975 onward). The watchdog fired, the retry loop ran all 3 attempts, the network was fine — and every attempt failed identically, because the one thing the job needed was a human with a browser. Meanwhile the new dedup ledger (shipped Jul 26) correctly held five failures to ONE Things task instead of five.

A program can prove its identity to Anthropic two ways. An **API key** is a long-lived credential — a string that works until you revoke it, billed per-token. An **OAuth session** is what you get when you 'log in with your Claude account' in a browser: a short-lived access token plus a refresh token that quietly mints new access tokens in the background. The `claude` CLI on your Mac uses the OAuth route, which is why headless runs ride on your Claude subscription instead of a separate API bill.

The catch: refresh only works while the refresh token itself is still valid. When it expires or gets invalidated (long idle stretches, a re-login elsewhere, a revocation), the CLI needs the full browser dance again — and a launchd job firing at 6:15am has no browser and no human. So it prints 'OAuth session expired and could not be refreshed' and exits 1. This is a fundamentally different failure class from a network blip: a retry loop fixes flakes, but retrying an auth failure just fails identically N times. That is exactly what your log shows — rc=1 on attempts 1, 2, and 3, every morning, five mornings straight.

Where it lives in your system: `PDB/scripts/run_plan_day.sh` wraps `claude -p` in a network-wait, a watchdog, and `run_with_retry` (3 attempts). All three guardrails are for *transient* failures; none can fix auth. The layer that behaved perfectly was the alert dedup ledger from commit d9b37fc — its `[alerts] deduped 'plan-day-fail' (×5, open since 2026-07-22)' line means: same outage, still open, no new task. One 🚨, not five. The fix was you opening an interactive session (which completed the login) — and this morning's run authenticated cleanly.

Worked example

Forensics on your own log — the whole incident is readable in four greps:

cd ~/Claude/PDB

# 1. The failure signature — five mornings of it
grep -n 'OAuth session expired' logs/plan-day.log | head -6

# 2. Proof retries can't fix auth: all 3 attempts, same rc=1
grep -n 'attempt 3 failed' logs/plan-day.log | tail -3

# 3. The dedup ledger doing its job — one task, not five
grep -n "deduped 'plan-day-fail'" logs/plan-day.log | tail -1

# 4. This morning's recovery
grep -n 'Jul 27.*plan-day fire' logs/plan-day.log
▶ Do it now
  1. Open Terminal and run grep #1 above. Find the FIRST occurrence — note the line number and confirm it's the Jul 22 06:16 run. That's the outage's birth certificate.
  2. Run grep #3. Read the dedup line and then open data/alert-state.json (`cat data/alert-state.json | python3 -m json.tool`) to see the ledger entry that suppressed the duplicate alerts.
  3. Run grep #4 and read today's block to the end: 'network ready' → four 'prefetch ok' lines → attempt 1/3 → the run that produced today's plan. You've now traced one full outage from first failure to recovery in the primary source — no dashboard, just the log.

Gotchas

Go deeper: Claude Code docs — authentication · OAuth 2.0 refresh tokens, explained simply
One-card takeaway

A watchdog can restart a job and a retry can outlast a flake, but only a human can re-login — so the job's real duty is to fail loudly, once, and point at the fix.