Skip to content

fix(usage): read keychain credential by account, not service name alone - #532

Merged
sirmalloc merged 2 commits into
sirmalloc:mainfrom
vishnujayvel:fix/keychain-multi-account
Oct 29, 2026
Merged

sirmalloc merged 2 commits into
sirmalloc:mainfrom
vishnujayvel:fix/keychain-multi-account

Conversation

@vishnujayvel

@vishnujayvel vishnujayvel commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

On macOS, multiple Keychain items can share the service Claude Code-credentials while differing by account. A service-only lookup can select an MCP-only item even when the OS-username item contains valid Claude Code OAuth credentials, leaving usage widgets showing [No credentials].

Fixes #521's missing-token case.

Change

For the default service lookup, getUsageCredentials() now tries:

  1. Claude Code-credentials by service name, as before.
  2. The same service scoped to os.userInfo().username when the first lookup yields no OAuth token.
  3. The existing suffixed-service candidates and credentials-file fallbacks.

The account-scoped read returns both access and refresh tokens, preserving the current cache identity behavior. If the username cannot be resolved, that retry is skipped. Existing five-second subprocess timeouts and the profile-specific no-credentials backoff remain in place.

Main has been merged into this branch. Custom profiles retain main's isolation: they read their own suffixed service, then only their own .credentials.json, without falling through to the default account. Suffixed services can contain profile credentials as well as MCP credentials; they are not exclusively MCP tokens.

Scope and related work

A successful service-only lookup still wins. This does not resolve the variant where the first item contains a stale but well-formed OAuth token, or guess arbitrary account names. The new username retry applies to the default service lookup.

Validation

  • The original missing-token regression fails on main and passes with the account-scoped fallback using mocked Keychain entries.
  • The adapted regression asserts that both access and refresh tokens are retained. Existing tests cover username lookup failure, service-only precedence, fallback ordering, profile isolation, and command timeout options.
  • bun test: 3,507 passed, 0 failed across 191 files on macOS with Bun 1.4.4. The run required local socket access for the proxy integration tests.
  • bun run lint, bun run build, and git diff --check: passed.

The original contributor reported a successful live reproduction on an affected machine. Validation of the main merge used mocks and the automated suite; it did not access a real Keychain or live usage API.

@sirmalloc
sirmalloc merged commit dd38d21 into sirmalloc:main Oct 29, 2026
3 checks passed
pcvelz added a commit to pcvelz/ccstatusline-usage that referenced this pull request Oct 29, 2026
Upstream: Extra Usage Limit widget (sirmalloc#756), live widget preview while picking (sirmalloc#755), Fast Mode widget (sirmalloc#754), transcript block fallback fixes (sirmalloc#752), editor keys on a global install (sirmalloc#751), git cache TTL (sirmalloc#750), npx cached copy skip (sirmalloc#749), Block Usage hide at 100% (sirmalloc#747), Block Reset Timer hide until 100% (sirmalloc#697), fish-style branch abbreviation (sirmalloc#614), jj detached HEAD (sirmalloc#705), hooks save warning (sirmalloc#703), dev reload (sirmalloc#700), merge ends at a hidden widget (sirmalloc#698), Block Forecast and Block Limit Timer widgets (sirmalloc#699), contributor guide (sirmalloc#706), POSIX path case fix (sirmalloc#707), whitespace-distinct conflict filenames (sirmalloc#709), Quick Start reorder (sirmalloc#715), bunx launch guidance (sirmalloc#714), jj diff summary counts (sirmalloc#711), powerline manual separators (sirmalloc#713), keychain credential by account (sirmalloc#532), Keychain probe timeout (sirmalloc#760), usage cache scoped per CLAUDE_CONFIG_DIR (sirmalloc#266), test isolation (sirmalloc#717, sirmalloc#753), dev-dependency bumps (sirmalloc#745)

Conflicts resolved per the standard-resolution table; fork deletions re-applied.

Hand edits outside conflicts:
- src/utils/global-command-resolution.ts: fork-rename (ccstatusline@ -> ccstatusline-usage@ in upstream's new bunx pattern)
- src/utils/__tests__/global-package-manager.test.ts: fork-rename (package dir ccstatusline-usage in upstream's new tests)
- src/utils/usage-fetch.ts: compat fix - cacheUsageData gained a fetchedAtMs arg (upstream sirmalloc#266); adapted the fork-only fetchFreshUsageData call site
- src/utils/__tests__/usage-fetch.test.ts: compat fixes - cache/lock now hashed per CLAUDE_CONFIG_DIR (usageFilePath helper) in four fork tests; upstream fableUsage renamed to fork's weeklyFableUsage; seedUsageCache takes TokenHome
- src/utils/__tests__/usage-prefetch.test.ts: compat fix - mockFetchUsageData renamed to upstream's mockFetchUsage in cleanly-merged tests
- README.md: took upstream's Quick Start move (sirmalloc#715) and dropped the fork's old copy; fork-rename of its ccstatusline@ specifiers; links to fork-deleted docs (docs/USAGE.md, docs/WINDOWS.md) replaced with the fork's inline text
- CONTRIBUTING.md: links to the fork-deleted docs/DEVELOPMENT.md pointed at the README Development section
- docs/test-retirements.md: ledger entries for the 12 upstream test renames (sirmalloc#697, sirmalloc#698, sirmalloc#700, sirmalloc#705, sirmalloc#713, sirmalloc#760)
eric-engberg added a commit to eric-engberg/ccstatusline that referenced this pull request Oct 30, 2026
On macOS the usage credential lookup ran `security find-generic-password
-s "Claude Code-credentials" -w`, naming no account. Claude Code reads
and writes its login with `-a <account>` as well: $USER, the login name
when that's unset or empty, and claude-code-user for a name with
characters outside [a-zA-Z0-9._-]. A login keychain can hold more than
one item for the service. Running Claude Code as root once leaves one
under the account "root" that nothing refreshes again, and a lookup by
service alone answers with whichever item macOS finds first.

On one Mac that was root's. Its access token expired about 8 hours
after it was saved, and from then on every usage request sent that
expired token while Claude Code kept refreshing the user's own item.
The usage widgets stopped updating for about two days, until the stale
item was deleted by hand.

Each service is now looked up under that account first. Only when no
item is saved under it (exit status 44) does the lookup fall back to the
service alone, so a setup whose item sits under another account keeps
working. Any other failure of the first call still ends the keychain
lookup and backs off as before, and each profile's service name is
unchanged.

Upstream sirmalloc#532 retries the service under the OS user name only after a
service-only lookup finds no OAuth token, so a stale but well-formed item
another account left still wins there. Looking up the account first, as
here, covers its case too (an item holding only MCP tokens), so its retry
is replaced.
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.

All usage widgets show [No credentials] on macOS when the login keychain has multiple Claude Code-credentials items (different accounts)

2 participants