Skip to content

Discover independent Claude Code profiles and label accounts by email - #1351

Closed
calvincorreli wants to merge 1 commit into
robinebers:mainfrom
calvincorreli:feat/claude-profile-accounts
Closed

calvincorreli wants to merge 1 commit into
robinebers:mainfrom
calvincorreli:feat/claude-profile-accounts

Conversation

@calvincorreli

@calvincorreli calvincorreli commented Oct 4, 2026 •

Copy link
Copy Markdown

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

  • Discovery covered the current CLI login, Claude Desktop, and Claude Swap, but missed independently configured Claude Code folders.
  • Organization labels such as “Personal” made multiple subscriptions hard to distinguish.

What this changes

  • Read account metadata from the default Claude home, the configured CLAUDE_CONFIG_DIR, and sibling ~/.claude-* directories.
  • Merge sources by account and organization, preserve existing card IDs, and show email labels with organization disambiguation when needed.
  • Read profile-specific keychain/file credentials and history, retaining matching Desktop and Swap fallbacks. Refreshes persist tokens back to their original source files.
  • Recheck profile ownership during history scans; explicit session ownership and Desktop indexes remain authoritative after a login changes.
  • Document profile discovery and add regression coverage. Isolate an existing Claude logs test from ambient pi history.

Tests

  • DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test: 1,568 XCTest tests, zero failures, three skipped; Swift Testing checks also pass.
  • Regression tests cover three subscriptions, same-identity merging, duplicate email labels, scoped credentials, original keychain path representation, wrong-account rejection, token rotation, malformed profiles, and history ownership changes.
  • Read-only review completed with no remaining P1/P2 findings.
  • Installed the custom build locally: default Claude home plus two independent profiles produce three email-labeled Max 20x cards, each with live Session and Weekly readings, and no provider errors.

Screenshots

Native account headers rendered from three synthetic profile fixtures:

Claude profile email labels

Related issue: #1214. Awaiting maintainer approval and assignment before reopening.

Copilot AI balanced review requested due to automatic review settings October 4, 2026 20:27
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown

Thanks for your interest in contributing to OpenUsage!

External pull requests must reference an open issue that:

  1. Has been approved by a maintainer with the approved label.
  2. Is assigned to the pull request author (@calvincorreli).

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 Fixes #123 in its description.

Read the contribution guidelines.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Medium severity

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.

Comment on lines +24 to +25
homes += listDirectories(userHome.path).filter { $0.hasPrefix(".claude-") }.sorted()
.map { let path = userHome.appendingPathComponent($0).path; return (path, path) }
Comment on lines +36 to +38
let email = account.emailAddress?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty,
let org = account.organizationUuid?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty,
let identity = DefaultAccountObserver.claudeIdentityKey(account)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

$0.hasUsableAccessToken && liveUsageAvailability($0) == .available
}
if swapAccount != nil || forceDesktopFallback || !hasUsableCLILogin || preferOrganizationScopedDesktop {
if profile != nil || swapAccount != nil || forceDesktopFallback || !hasUsableCLILogin || preferOrganizationScopedDesktop {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants