Headless `claude -p` fails "OAuth session expired and could not be refreshed" while concurrent interactive session works — no recovery path for unattended runners

Status Open
Maintainer reply None cached
Activity 0 comments · opened Jul 21, 2026

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 spawns claude -p "<context>" once per incoming mention

Repro

  1. Machine state: interactive Claude Code session running and healthy (requests succeed).
  2. 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).
  3. 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
``

  1. 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 -p spawn 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

  1. Make the OAuth refresh path work for headless claude -p when the Keychain holds a refreshable credential.
  2. Failing that: machine-readable auth-failure signaling + documented recovery for daemon use.
  3. Longer term: a supported long-lived headless credential that keeps feature parity (shared ask with #76905).

View original on GitHub ↗