Skip to content

Bare mode always fails for /login users: apiKeyHelper is given an OAuth token, causing a 401 retry loop and 90s+ stall on every start #1033

Description

@thomasweld

Summary

resolveBareModeConfig() builds an apiKeyHelper from the Claude Code OAuth token. Per Anthropic's docs, apiKeyHelper must return an API key, so the credential is rejected. Because the helper succeeds and returns a non-empty string, Claude Code treats the resulting 401 as a retryable auth failure, re-invokes the helper, gets the same rejected token, and loops.

The result is a stall of 90+ seconds on every il start before the v0.14.2 fallback kicks in. It is not intermittent: for any user authenticated via /login, bare mode can never succeed, so the doomed attempt runs every time.

This is the same overhead described in #1028, but that issue frames it as an Enterprise-only edge case needing an opt-out config. It is broader than that, and it is a bug rather than a missing option: the condition is deterministically detectable before spawning anything.

Introduced by #977, which added --bare to branch naming and commit message generation.

Environment

  • @iloom/cli 0.14.2
  • Claude Code 2.1.220, macOS
  • Auth: /login with a Claude Max subscription (token in Keychain, sk-ant-oat01…, valid, with a refresh token)
  • No ANTHROPIC_API_KEY set

Root cause

extractOAuthToken() pulls claudeAiOauth.accessToken from the macOS Keychain. resolveBareModeConfig() then does:

const settings = JSON.stringify({ apiKeyHelper: "echo $__ILOOM_OAUTH_TOKEN" });
return { bare: true, settings, oauthToken: token };

The Anthropic docs are explicit that this cannot work:

"Bare mode does not read CLAUDE_CODE_OAUTH_TOKEN. If your script passes --bare, authenticate with ANTHROPIC_API_KEY or an apiKeyHelper instead."
— Authentication → Generate a long-lived token

apiKeyHelper output is sent as an API key. An OAuth access token is a different credential type, so the request 401s.

And on the retry behaviour:

"by default, apiKeyHelper is called after 5 minutes or on HTTP 401 response"
"Helper failures: when the script exits with an error, times out, or prints nothing, requests fail with Your apiKeyHelper script is failing within three attempts."

The three-attempt cap only applies when the helper fails. Here it succeeds and returns a plausible string, so the cap does not apply and the 401 → re-invoke → 401 loop runs long.

Reproduction

Verified by invoking the CLI exactly as launchClaude() builds it. Only the apiKeyHelper output varies:

apiKeyHelper returns Result
echo '' (empty) fails in 0.7s — counts as a helper failure, capped at 3 attempts
echo $UNSET_VAR (empty) fails in 0.8s — same
echo sk-ant-bogus (non-empty, rejected) still running at 40s
real OAuth token (non-empty, rejected) still running at 90s, cap reached before it returned

Plain headless with the same account works instantly:

$ claude -p 'Reply with exactly: OK' --model haiku
OK

Two smaller problems

1. The fallback message misattributes the cause.

⚠️  Bare mode failed (likely expired OAuth token), retrying without --bare

The token is not expired — in my case it had 5.5 hours left and a valid refresh token, and worked fine on the non-bare path seconds later. This message sends users to /login, which does not help and does not change the outcome.

2. generateBranchName() has an unreachable fallback.

It returns feat/issue-<n> on error, but a hung child process never throws, so the catch is never reached. launchClaude() already accepts a signal option; generateBranchName() never passes one. A timeout would turn this from a stall into an immediate, correct fallback — worth doing regardless of the fix below, since it bounds any future hang in a subprocess.

Suggested fix

Skip bare mode when the only available credential is an OAuth token, rather than attempting and falling back:

async function resolveBareModeConfig() {
  if (process.env.ANTHROPIC_API_KEY?.trim()) return { bare: true };
  // apiKeyHelper requires an API key; an OAuth access token will 401 and retry-loop.
  return { bare: false };
}

That makes #1028's opt-out unnecessary for this case, since the condition is knowable up front. If bare mode should still be reachable for subscription users, it needs a credential path that is not apiKeyHelper — but the docs suggest ANTHROPIC_API_KEY is the only supported route.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions