--channels host mode uses stored Keychain OAuth credentials instead of valid CLAUDE_CODE_OAUTH_TOKEN (precedence inverted vs docs)

Status Open
Reported on v2.1.216
Maintainer reply None cached
Activity 0 comments · opened Jul 22, 2026

Summary

In channel host mode (claude --channels plugin:discord@claude-plugins-official, long-running headless process under launchd via a pty.spawn wrapper), Claude Code authenticates with the stored Keychain OAuth session (Claude Code-credentials) even when a valid CLAUDE_CODE_OAUTH_TOKEN (long-lived sk-ant-oat01 from claude setup-token) is present in the process environment.

Per the documented precedence (https://code.claude.com/docs/en/authentication.md), CLAUDE_CODE_OAUTH_TOKEN (tier 5) should win over stored subscription OAuth credentials (tier 6). Interactive mode and headless -p mode both honor this; channel host mode appears not to.

Because the stored access token has a ~8h TTL and is not refreshed in headless/pty contexts (see #50743, #28827), the host silently goes deaf: every command fails with API Error: 401 OAuth access token has expired · Please run /login, while a perfectly valid env token sits unused in its environment.

Environment

  • Claude Code 2.1.216 and 2.1.217 (both reproduce), npm global install, macOS (Darwin 25.5.0)
  • Host: claude --channels plugin:discord@claude-plugins-official --no-chrome --mcp-config … --settings …, launched by launchd through python3 -c 'import pty,sys; pty.spawn(sys.argv[1:])' (launchd provides no TTY)
  • CLAUDE_CODE_OAUTH_TOKEN exported from a 600 env file sourced by the launcher; only auth-related var in the environment (verified via ps eww)
  • Stored Keychain entry Claude Code-credentials present (claudeAiOauth with expiresAt in the past)

Evidence

  1. Env token valid: direct curl https://api.anthropic.com/v1/messages with Authorization: Bearer <env token> + anthropic-beta: oauth-2025-04-20200.
  2. Env token present in the host process: ps eww <pid> shows CLAUDE_CODE_OAUTH_TOKEN whose value hash equals the env file's token hash.
  3. Keychain access token expired: claudeAiOauth.expiresAt ≈ 18h in the past at failure time.
  4. First message of the day to the host → session transcript records a synthetic assistant turn: Please run /login · API Error: 401 OAuth access token has expired. Re-authenticate to continue. (turn duration ~2.3s).
  5. Control experiments (same machine, same expired Keychain entry, same env token, same binary 2.1.217):
  • headless: CLAUDE_CODE_OAUTH_TOKEN=… claude -p "…" → success; Keychain mdat untouched.
  • interactive clean-room: env -i HOME=… PATH=… CLAUDE_CODE_OAUTH_TOKEN=… claude under a fresh pty → success; Keychain mdat untouched (so the reply came from the env token, not a refreshed session).
  1. Deleting the stored entry (security delete-generic-password -s "Claude Code-credentials") and restarting the channel host restores service — the host then runs on the env token.

Expected

CLAUDE_CODE_OAUTH_TOKEN takes precedence over stored OAuth credentials in channel host mode, as documented and as observed in -p and plain interactive mode.

Actual

Channel host mode sends the stored (expired) Keychain access token → every turn dies with 401 while a valid env token is available. Failure is silent from the operator's perspective (process, gateway and socket all healthy).

Notes

  • A secondary sharp edge: claude setup-token's browser OAuth flow also (re)writes the Keychain session entry as a side effect. An operator who "fixes" the 401 by minting a fresh env token thereby re-arms the stored-credential trap with a fresh ~8h access token — which is exactly how this presented as a recurring daily outage.
  • Related: #50743, #28827, #12447 (headless refresh of stored OAuth not functional).

View original on GitHub ↗