Skip to content

feat(limits): track several Claude accounts - #327

Open
DyRize wants to merge 11 commits into
chattymin:mainfrom
DyRize:feat/multi-account-claude-limits
Open

DyRize wants to merge 11 commits into
chattymin:mainfrom
DyRize:feat/multi-account-claude-limits

Conversation

@DyRize

@DyRize DyRize commented Sep 17, 2026

Copy link
Copy Markdown

Summary

Many people run two Claude subscriptions side by side, for example an employer Team plan and a personal Pro plan, each logged in its own CLAUDE_CONFIG_DIR. Token counting already follows extra config folders, but the official limits only read the default login, so the popover and the menu bar keep showing that account while you work on the other one. This PR shows every Claude account the machine is logged in to.

Finding the accounts

  • ~/.claude-* and ~/.claude_* folders whose .claude.json holds a signed-in account, plus the CLAUDE_CONFIG_DIR exported by the login shell (already resolved by UsageEnvironment), plus a new "Additional Claude accounts" field in Settings → Advanced for folders stored elsewhere. The field only adds folders, like the custom scan roots. Many people set CLAUDE_CONFIG_DIR in a shell alias only, which is why discovery looks at the folders too.
  • Discovery and every read of real account files only run in the bundled app (AppEnv.isBundledApp), so swift test never touches real folders or the Keychain. .claude.json is parsed again only when its modification date or size changes.
  • Claude Code keeps a non-default login under the Keychain service Claude Code-credentials-<first 8 hex chars of SHA-256(CLAUDE_CONFIG_DIR)> (/Users/example/.claude-work maps to -dd1118a7), or in <config dir>/.credentials.json. It hashes the variable as given: claude auth status (2.1.274) reports the same folder exported with a trailing slash as logged out. The app therefore tries the folder path, then the same path with a trailing slash (what shell completion writes). A missing item moves on without a prompt, and a cancelled prompt stops the lookup (fix(limits): prioritize user account over MCP placeholders to prevent duplicate Keychain prompts #280).
  • Accounts logged in to several folders are shown once (email + organization).

Keychain rules are unchanged. Automatic polling never reads the Keychain. A manual refresh may prompt for each account. "Disable Keychain access" stops every account. With a claude.ai session key set, a manual refresh only prompts for folders listed by hand in Settings.

Popover

  • With two or more accounts, "Limits (official)" gets one tab per account, in the same capsule style as the provider tabs (CapsuleTabBar is extracted from them, and their rendering is unchanged). With a single account nothing changes.
  • Tab title: the organization for team plans, the email for personal plans (their generated organization name repeats the email), the folder name when the profile is unknown. The selected tab is remembered.
  • Each tab shows plan, account, all limit gauges, its own current 5h block, and today's and this month's tokens and cost for that account.
  • A tab's current 5h block counts that account's turns in its running official window and ends at the account's reset. The local rolling block (first turn of the last five hours, plus five hours) rarely matched it, and the machine-wide one mixed accounts. With no session running there is no block, and a 5-hour gauge at 0% without a reset now says "Starts with your next message" instead of showing nothing. Expired or missing limits keep the rolling block.
  • 401/403 marks only that account as expired: its last values stay greyed out, with a hint to run Claude Code once with CLAUDE_CONFIG_DIR=<folder> and a Retry button. 429 backoff is per account, so a rate-limited account no longer pauses the others.

Tracked account. A new setting in Settings → General, shown only when there are several accounts: Automatic (last used, from the modification date of each folder's history.jsonl), Highest usage, or one specific account. It drives the menu bar percentage, the warning state and companion mood, the floating pet hover, and the 5h forecast, which now uses that account's own 5h block.

Tokens and cost per account

  • Each usage entry keeps its session id, taken from the transcript path. Subagent transcripts count with their parent session. Cached entries get it on read, without rescanning files.
  • Each exchange goes to the account that sent the session's last prompt before it, according to each folder's history.jsonl. The file is read incrementally, and prompt text is ignored. A session resumed on another account changes owner at that point.
  • Sessions missing from every history (-p, SDK, older than the history) are not guessed. They appear as "Not linked to an account this month".
  • Every folder's projects/ is added to the Claude scan roots. A projects/ symlinked to ~/.claude/projects collapses into the default root.
  • This is only computed with several accounts, off the main thread, from the existing usage cache. The top totals stay machine-wide.
  • Checked on real data (two accounts, one month): 7,363 exchanges, all linked to a session. The per-account sums plus the unlinked part match the monthly total exactly. A session started on one account and resumed on the other is split at the switch.

Candy and alerts apply to every gauge of every account. With several accounts, each notification names its account. Alerts put it after the percentage ("Claude 5-hour session at 96% · Acme Corp"), and the floating pet bubble, cut at two lines, drops the name when it does not fit, so the percentage always shows.

  • The default account keeps its keys (claude.fiveHour, claude.sevenDay), so existing saves stay valid. Other accounts use claude.<8 hex chars of the folder hash>.fiveHour, so removing and adding a folder back doesn't pay twice.
  • A gauge of an additional account only pays after it has been seen below 100% (no retroactive candy).

Claude 5 pricing. claude-opus-5 and claude-sonnet-5 were missing from ModelPricing, so a day on Opus 5 showed "$—" in the menu bar and a zero cost per account. They now use the published rates (https://platform.claude.com/docs/en/about-claude/pricing): Opus 5 $5 / $25 per million (5-minute cache write $6.25, cache hit $0.50), Sonnet 5 $2 / $10 ($2.50, $0.20). The existing test that kept claude-opus-5 unpriced now checks both rates, and later ids such as claude-opus-5-1 stay unpriced. Checked on a real day of Opus 5 use: 127.6M tokens, about $99 instead of "$—". It is a separate commit (fix(pricing): …), so it can move to its own PR if you prefer.

Docs. README (en, ko, ja) describes the new data sources (other config folders, history.jsonl, the per-folder Keychain items) in the privacy notes.

Relation to #201. #201 attributes token counts to accounts with an epoch ledger that assumes one live account at a time. This PR handles accounts that are live at the same time, and attributes usage through each folder's history.jsonl. I'm happy to align the two designs.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Other:

UI changes

Before After
One "Limits (official)" block for the default login, whatever account is in use. With two or more accounts, one tab per account above the limits block. The tracked account is marked with a dot. Each tab shows plan, account, gauges, and Today / This month tokens and cost. Unchanged with a single account.
Menu bar percentage, warning state and 5h forecast follow the default login. They follow the tracked account (new "Tracked Claude account" picker in Settings → General, only with several accounts). With several accounts, hovering the menu bar item names that account.
"Claude current 5h block" below the limits, summing the whole machine, with a reset 5 hours after the first turn of the last five hours. With several accounts, in each tab: that account's turns in its running 5h window, reset at the account's official reset. Unchanged with a single account.
A 5-hour session that has not started shows 0% and no reset. "Starts with your next message".
A failed manual refresh (e.g. rate limited) only shows its reason in Settings, so the button looks inert. The reason shows under the refresh row. With several accounts it starts with the default account's title ("Acme Corp · …"), the only account that error is about.
Settings → Advanced: session key hint. Adds "Applies to the default Claude account (~/.claude) only." with several accounts. New "Additional Claude accounts" field, listing the detected folders.
The expired-session banner text is cut to one line. It wraps.
Settings → Advanced: the refresh error is cut at two lines. It wraps, and names the default account when there are several.
Limit alerts and candy notifications say "Claude 5-hour session at 96%". With several accounts they name the account ("… at 96% · Acme Corp").
A day on Claude Opus 5 shows "$—" in the menu bar. It shows the estimated cost.
Tracked account tab (Acme Corp) Other account Expired account
pr-en-tab-corp pr-en-tab-personal pr-en-tab-expired
Settings → General Settings → Advanced
pr-en-settings-tracked pr-en-settings-accounts

Demo data: the two tabs add up to the machine-wide totals (38.4M + 7.7M = 46.1M tokens, $41.85 + $8.37 = $50.22), and 640K tokens belong to a session missing from both histories.

Checklist

  • swift build and swift test pass locally (1173 tests, 11 skipped, 0 failures; scripts/test-gate.sh passes at 93.91% line coverage of the logic core)
  • PR title and description are written in English
  • UI changes are described above (before/after — images optional)
  • No copyrighted assets, secrets, or private tooling references are committed (see CONTRIBUTING)
  • Tests were added or updated for this change (AdditionalClaudeAccountsTests, plus cases in KeychainAutoPathTests, LocalizationInterpolationTests and LocalUsageReaderTests; each new branch was checked by injecting a defect and watching a test fail)

Open questions

  1. Keychain prompts per account. Claude Code creates its items with an apple-tool: partition. For an app outside that partition, macOS asks twice per item, even with "Always Allow": once for access, then once more after an "ACL partition mismatch" (seen in the securityd log). With "Always Allow" this happens once per app signature (every build for an ad-hoc signed dev build). The app reads each item once per manual refresh (the silent read never prompts), so this is macOS behavior, but every extra account adds its own prompts, and a claude.ai session key only removes the default account's. Is a per-account session key worth a follow-up?
  2. Opting out of discovery. A detected folder can only be hidden by renaming it or signing out. Would you prefer a "detect automatically" toggle or an exclusion list?
  3. Menu bar. It shows the tracked account only. Is a combined "all accounts" mode worth a follow-up?
  4. No retroactive candy. A gauge of a newly added account pays only after it has been seen below 100% (remembered per gauge). The default account keeps its current behavior. OK?
  5. Unusual CLAUDE_CONFIG_DIR spellings. The app covers the folder path with and without a trailing slash. A value such as ~/./.claude-work or ~/.claude-work// maps to yet another Keychain item that the app does not try. Worth reading the exported value verbatim instead?
  6. Single-account 5h block. With one account the "Claude current 5h block" row keeps the existing rolling block, whose reset rarely matches the official one. Should it follow the official window too, as the tabs now do?
  7. Cost of the per-account split. It adds a third read of the usage cache per refresh when several accounts exist (20 to 60 ms warm for about 7,500 entries). I kept it simple rather than add another cache.

@DyRize

DyRize commented Sep 17, 2026

Copy link
Copy Markdown
Author

Heads-up on the pricing commit (333bc20, fix(pricing): price Claude Opus 5 and Sonnet 5): #304 and #313 were opened earlier for the same fix (#303, #312) and add the same two rows at the same rates. If one of them lands first, I'll drop 333bc20 from this branch and rebase, so the multi-account part doesn't depend on it.

@justinjeong5

Copy link
Copy Markdown
Contributor

Hi @DyRize! First off, thank you for tackling multi-account tracking here. The scope, attention to edge cases, and test suite are really impressive.

Having recently touched the Keychain prompt handling and session-key flow in #275 and #280, I wanted to share a few thoughts and trade-offs on the open questions you raised:

  1. Keychain prompts per account:
    Given how macOS enforces ACL partition mismatches for items in the apple-tool: partition, completely avoiding prompts on fresh signatures is tricky without an explicit session key. In our experience with fix(limits): prioritize user account over MCP placeholders to prevent duplicate Keychain prompts #280, the most helpful principle was containing the blast radius—making sure prompts don't cascade across accounts automatically, and only allowing a prompt when the user explicitly triggers a refresh on that specific account's tab. Stopping on errSecUserCanceled is a great safety net here. A per-account session key could be a clean follow-up if users find the initial Keychain prompts cumbersome.

  2. Opting out of discovery:
    A simple enable/disable toggle or exclusion list in Settings -> Advanced seems worthwhile. Developer machines often accumulate stale or experimental folders (e.g. ~/.claude-test, ~/.claude-backup). Giving users a way to hide or ignore specific folders could prevent phantom tabs from cluttering the popover.

  3. Menu bar tracking target:
    Focusing the menu bar on a single chosen "Tracked account" feels like the most predictable UX. Because different accounts run on completely independent 5h quota curves and reset timestamps, trying to aggregate or blend them into one metric would likely be confusing to interpret.

  4. Cache read overhead (20–60 ms):
    Since this happens off the main thread, 20–60 ms during periodic refreshes feels quite reasonable. Keeping the data flow straightforward without adding another layer of cache seems like a healthy trade-off against cache invalidation complexity.

Really excited about this direction. Hope these observations are helpful, and happy to help test the Keychain flow if needed!


🤖 Generated with Antigravity 2.0 (AGY 2.0)

Two Claude Code logins side by side (CLAUDE_CONFIG_DIR) only showed the
default account's limits. Additional config folders are now detected
(~/.claude-* and ~/.claude_* folders Claude Code is logged in to, plus an
exported CLAUDE_CONFIG_DIR) or listed in Settings > Advanced, and each
account gets its own tab in the official limits.

- One token cache per folder: its .credentials.json, or the
  "Claude Code-credentials-<sha256 prefix>" Keychain item. Automatic polls
  never read the Keychain; a manual refresh may prompt once per folder and
  stops after a declined prompt (chattymin#280).
- Candy and threshold alerts cover every account's gauges. The default
  account keeps its historical keys, so existing saves carry over.
- A rejected token marks that account expired (dimmed values, retry
  banner) instead of pinning the refresh row. A 429 pauses that account
  only. The default tab stays while its limits load.
- The refresh row shows why a manual refresh failed.
- The profile cache keeps one identity per token.
- Menu bar percentage, warning state, companion mood, floating pet hover
  and the 5h forecast still follow the default account.
With several Claude accounts, the menu bar percentage, warning state,
companion mood, floating pet hover and 5h forecast followed the default
account only, even while working on another one. A "Tracked Claude
account" setting, shown when several accounts are listed, now drives them:

- Automatic (default): the account that received the latest prompt, read
  from the modification date of each config folder's history.jsonl, which
  Claude Code never shares between logins.
- The default account, a pinned account (back to automatic when it
  disappears), or the highest usage across accounts.

The tracked tab gets a dot, the 5h forecast shows in its tab, and each
additional account shows its own stale label after 15 minutes.
Transcripts carry no account and projects/ may be shared between logins,
but each login's history.jsonl lists its prompts with their session and
time. Usage entries now keep their session (from the transcript path,
also for blobs cached before this change, without a re-parse), and each
turn goes to the login that sent the latest prompt of its session before
it, so a session resumed on another login changes owner at that point.

Each account tab shows its today and month tokens and cost; turns no
history explains are counted as unattributed and shown under the tabs.
The header totals stay machine-wide. The default account's placeholder
tab now only appears when that login exists. README (EN/KO/JA) documents
the new data sources and the Keychain reads per config folder.
Account folders that keep their own projects/ were never scanned, so
their turns were missing from every total. Each config folder now adds
its projects/ to the Claude scan roots; a symlinked shared folder folds
into the default one.

Review fixes on the multi-account work:

- history.jsonl is read again only for its new complete lines, and a
  saved login is read again only when its file changes.
- The 5h forecast uses the tracked account's own block with several
  accounts, and the scan covers a block that started last month.
- Additional account refreshes run one after the other. Turning Keychain
  access off clears them. With a session key, only folders listed in
  Settings may prompt.
- The same email in another organization is another account. A folder
  of an account already shown adds its activity and usage to that tab;
  usage of an account without a tab is unattributed.
- An account first seen at its limit earns no candy until seen below it.
- Changing the tracked account updates the companion at once, an
  automatic success clears the manual refresh error, and the refresh row
  waits for default limits only when the default login exists.
- History, usage and last prompt dates are read from the user's folders
  only inside the app, like the folder discovery.

README (EN/KO/JA) lists the per-folder projects/ and the incremental
history read.
With several accounts the refresh error sits above the account tabs and
read as everyone's failure. It now names the default account, the only
one it is about. The 5h block row says it sums all accounts, the session
key hint says it only covers ~/.claude, and additional account refreshes
are logged like the default one.
Claude Code hashes CLAUDE_CONFIG_DIR as given, so a folder exported with a
trailing slash (as shell completion writes it) lives under another Keychain
item. Both names are tried; a missing item moves on, a cancelled prompt
stops the lookup.

The menu bar percentage names its account in a tooltip when there are
several, and the Settings refresh error names the account too and wraps
instead of being cut at two lines.
…centage

With several accounts, only the additional ones were named in alerts and
candy notifications, and the name sat before the percentage: the floating
pet bubble, cut at two lines, could lose the percentage. Every account is
now named next to others, alerts put the account after the percentage, and
the bubble drops an account name that does not fit.
The store already falls back to the last used account; the picker showed an empty selection instead.
macOS can ask twice per Keychain item (access, then partition), and again
after every "Allow" that was not "Always Allow", so the hint now says a
manual refresh may ask for each folder.
…le session

With several accounts the current 5h block summed every account and ended at
neither account's reset. Each tab now shows its own block, like the single
account view. A 5-hour window at 0% without a reset date has not started:
the row now says it starts with the next message instead of showing nothing.
The local block starts at the first turn of the last five hours, so its reset
rarely matched the account's real one. A tab now counts the turns of the
account's running official window and ends at its reset. With no session
running there is no block; expired or missing limits keep the rolling block.
@DyRize
DyRize force-pushed the feat/multi-account-claude-limits branch from 61d8573 to 8edaca2 Compare September 21, 2026 15:40
@DyRize

DyRize commented Sep 21, 2026

Copy link
Copy Markdown
Author

Thanks @justinjeong5, this is exactly the feedback I was hoping for, and #280's prompt handling is what this branch builds on.

  1. Keychain prompts: that is the rule the branch follows. Automatic polls always read with allowKeychainPrompt: false, and a prompt only ever comes from an explicit Refresh on that account's tab. fix(limits): prioritize user account over MCP placeholders to prevent duplicate Keychain prompts #280's errSecUserCanceled / errSecAuthFailed stop is kept per account, so a cancel ends the read instead of walking on to the next service. A per-account session key is the natural follow-up: the field is already per account in Settings, so it is mostly plumbing.
  2. Opting out of discovery: agreed, and I would rather ship it as a follow-up than grow this PR. Two things limit the noise meanwhile: a folder is only picked up when its .claude.json holds an oauthAccount (a logged-out ~/.claude-backup never shows), and only ~/.claude-* and ~/.claude_* are scanned. An exclusion list in Settings -> Advanced, next to the additional folders field, is the right home for it.
  3. Menu bar tracking: that is what it does. One tracked account, picked in Settings, nothing blended. Each tab keeps its own 5h block and reset time, and the forecast row only shows on the tracked account since it mixes local usage with that account's utilization.
  4. Cache read: agreed, no extra layer.

The branch is now rebased on main: the pricing commit is dropped since #304 landed, and the account tabs are merged with the new pace marks, so the 5h row shows both the pace tick and the idle session hint.

I would happily take you up on the Keychain testing offer, the two-folder setup is the one path I cannot fully cover with tests.

@justinjeong5

Copy link
Copy Markdown
Contributor

Hi @DyRize,

I ran a local test of the two-folder setup on macOS (Apple Silicon) using a simulated secondary directory (~/.claude-work with a mock .claude.json containing oauthAccount):

  1. Automatic Discovery & Identity: ClaudeAccountRoots.discovered immediately picked up ~/.claude-work without manual configuration, correctly parsed the email and organization name, and derived the expected SHA-256 hashed Keychain services (Claude Code-credentials-<hash>).
  2. Silent Polling Contract: Verified that during automatic background polling (allowKeychainPrompt: false), the provider cleanly bails with zero Keychain queries (SecItemCopyMatching query count strictly remained 0). No system authentication dialogs or prompt cascades occurred while polling.
  3. Pending / Manual Refresh: An unauthenticated secondary folder correctly marks additionalLimitsPending = true rather than popping broken UI or triggering unsolicited prompts. On manual refresh, missing credentials fail gracefully without cascading across services.

(Note: Since I tested with a simulated secondary folder, I haven't completed an end-to-end quota fetch with a live second Anthropic account yet, but the discovery pipeline and silent Keychain guard contracts hold up solidly on real macOS!)


🤖 Generated with Antigravity 2.0 (AGY 2.0)

This branch has not been deployed

No deployments
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.

2 participants