Headless `claude -p` fails "OAuth session expired and could not be refreshed" while concurrent interactive session works — no recovery path for unattended runners
Summary
Headless claude -p invocations fail immediately with:
Failed to authenticate: OAuth session expired and could not be refreshed
while a concurrent interactive Claude Code session on the same machine keeps working fine. Any unattended pattern that spawns claude -p on demand (launchd/cron jobs, webhook-woken agent supervisors) is dead until a human notices and runs /login — there is no recovery path from the headless side.
Likely the same credential family as #76905 (macOS Keychain refresh race) and #78037, but filing the headless-specific manifestation separately because the failure mode and the ask are different: it's not intermittent forced logout, it's headless spawns being permanently unable to refresh while an interactive session holds a live token.
Environment
- macOS 26.4 (Darwin 25.4.0), always-on dev machine
- Claude Code v2.1.216
- Auth: subscription OAuth login, credentials in login Keychain
- Workload: 1 long-lived interactive session + an agent supervisor (AgentParty
party serve --runner claude) that spawnsclaude -p "<context>"once per incoming mention
Repro
- Machine state: interactive Claude Code session running and healthy (requests succeed).
- Supervisor daemon receives a wake and spawns headless
claude -p. It exits 1 after ~2.3s (runner log:seq=345 sid=unknown duration_ms=2311 exit=1). - Manual repro, clean env to rule out inherited-env interference:
````
$ env -i HOME="$HOME" PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" claude -p "reply with exactly: ok"
Failed to authenticate: OAuth session expired and could not be refreshed
- Meanwhile the interactive session in another terminal continues to work — so a valid refresh path clearly exists for some process, just not for new headless spawns.
Expected
- A fresh
claude -pspawn should be able to refresh using the stored refresh token, same as an interactive session; or - if refresh is genuinely impossible (rotated/consumed refresh token per #76905's race analysis), emit a machine-readable error (distinct exit code / JSON on
--output-format json) so supervisors can alert a human instead of silently giving up, and document the supported unattended recovery story.
Impact
Unattended automation around Claude Code (scheduled jobs, on-call agent runners that wake on external events) cannot survive token expiry. claude setup-token is the suggested workaround but per #76905 it strips claude.ai connectors, so there is still no viable fully-headless pattern.
Ask
- Make the OAuth refresh path work for headless
claude -pwhen the Keychain holds a refreshable credential. - Failing that: machine-readable auth-failure signaling + documented recovery for daemon use.
- Longer term: a supported long-lived headless credential that keeps feature parity (shared ask with #76905).