Skip to content

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

Description

@margastasian

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).
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleIssue is inactive

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions