Skip to content

Claude auth breaks every ~8h: OAuth token never reaches chat sessions (SDK env replaces process.env) #38

Description

@wisamamid

Version: v0.2.94 (also present on main @ 6ce6383)
Platform: macOS (packaged app)
Auth: Claude subscription (OAuth) — CLI also logged in, so provider resolves to cli_keychain

Symptoms

  • Dorabot works right after signing in with a Claude account, then starts failing with Failed to authenticate. API Error: 401 Invalid authentication credentials after ~8 hours (access-token lifetime).
  • Every new context window is blocked once this happens; re-authenticating fixes it temporarily, then the cycle repeats.
  • The UI keeps reporting the provider as connected while every run 401s.

Root cause

Chat sessions never receive the OAuth token that dorabot manages. Three things combine:

  1. agent.ts passes an explicit env to the SDK, built from a login-shell snapshot cached once at startup.
    cleanEnvForSdk()getShellEnv() (src/agent.ts:75-131) resolves the user's login-shell environment a single time and caches it in _resolvedShellEnv. This snapshot obviously does not contain CLAUDE_CODE_OAUTH_TOKEN (dorabot only sets that on process.env after login/refresh).

  2. The Claude Agent SDK does NOT merge options.env with process.env — it replaces it.
    In @anthropic-ai/claude-agent-sdk (checked 0.3.211), the subprocess env is options.env ? {...options.env} : {...process.env}. So everything ClaudeProvider does with process.env.CLAUDE_CODE_OAUTH_TOKEN (constructor, ensureOAuthToken(), completeOAuthLogin()src/providers/claude.ts) is invisible to actual chat subprocesses spawned via agent.ts (env: cleanEnvForSdk() at src/agent.ts:258 and :438).

  3. Result: chats silently depend on the Claude Code CLI's own keychain credentials.
    With no auth env var, the spawned CLI falls back to its own stored login. When that access token expires (~8h) and the headless child can't refresh it cleanly (refresh-token rotation races between dorabot-spawned children and the user's own terminal sessions), every session 401s. Re-authenticating in dorabot stores fresh tokens in dorabot's keychain entry, which chat sessions never read, so nothing is actually fixed — the loop repeats.

Two aggravating factors that mask the bug:

  • verifyAuth() (src/providers/claude.ts) calls query() without an env option, so its subprocess inherits process.env including the fresh token → the post-login verification passes even though real sessions are spawned with a different (token-less) environment.
  • _cliHasAuth is cached once per process and the cli_keychain branch of getAuthStatus() unconditionally returns authenticated: true, tokenHealth: 'valid' without ever validating, so the UI reports connected while runs 401.

Fix

Inject the freshly-ensured credentials into the per-run env in ClaudeProvider.query() (src/providers/claude.ts). This makes sessions independent of the CLI keychain state and lets dorabot's own refresh machinery (ensureOAuthToken + scheduleTokenRefresh) do its job:

async *query(opts: ProviderRunOptions): AsyncGenerator<ProviderMessage, ProviderQueryResult, unknown> {
    const method = getActiveAuthMethod();
    if (method === 'dorabot_oauth') {
      await ensureOAuthToken();
    }

    // The SDK REPLACES the subprocess env with opts.env (no merge with
    // process.env), and opts.env is a login-shell snapshot cached at startup.
    // Inject fresh credentials so every new session authenticates reliably.
    if (opts.env) {
      if (method === 'api_key') {
        const key = getApiKey();
        if (key) opts.env.ANTHROPIC_API_KEY = key;
      } else if (hasOAuthTokens()) {
        // Covers dorabot_oauth AND cli_keychain-with-dorabot-tokens: the env
        // var takes precedence over the CLI keychain in the child process.
        const token = await ensureOAuthToken();
        if (token) opts.env.CLAUDE_CODE_OAUTH_TOKEN = token;
        else delete opts.env.CLAUDE_CODE_OAUTH_TOKEN; // never pass a stale token
      }
    }
    // ... rest unchanged

I've been running this patch against the compiled gateway/dist/providers/claude.js of the packaged v0.2.94 app and the 8-hour auth loop is gone.

Suggested follow-ups (separate, smaller)

  • Re-check _cliHasAuth periodically (or on auth errors) instead of caching once per process.
  • Make verifyAuth() use the same env-construction path as real runs so verification can't pass while sessions fail.
  • Consider preferring dorabot_oauth over cli_keychain in getActiveAuthMethod() when dorabot has its own tokens — dorabot can refresh those itself, whereas it has no control over the CLI keychain entry.

Repro

  1. Log in to Claude Code CLI in a terminal and sign in to dorabot with a Claude account (so the provider resolves to cli_keychain).
  2. Use dorabot normally; wait for the CLI access token to expire (~8h, or force it by revoking/clearing the CLI's keychain entry Claude Code-credentials).
  3. Open a new context window → 401 Invalid authentication credentials.
  4. Re-authenticate in dorabot → verification passes, sessions still (or soon again) 401.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions