Repository navigation
Discover independent Claude Code profiles and label accounts by email - #1351
calvincorreli wants to merge 1 commit into
Conversation
|
Thanks for your interest in contributing to OpenUsage! External pull requests must reference an open issue that:
Please discuss the change on an issue first, wait for a maintainer to approve and assign it to you, then reopen this pull request with |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Profile discovery can omit valid email-less accounts and miss keychain credentials stored under equivalent path spellings.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Adds independent Claude Code profile discovery with account-specific labels, credentials, and usage history.
Changes:
- Discovers and merges default, configured, and sibling Claude profiles.
- Scopes credentials and history to profile ownership.
- Adds documentation and regression coverage.
| File | Description |
|---|---|
Tests/OpenUsageTests/ClaudeProviderTests.swift |
Isolates usage history test inputs. |
Tests/OpenUsageTests/ClaudeProfileHistoryTests.swift |
Tests profile history ownership. |
Tests/OpenUsageTests/ClaudeProfileAccountTests.swift |
Tests discovery, credentials, labels, and rotation. |
Tests/OpenUsageTests/ClaudeDesktopAuthStoreTests.swift |
Updates email-label expectation. |
Sources/OpenUsage/Stores/ProviderAccountsStore.swift |
Adds Claude home source type. |
Sources/OpenUsage/Services/ProviderAccountAssembly.swift |
Merges profiles into account cards. |
Sources/OpenUsage/Providers/ProviderCatalog.swift |
Configures profile-scoped runtimes. |
Sources/OpenUsage/Providers/Claude/ClaudeProfileLogin.swift |
Implements profile discovery and environment scoping. |
Sources/OpenUsage/Providers/Claude/ClaudeLogUsageScanner.swift |
Attributes history to current profile owners. |
Sources/OpenUsage/Providers/Claude/ClaudeAuthStore.swift |
Loads and saves profile-specific credentials. |
docs/providers/claude.md |
Documents separate profiles. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| homes += listDirectories(userHome.path).filter { $0.hasPrefix(".claude-") }.sorted() | ||
| .map { let path = userHome.appendingPathComponent($0).path; return (path, path) } |
| let email = account.emailAddress?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty, | ||
| let org = account.organizationUuid?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty, | ||
| let identity = DefaultAccountObserver.claudeIdentityKey(account) |
There was a problem hiding this comment.
Important
Two existing single-account behaviors regress: a healthy CLI login now eagerly accesses and prefers Desktop credentials, and standard-profile history no longer includes the XDG Claude root.
Reviewed changes: Reviewed profile discovery, account assembly, credential routing and persistence, history attribution, regression tests, documentation, and the account-label screenshot.
- Profile Discovery: Finds the standard, configured, and sibling Claude homes and merges matching account/organization identities while preserving card IDs.
- Account Labels: Uses profile emails and adds organization disambiguation for duplicate profile emails.
- Credential Routing: Loads profile-specific credentials, retains Desktop and Swap sources, and saves rotated file tokens to their original paths.
- History Attribution: Scans matching profile homes, rechecks their current ownership, and retains explicit session and Desktop-index attribution.
- Verification: Reviewed the added tests and traced the affected runtime paths; the full SwiftPM test suite could not run on Linux because the package requires macOS frameworks.
gpt-6.1-sol | 𝕏
| $0.hasUsableAccessToken && liveUsageAvailability($0) == .available | ||
| } | ||
| if swapAccount != nil || forceDesktopFallback || !hasUsableCLILogin || preferOrganizationScopedDesktop { | ||
| if profile != nil || swapAccount != nil || forceDesktopFallback || !hasUsableCLILogin || preferOrganizationScopedDesktop { |
There was a problem hiding this comment.
[P2] Preserve CLI-first loading for healthy single-account profiles. The standard login is now attached as a profile, so this condition loads Desktop even when hasUsableCLILogin is true: a manual refresh can open the unrelated Safe Storage permission dialog despite already having working CLI credentials. When access is granted, the subsequent insertion also puts Desktop ahead of those credentials for a single non-Swap account, reversing the existing source preference.
Technical details
## Affected Sites
- [ClaudeAuthStore.swift:171-186](https://github.com/robinebers/openusage/blob/a86732b04c35e0397179414f4367311addb03984/Sources/OpenUsage/Providers/Claude/ClaudeAuthStore.swift#L171-L186): profile presence unconditionally triggers Desktop loading, and Desktop is inserted at index zero without Swap.
- [ProviderAccountAssembly.swift:284-300](https://github.com/robinebers/openusage/blob/a86732b04c35e0397179414f4367311addb03984/Sources/OpenUsage/Services/ProviderAccountAssembly.swift#L284-L300): ordinary default-home logins acquire a profile too.
- ClaudeDesktopAuthStore.load passes allowInteraction to Safe Storage access; ClaudeProvider passes true for manual refreshes.
## Required Outcome
- Preserve a usable single-account CLI login as the first source without requesting Desktop permission unnecessarily.
- Retain Desktop fallback after CLI auth failure and the intentional organization-scoped preference for multiple accounts.
- Add coverage with a usable default profile plus matching Desktop material, checking both Safe Storage calls and candidate order.| let identity = claudeIdentityKeys[card.id] ?? card.identityKey | ||
| let user = identity.split(separator: "|").first.map(String.init) | ||
| let scanner = ClaudeLogUsageScanner( | ||
| environment: ClaudeProfileEnvironment(base: ProcessEnvironmentReader(), profile: card.profile), |
There was a problem hiding this comment.
[P2] Keep the existing default history roots for the standard profile. Wrapping the ordinary ~/.claude profile sets CLAUDE_CONFIG_DIR even when the user has no override, so the scanner takes its explicit-root branch and stops reading $XDG_CONFIG_HOME/claude (or ~/.config/claude). An installation with a normal login and history in that previously supported root therefore loses those tokens and spend estimates after this change.
Technical details
## Affected Sites
- [ProviderCatalog.swift:23](https://github.com/robinebers/openusage/blob/a86732b04c35e0397179414f4367311addb03984/Sources/OpenUsage/Providers/ProviderCatalog.swift#L23): all profiles, including the standard home, receive a forced history override.
- [ClaudeProfileLogin.swift:57-69](https://github.com/robinebers/openusage/blob/a86732b04c35e0397179414f4367311addb03984/Sources/OpenUsage/Providers/Claude/ClaudeProfileLogin.swift#L57-L69): the wrapper always returns profile.home for CLAUDE_CONFIG_DIR.
- [ClaudeLogUsageScanner.swift:205-228](https://github.com/robinebers/openusage/blob/a86732b04c35e0397179414f4367311addb03984/Sources/OpenUsage/Providers/Claude/ClaudeLogUsageScanner.swift#L205-L228): XDG roots are included only when CLAUDE_CONFIG_DIR is absent; neither additionalConfigDirectories nor ownedProfileDirectories restores them.
## Required Outcome
- Preserve the pre-existing default-root behavior for the standard profile when no explicit override applies, while keeping independent custom profiles isolated.
- Keep ownership filtering consistent with the restored roots.
- Add a regression fixture with a standard-home account and usage under the XDG Claude projects directory, exercising the profile-specific environment used by ProviderCatalog.

TL;DR
Discover independent Claude Code profiles and label their account cards by email. Users with separate
~/.claude-*logins can see each subscription without switching the default shell login.What was happening
What this changes
CLAUDE_CONFIG_DIR, and sibling~/.claude-*directories.Tests
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test: 1,568 XCTest tests, zero failures, three skipped; Swift Testing checks also pass.Screenshots
Native account headers rendered from three synthetic profile fixtures:
Related issue: #1214. Awaiting maintainer approval and assignment before reopening.