[BUG] claude -p rejects just-issued OAuth token with 'invalid claims: token is expired' outside host-managed-auth env (2.1.126, macOS)

Resolved 💬 3 comments Opened May 1, 2026 by AgelessK Closed May 5, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (closest neighbours: #51047 about --bare, #50107 about claude -p hanging — both distinct symptoms)
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.126)

What's Wrong?

After a successful claude auth login (browser flow completes; claude auth status returns loggedIn: true, authMethod: oauth_token), claude -p immediately rejects the just-issued token with API Error: 400 invalid token: token has invalid claims: token is expired — but only when the calling environment lacks the host-managed-auth env vars (CLAUDECODE=1, CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1, CLAUDE_CODE_SDK_HAS_OAUTH_REFRESH=1).

Inside a Claude Code Bash-tool subprocess (env vars set), the same machine, same keychain entry, same binary version works fine. Outside (plain Terminal.app session, vars not set), the same claude -p invocation fails 100% of the time.

So the bug is: keychain-based oauth_token reads succeed locally (claude auth status confirms), but the token claims sent to the API are rejected. This makes claude -p unusable for non-interactive scripting on a freshly-logged-in machine unless the script happens to run inside a host-managed Claude session.

What Should Happen?

After claude auth login succeeds and claude auth status returns loggedIn: true, claude -p "ping" should also work — regardless of whether CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1 is set in the calling env. The keychain-based OAuth path should be self-sufficient.

Either:

  • Fix: claim format mismatch / refresh-token-not-used in the keychain-only auth path, OR
  • Fix: claude auth status should validate against the API rather than just checking that a token is cached, so the user knows the token is unusable BEFORE they invoke claude -p.

Error Messages/Logs

$ claude --version
2.1.126 (Claude Code)

$ claude auth login
# (browser flow completes successfully)

$ claude auth status
{
  "loggedIn": true,
  "authMethod": "oauth_token",
  "apiProvider": "firstParty"
}

$ echo "ping" | claude -p --output-format text
API Error: 400 invalid token: token has invalid claims: token is expired

Even after claude auth logout && claude auth login (full reset → fresh browser flow), the just-issued token is still rejected by claude -p with the same error.

Steps to Reproduce

  1. macOS, Claude Code 2.1.126.
  2. Open Terminal.app directly (i.e., NOT from inside Claude Code's Bash tool / Claude Desktop's "open terminal"). Verify env: env | grep -i claude should show no CLAUDECODE=1 / CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1.
  3. Confirm baseline: claude --version2.1.126.
  4. Run claude auth login. Complete browser flow. Verify claude auth status returns loggedIn: true.
  5. Immediately run: echo "ping" | claude -p --output-format text
  6. Observe: API Error: 400 invalid token: token has invalid claims: token is expired.
  7. Discriminator check: spawn the same command from a host-managed Claude Code Bash-tool subprocess (where CLAUDECODE=1, CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1, CLAUDE_CODE_SDK_HAS_OAUTH_REFRESH=1 are set in the env). Same keychain entry, same binary, same claude -p invocation — works fine.

Workaround: export ANTHROPIC_API_KEY=<key> to bypass the keychain OAuth path entirely. With an API key set, claude -p works in any env.

This was caught while running skill-creator's run_eval.py (which spawns claude -p subprocesses) — a real-world non-interactive scripting use case where host-managed-auth env vars aren't naturally present.

Environment

  • macOS (Darwin)
  • Claude Code 2.1.126 (installed at ~/.local/bin/claude)
  • Shell: zsh
  • Auth method: OAuth via claude auth login (browser flow, first-party Anthropic account)
  • Keychain entry exists and is recent (security find-generic-password -s "Claude Code-credentials" shows mdat = today)

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗