Skip to content

fix(agents-login): wait for a populated .credentials.json before capturing#744

Merged
ExtraToast merged 1 commit into
mainfrom
fix/agents-login-wait-for-populated-credential
Jun 27, 2026
Merged

fix(agents-login): wait for a populated .credentials.json before capturing#744
ExtraToast merged 1 commit into
mainfrom
fix/agents-login-wait-for-populated-credential

Conversation

@ExtraToast

Copy link
Copy Markdown
Owner

Root cause (the real "sign-in doesn't stick")

After a successful Claude sign-in, runners still showed "Not logged in" / API billing. Live inspection of a properly-owned runner pod found the injected .credentials.json was 2 bytes ({}) and the captured credentials_json secret was empty — while account_json (the profile) had real data.

The worker finalized capture the instant ~/.claude/.credentials.json existed, but Claude creates the file then writes claudeAiOauth.accessToken a moment later. The credential probe (and the "Logged in as" success-line path) won that race and captured an empty {}, which was stored and injected verbatim → the runner had no token.

Fix

  • claudeCredentialIsPopulated() (type guard): the credential counts only when an OAuth token can be extracted (claudeAiOauth.accessToken / nested / sk-ant-oat).
  • probeClaudeCredentialFile finalizes only when populated; an existing-but-empty {} keeps polling until the token lands.
  • The success-line path no longer finalizes Claude directly (it can precede the token write) — it defers to the populated-credential probe.
  • captureClaude throws unless populated, so an empty {} can never be stored from any path.

Tests

Unit + capture coverage for the empty-{} race; existing tests updated to the populated-credential contract. 104 pass, branch coverage 84.42%.

🤖 Generated with Claude Code

…uring

Root cause of runners stuck on "Not logged in" / API billing after a
successful sign-in: the worker finalized capture the instant
~/.claude/.credentials.json EXISTED, but Claude creates the file and writes
claudeAiOauth.accessToken a moment later. The probe (and the success-line
path) won the race and captured an empty `{}`, which was stored and injected
verbatim — so the runner had no token (the profile/account_json won its race
and looked fine, masking the problem).

- Add claudeCredentialIsPopulated() (type guard): the credential counts only
  when an OAuth token can be extracted (claudeAiOauth.accessToken / nested /
  sk-ant-oat).
- probeClaudeCredentialFile finalizes only when populated; an existing-but-empty
  {} keeps it polling until the token lands.
- The "Logged in as" success line no longer finalizes Claude directly (it could
  precede the token write); it defers to the populated-credential probe.
- captureClaude throws unless the credential is populated, so an empty {} can
  never be stored from any path.

Tests: unit + capture coverage for the empty-{} race; existing tests updated to
the populated-credential contract. 104 pass, branch coverage 84.42%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ExtraToast ExtraToast merged commit a7d5878 into main Jun 27, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant