11 KiB
summary, read_when
| summary | read_when | |||
|---|---|---|---|---|
| Accepted design for Claude subscription accounts and per-account menu bar items. |
|
Claude multi-account and status item decision
Status: Phase 1 account display implemented; Phase 2 explicit account activation accepted.
Related: #1756, #1268, and the bounded Claude sign-in repair in #1811.
Accepted direction
- Use an opt-in
claude-swapadapter as the first Claude subscription multi-account source. - Normalize its results behind a provider-neutral account snapshot before adding any status item UI.
- Make per-account status items opt-in, replace the provider item for that provider, cap selection at four, and keep them mutually exclusive with Merge Icons.
- Allow an explicit click on an inactive account card to invoke exactly
cswap --switch-to <slot> --json. Keep automatic switching and session launching out of scope.
This solves the durable OAuth refresh problem without making CodexBar a second credential vault. It also avoids a Claude-only status item implementation that would need to be redesigned for Codex and other providers.
Current architecture and gap
CodexBar has three account concepts today:
- The ambient Claude OAuth credential is routed from CodexBar's cache, Claude Code's credentials file, or Claude Code's Keychain item. It represents one active credential. Claude Code-owned expired credentials delegate refresh back to the CLI; CodexBar-owned cached credentials can refresh directly.
ProviderTokenAccountstores a label and one token plus optional provider metadata. It has no refresh token or expiry model. Claude entries therefore work for session cookies, Admin API keys, or short-lived OAuth access tokens, but they are not durable multi-subscription OAuth sessions.TokenAccountUsageSnapshotandCodexAccountUsageSnapshotseparately project multi-account usage into menus. Status items remain provider-scoped:StatusItemIdentityhas onlymergedandprovider, andstatusItemsis keyed byUsageProvider.
The recently merged #1800 scopes Claude OAuth history to the routed Keychain identity. #1776 prevents CLI-runtime usage refreshes from delegating credential repair to Claude Code, while app and user-initiated repair remain available. Both changes improve single-active-account correctness; neither discovers or displays multiple subscriptions.
The closed #1707 should not be revived. It coupled account discovery, credential resolution, provider routing, menu rendering, and animation across a large patch while broadening Keychain and prompt behavior. The safer seam is a credential-free usage adapter first.
Source options
| Option | Credential ownership | Durability | Risk | Recommendation |
|---|---|---|---|---|
| First-party OAuth account vault | CodexBar | High | New login, refresh, storage, revocation, migration, and security surface | Defer |
Bounded claude-swap adapter |
claude-swap |
High | External executable and schema dependency | Phase 1–2 |
| Discover Claude Code Keychain entries | Claude Code / ambiguous | Unknown | Undocumented enumeration; prompt and identity hazards | Reject |
| Existing token accounts | CodexBar config | Low for OAuth | Access token expires without refresh metadata | Keep for current cookie/API-key uses |
As of claude-swap v0.18.0,
cswap --list --json still returns a versioned object with schemaVersion: 1, an active account number, account slots,
redaction-sensitive email labels, 5-hour and 7-day usage percentages, and reset timestamps. Handled failures return an
error object and non-zero exit. Direct switching returns the same versioned envelope. CodexBar does not need
--token-status, credential files, Keychain access, or raw OAuth values for display or explicit activation.
Phase 1 adapter contract
- Disabled by default. User chooses an executable path and enables “Read accounts from claude-swap.”
- Execute exactly the argument array
cswap --list --json. Never invoke a shell or accept config-defined passthrough arguments. - Require
schemaVersion == 1; reject unknown versions and partial top-level shapes. - Bound runtime and stdout, terminate on timeout, and retain the last successful snapshot with a stale marker.
- Parse only slot number, active state, usage status, 5-hour/7-day percentages, and reset timestamps.
- Treat email as display-only sensitive data. Never log or persist it. Respect Hide Personal Info.
- Use the source-issued numeric slot for identity (
claude-swap:<slot>), not email or credential-derived values. - CodexBar never reads
claude-swapstorage, Claude Code storage, environment credentials, or Keychain entries. The subprocess remains solely responsible for its own credential access. The adapter copies only allow-listed usage/identity fields into its model and never logs or persists raw stdout. - Never run
auto,run,--switch,--switch-to,--add-account, export, import, purge, or any other command in Phase 1. - Isolate adapter failure from ambient Claude usage. Users without
claude-swapsee no behavior change.
The executable is an optional external dependency, not a bundled component. Preferences should show detected version, last refresh, adapter errors, and a link to the upstream project; CodexBar should not install or update it.
Phase 2 explicit activation contract
- Only an explicit click on an inactive, actionable account card can start a switch.
- Derive the numeric slot from the already validated account snapshot and execute exactly
cswap --switch-to <slot> --json; never accept free-form arguments or invoke a shell. - Serialize switches, validate
schemaVersion == 1and the returned target slot, and bound captured output. - Once launched, let the external credential transaction reach its natural exit without forced timeout or cancellation. If the adapter setting changes, hide its UI state and discard its result when the original configuration is no longer current.
- Refresh ambient Claude usage and the adapter account list after completion. Show switch errors independently from list-refresh errors and preserve the last successful usage snapshots.
- Keep expired, missing, unknown, and Keychain-inaccessible credential slots non-actionable. Never auto-switch, launch sessions, add/import/export/purge accounts, or mutate credentials directly.
Provider-neutral account model
Introduce one projection used by menus and status items rather than teaching status item code about Claude OAuth:
struct ProviderAccountUsageSnapshot: Identifiable {
let id: ProviderAccountIdentity
let provider: UsageProvider
let displayLabel: String
let isActive: Bool
let canActivate: Bool
let snapshot: UsageSnapshot?
let error: String?
let sourceLabel: String?
}
struct ProviderAccountIdentity: Hashable {
let source: String
let opaqueID: String
}
Adapters own identity conversion. UI receives a user alias or privacy-safe ordinal when personal information is hidden. No provider may fill identity, plan, or usage fields using another provider's data.
Existing TokenAccountUsageSnapshot and CodexAccountUsageSnapshot can migrate behind this projection in small,
separately reviewed steps. Their credential and refresh logic stays source-specific.
Per-account status item behavior
Proposed setting under each provider's Accounts section:
One provider icon(default; current behavior)Selected account icons, with up to four account checkboxes
Selecting account icons replaces that provider's aggregate item; it does not add duplicates. Account items use a
stable StatusItemIdentity.account(provider:source:opaqueID:), preserve existing provider autosave names, and open the
provider menu focused on that account. A short user alias or ordinal badge distinguishes otherwise identical provider
icons. Hide Personal Info replaces labels with Account 1, Account 2, and so on.
Merge Icons continues to mean exactly one status item. Account-icon controls are disabled while it is enabled, with a button to turn Merge Icons off. Existing users and status item positions remain unchanged until they opt in.
The alternative proposed in the #1268 discussion is a per-account toggle that adds selected account items, leaves unselected accounts under the provider item, and coexists with Merge Icons. That is more granular, but it creates duplicate provider/account items, makes “Merge Icons” no longer mean one item, and multiplies autosave and recovery states. The replacement mode above is the recommendation; if maintainers prefer the additive mode, grouping and Merge Icons semantics must be decided before implementation.
UI proof
The mock above shows the recommended mode and its Merge Icons conflict. It is intentionally a decision artifact, not an implementation screenshot. The following packaged synthetic-account proof verifies the bounded current behavior: the account action is now named “Sign in with Claude Code…” and no longer claims it will add a durable CodexBar account. No real credential, browser session, or provider call was used.
Accepted decisions
- The optional external
claude-swapdependency is accepted for exactcswap --list --jsonexecution and explicitcswap --switch-to <slot> --jsonactivation. - Automatic switching, account add/import/export/purge, and session launching stay out of scope.
- Provider-neutral account snapshots land before any per-account status item work.
- Per-account status items are capped at four and mutually exclusive with Merge Icons.
- Status item labels use aliases or privacy-safe ordinals, never email identity.
Any further change to these decisions requires a new product/auth review before implementation because it changes storage, status item migration, process authority, or the credential boundary.
Implementation and validation sequence
- Add fixtures for schema v1, error payloads, unknown versions, invalid percentages/timestamps, output limits, and process timeout. Use a fake executable only.
- Add the opt-in adapter and provider-neutral projection. Verify no credential reads and no impact on ambient Claude.
- Add settings-state and menu-model tests. Keep AppKit status item creation out of headless tests.
- Add status item identity/migration tests, then implement account items behind the opt-in setting.
- Add exact-argv, strict switch-result, serialization, and refresh tests using a fake executable only.
- Run focused tests,
make check,make test, packaged synthetic proof, and macOS UI proof with redacted fixtures.
No credential import, automatic switching, session launching, or compatibility shim is part of this proposal.
