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:
-
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).
-
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).
-
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
- 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).
- 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).
- Open a new context window →
401 Invalid authentication credentials.
- Re-authenticate in dorabot → verification passes, sessions still (or soon again) 401.
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_keychainSymptoms
Failed to authenticate. API Error: 401 Invalid authentication credentialsafter ~8 hours (access-token lifetime).Root cause
Chat sessions never receive the OAuth token that dorabot manages. Three things combine:
agent.tspasses an explicitenvto 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 containCLAUDE_CODE_OAUTH_TOKEN(dorabot only sets that onprocess.envafter login/refresh).The Claude Agent SDK does NOT merge
options.envwithprocess.env— it replaces it.In
@anthropic-ai/claude-agent-sdk(checked 0.3.211), the subprocess env isoptions.env ? {...options.env} : {...process.env}. So everythingClaudeProviderdoes withprocess.env.CLAUDE_CODE_OAUTH_TOKEN(constructor,ensureOAuthToken(),completeOAuthLogin()—src/providers/claude.ts) is invisible to actual chat subprocesses spawned viaagent.ts(env: cleanEnvForSdk()atsrc/agent.ts:258and:438).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) callsquery()without anenvoption, so its subprocess inheritsprocess.envincluding the fresh token → the post-login verification passes even though real sessions are spawned with a different (token-less) environment._cliHasAuthis cached once per process and thecli_keychainbranch ofgetAuthStatus()unconditionally returnsauthenticated: 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:I've been running this patch against the compiled
gateway/dist/providers/claude.jsof the packaged v0.2.94 app and the 8-hour auth loop is gone.Suggested follow-ups (separate, smaller)
_cliHasAuthperiodically (or on auth errors) instead of caching once per process.verifyAuth()use the same env-construction path as real runs so verification can't pass while sessions fail.dorabot_oauthovercli_keychainingetActiveAuthMethod()when dorabot has its own tokens — dorabot can refresh those itself, whereas it has no control over the CLI keychain entry.Repro
cli_keychain).Claude Code-credentials).401 Invalid authentication credentials.