Day 18 of 29 · Wednesday · Learning

Headless Claude — `claude -p` and the fallbackModel safety netCapability

When plan-day runs itself at 6:15am with no human watching, a single overloaded-model blip kills the whole run. `--fallback-model` is the one flag that turns that fatal error into an automatic retry on a cheaper model.

Catch-up progress
18/29
Why this matters to you

Three of the 🚨 tasks sitting in your Inbox right now are 'PDB plan-day failed 2026-06-13' and '2026-06-14'. Those almost certainly weren't bugs in your code — they were the Anthropic API returning 'overloaded' to a headless run that had no fallback. Your wrapper at scripts/run_plan_day.sh line 116 calls `claude -p` with a watchdog and a failure-notifier, but no fallback model. So when the primary is busy, the job doesn't downgrade — it dies, and you get a red task instead of a plan.

`claude -p "<prompt>"` is 'headless' or 'print' mode — it runs Claude with no interactive terminal, prints the result, and exits. This is the engine behind every unattended job you own: the 6:15am plan-day, the language email, the learning email. There's no human to click 'retry' when something hiccups, so every failure mode has to be handled by flags and wrapper logic instead of by you.

The most common transient failure isn't your fault: the API occasionally returns an 'overloaded' response (HTTP 529) when demand spikes. Interactively you'd just hit enter again. Headless, that response makes `claude -p` exit non-zero — and your wrapper correctly treats non-zero as 'the job failed,' fires a macOS notification, and creates the 🚨 Things task. Everything downstream worked exactly as designed; the problem is the job gave up on the first stumble.

`--fallback-model <name>` is the fix. You give it a second, usually cheaper/faster model (e.g. `sonnet`). If the primary model is overloaded or errors out, Claude automatically retries the same prompt on the fallback instead of exiting. For a planning job, a plan written by the fallback model is infinitely better than no plan and a red alert — graceful degradation beats hard failure every time for unattended work.

Worked example

Here's your actual invocation today (run_plan_day.sh, lines 116-122) and the one-line change that hardens it:

# TODAY — no fallback. One overload = dead run = 🚨 task.
run_with_timeout "$CLAUDE_TIMEOUT_S" \
    "$CLAUDE_BIN" \
        --settings '{"enabledPlugins":{}}' \
        --strict-mcp-config \
        --dangerously-skip-permissions \
        --permission-mode bypassPermissions \
        -p "/plan-day"

# HARDENED — add one line. Overload now retries on Sonnet.
run_with_timeout "$CLAUDE_TIMEOUT_S" \
    "$CLAUDE_BIN" \
        --settings '{"enabledPlugins":{}}' \
        --strict-mcp-config \
        --dangerously-skip-permissions \
        --permission-mode bypassPermissions \
        --fallback-model sonnet \
        -p "/plan-day"
▶ Do it now
  1. Open the wrapper: `open -e /Users/tom/Claude/PDB/scripts/run_plan_day.sh` (or in your editor).
  2. Find the `claude -p "/plan-day"` block near line 116. Add a single line `--fallback-model sonnet \` directly above the `-p "/plan-day"` line. Save.
  3. Dry-run it once by hand: `bash /Users/tom/Claude/PDB/scripts/run_plan_day.sh` and watch logs/plan-day.log — confirm it still exits status 0 and writes today's plan. If it does, the fallback is now armed for tomorrow's 6:15am run and those plan-day-failed 🚨 tasks stop recurring.

Gotchas

Go deeper: Claude Code CLI reference — print mode & flags
One-card takeaway

Unattended jobs should degrade, not die. One flag — `--fallback-model` — converts 'plan-day failed, here's a red alert' into 'plan-day ran on the backup model, here's your plan.'