--channels host mode uses stored Keychain OAuth credentials instead of valid CLAUDE_CODE_OAUTH_TOKEN (precedence inverted vs docs)
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 throughpython3 -c 'import pty,sys; pty.spawn(sys.argv[1:])'(launchd provides no TTY) CLAUDE_CODE_OAUTH_TOKENexported from a 600 env file sourced by the launcher; only auth-related var in the environment (verified viaps eww)- Stored Keychain entry
Claude Code-credentialspresent (claudeAiOauthwithexpiresAtin the past)
Evidence
- Env token valid: direct
curl https://api.anthropic.com/v1/messageswithAuthorization: Bearer <env token>+anthropic-beta: oauth-2025-04-20→ 200. - Env token present in the host process:
ps eww <pid>showsCLAUDE_CODE_OAUTH_TOKENwhose value hash equals the env file's token hash. - Keychain access token expired:
claudeAiOauth.expiresAt≈ 18h in the past at failure time. - 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). - Control experiments (same machine, same expired Keychain entry, same env token, same binary 2.1.217):
- headless:
CLAUDE_CODE_OAUTH_TOKEN=… claude -p "…"→ success; Keychainmdatuntouched. - interactive clean-room:
env -i HOME=… PATH=… CLAUDE_CODE_OAUTH_TOKEN=… claudeunder a fresh pty → success; Keychainmdatuntouched (so the reply came from the env token, not a refreshed session).
- 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).