fix(agents-login): wait for a populated .credentials.json before capturing#744
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.jsonwas 2 bytes ({}) and the capturedcredentials_jsonsecret was empty — whileaccount_json(the profile) had real data.The worker finalized capture the instant
~/.claude/.credentials.jsonexisted, but Claude creates the file then writesclaudeAiOauth.accessTokena 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).probeClaudeCredentialFilefinalizes only when populated; an existing-but-empty{}keeps polling until the token lands.captureClaudethrows 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