Skip to content

feat(credentials): Per-profile keychain source pin + auto-refresh#242

Open
Taeknology wants to merge 1 commit into
hamed-elfayome:mainfrom
Taeknology:feat/keychain-source-pin
Open

feat(credentials): Per-profile keychain source pin + auto-refresh#242
Taeknology wants to merge 1 commit into
hamed-elfayome:mainfrom
Taeknology:feat/keychain-source-pin

Conversation

@Taeknology

Copy link
Copy Markdown

Fixes #239.

Summary

Multi-account CLAUDE_CONFIG_DIR setups currently lose usage tracking for non-active profiles after Claude Code rotates their OAuth tokens. This PR:

  1. Per-profile credentials source pin — each profile can target a specific Claude Code-credentials-<HASH> keychain entry (the same entry Claude Code itself rotates).
  2. Source-aware token refresh — when the pinned token is expired, Tracker performs the OAuth refresh_token grant against https://platform.claude.com/v1/oauth/token and writes the new tokens back to both the keychain entry and the profile cache. Falls back to the profile's cached cliCredentialsJSON if the pinned keychain payload is unparseable.
  3. Advanced UI — picker under CLI Account → Advanced — Credentials source populated via SecItemCopyMatching (no shell dump-keychain subprocess, which has been observed to hang on macOS 26.x), with human-readable labels assembled from .claude.json oauthAccount metadata across ~/.claude* config dirs.

No breaking changes; the new optional Profile.customKeychainServiceName field decodes via decodeIfPresent, so existing profiles_v3 plists load unchanged.

Files changed

File Change
Claude Usage/Shared/Models/Profile.swift customKeychainServiceName: String? + init param; hasAnyCredentials updated
Claude Usage/Shared/Services/ClaudeCodeSyncService.swift OAuth refresh + per-svc keychain read/write + entry enumeration (SecItemCopyMatching) + discoverAccountLabels (resolves emails via .claude.json)
Claude Usage/MenuBar/MenuBarManager.swift Filter widened from hasUsageCredentials to hasAnyCredentials; fetchUsageForProfile Priority 2 calls ensureFreshCredentials
Claude Usage/Views/Settings/Credentials/CLIAccountView.swift New "Advanced — Credentials source" section (Picker + Refresh)
Claude UsageTests/ClaudeUsageTests.swift 11 unit tests covering Codable back-compat, keychain-hash algorithm, mergeRefreshedCredentials correctness, KeychainEntryDescription.displayLabel (5 branches)

Implementation notes

  • Reverse-engineered (and locked by a unit test): Claude Code-credentials-<HASH> is Claude Code-credentials- + sha256(absolute CLAUDE_CONFIG_DIR path).hex()[:8]. Verified against live entries across multiple config dirs.
  • discoverAccountLabels() walks ~/.claude and ~/.claude-* dirs, reads each .claude.json oauthAccount, computes the path hash, and maps it to the corresponding keychain entry so the picker shows <email> — <org> · <hash> instead of bare hashes.
  • writeKeychainCredentials uses a single atomic add -U (not delete-then-add) and mergeRefreshedCredentials emits single-line JSON — both observed-and-fixed during testing to avoid security -w payload corruption.
  • OAuth client_id defaults to the public Claude Code value and is overridable via CLAUDE_CODE_OAUTH_CLIENT_ID env var, matching the convention used in Claude Code's own HUD.

Test plan

Tested locally with two CLAUDE_CONFIG_DIR accounts:

  1. Set up two CLI Account profiles, sync each.
  2. Wait long enough that the cached refresh_token for the non-active profile becomes stale (Claude Code rotates within hours of active use).
  3. Confirm Tracker logs HTTP 400 invalid_grant on the affected profile and lastWeeklyRecordTime_<id> stops advancing.
  4. Open CLI Account → Advanced — Credentials source, select the matching Claude Code-credentials-<hash> entry for that profile.
  5. Confirm the next refresh cycle re-anchors to the keychain entry, lastWeeklyRecordTime_<id> advances to today, no further invalid_grant.

Unit tests run via Xcode's Test action; all 11 new cases pass alongside the existing suite (** TEST SUCCEEDED **).

Caveats

  • macOS 14+ for CryptoKit (already the project's minimum target).
  • discoverAccountLabels walks the home directory, so picker labels assume .claude* config dirs live there — fine for the common pattern (~/.claude-work, ~/.claude-personal, etc.). Exotic locations will still show as raw hashes; the pin still works.

Related

The 5-hour / weekly reset notifications follow-up is split into a separate PR for #241 — it depends on this PR for non-active-profile reliability but is otherwise independent.

…elfayome#239)

For multi-account CLAUDE_CONFIG_DIR setups, non-active profiles silently stop
tracking usage once their cached refresh_token falls behind Claude Code's
rotation (HTTP 400 invalid_grant). This adds:

- Profile.customKeychainServiceName: pin a profile to a specific
  `Claude Code-credentials-<HASH>` keychain entry as its source of truth.
- ClaudeCodeSyncService.ensureFreshCredentials: source-aware; reads fresh
  tokens directly from the pinned entry, falls back to the cached cliJSON when
  the pinned payload is unparseable, auto-refreshes via the OAuth refresh_token
  grant against platform.claude.com when expired, and writes rotated tokens
  back to BOTH the keychain entry and the profile cache.
- listClaudeCodeKeychainServices via SecItemCopyMatching (no shell subprocess,
  no dump-keychain hang on macOS 26.x) + describeKeychainEntry that resolves
  account labels by walking ~/.claude* config dirs and matching their
  oauthAccount via the path hash (sha256(absolute path).hex()[:8]).
- CLIAccountView 'Advanced — Credentials source' picker.
- MenuBarManager: filter widened from hasUsageCredentials to hasAnyCredentials
  so expired-but-refreshable profiles still get polled; fetchUsageForProfile
  Priority 2 now routes through ensureFreshCredentials.
- Unit tests covering Codable back-compat, the keychain-hash algorithm,
  mergeRefreshedCredentials correctness, and KeychainEntryDescription.displayLabel.

No breaking changes; new optional Profile field decodes via decodeIfPresent.
@Taeknology Taeknology changed the title feat: Per-profile keychain source pin + auto-refresh (fixes #239) feat(credentials): Per-profile keychain source pin + auto-refresh May 18, 2026
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.

[Bug] Multi-account profiles silently stop tracking after Claude Code rotates tokens

1 participant