# Claude Code Account Parity Design

> Audience: AI coding agents first. Implement this contract exactly; do not infer weaker parity from the older Claude switcher spec.

## Purpose

Give Claude Code accounts the same tray functionality as Codex accounts:

- add account
- set default
- rename
- remove local account data
- re-authenticate
- refresh authentication health
- display current session and weekly usage limits with reset times
- route `cld` away from accounts proven unusable

The current Claude switcher spec does not provide this parity. It explicitly defers Claude add/repair, defines conservative health without a producer, and leaves lifecycle UI wired to the Codex registry. This spec supersedes those deferrals; unchanged storage and routing decisions remain valid.

## Verified Inputs

- Installed Claude Code `2.1.199` supports `claude auth login`, `logout`, and machine-readable `status --json`.
- `claude auth status --json` returns login state, auth method, provider, email, organization identity, and subscription type.
- Claude Code itself fetches utilization from `GET https://api.anthropic.com/api/oauth/usage` with OAuth refresh enabled.
- A local authenticated probe returned `five_hour`, `seven_day`, optional scoped weekly limits, `extra_usage`, `limits`, `spend`, utilization percentages, and reset timestamps.
- Claude account homes already isolate `.credentials.json`; `AccountRegistry` already supports Claude rename/remove/default persistence.

The utilization endpoint is an internal Claude Code dependency, not a documented public API. Its failure must remove limit detail only. It must never turn a separately authenticated account into `BROKEN`.

## Scope Boundaries

- Preserve independent Codex and Claude registries/defaults.
- Preserve immutable slugs and rename aliases only.
- Remove local account data; do not revoke remote OAuth tokens.
- Support Claude subscription login (`--claudeai`) in this change. Do not add Console/API billing account setup.
- Cache spend/extra-usage data for forward compatibility; do not render it in the first UI.
- Do not consume model quota to check health or limits.
- Do not scrape terminal-rendered `/usage` output.

## Recommended Architecture

Use shared lifecycle and health contracts with Codex and Claude implementations. Keep `Indicator` responsible for UI orchestration only; it selects services by `Account.tool` and never calls a provider-specific auth command or endpoint directly.

### Provider Services

Add a small immutable service record keyed by `AccountRegistryKind`:

```python
@dataclass(frozen=True)
class ProviderServices:
    registry: AccountRegistry
    lifecycle: AccountLifecycle
    health: AccountHealthProvider
```

`Indicator` receives both records. Replace direct `self.registry` lifecycle mutations and the single `AccountHealthClient` assumption with lookup by tool-qualified account key. Keep existing Codex behavior behind the same contracts before adding Claude behavior.

### Account Lifecycle

Define one orchestration contract with two implementations:

```python
class AccountLifecycle(Protocol):
    def add(self, alias: str) -> Iterator[LifecycleEvent]: ...
    def reauthenticate(self, account: Account) -> Iterator[LifecycleEvent]: ...
```

Reuse the existing Codex operation as the Codex implementation. Add `ClaudeAuthOperation` for Claude. Events must cover started, browser-waiting, success, collision, failure, and rollback. UI code displays progress and cancel state but must not interpret subprocess output.

Extend `AccountRegistry` with a staged-add transaction owned by the registry rather than a new module. `stage_add(alias)` creates an unregistered temporary account home under the provider account root. `commit()` atomically renames it to the final slug and writes the registry; `rollback()` removes it. The transaction must fail closed if the slug or destination appears concurrently.

Claude add flow:

1. Validate the alias and create a staged, unregistered Claude account through `AccountRegistry.stage_add(alias)`.
2. Create the isolated `CLAUDE_HOME` inside that staging directory.
3. Spawn `claude auth login --claudeai --email <email>` when the user supplied an email; omit both `--email` and its value otherwise. Set `CLAUDE_CONFIG_DIR` to the staged home.
4. Show a cancellable “Complete sign-in in your browser” dialog while the CLI owns browser login.
5. Require exit code zero, `.credentials.json`, and `claude auth status --json` with `loggedIn: true`.
6. Derive identity from normalized email plus organization ID. Reject a duplicate identity, roll back the staged account, and preserve the existing account.
7. Write `account_identity.json` in the account home with normalized email and organization ID, then commit the staged account to registry/default state and rebuild both menu and popup.
8. On every failure or cancel, roll back the staged transaction. No incomplete account may become visible to registry readers.

Claude re-authentication flow:

1. Lock the account home and back up `.credentials.json`, `account_identity.json`, and account metadata.
2. Run the same login and verification sequence in that account home.
3. Load the pre-repair identity from `account_identity.json`, falling back to existing Claude account metadata. Require the verified identity to match it. For a legacy account with no recoverable identity, reject any identity already registered to another account, accept the successful login as its baseline, and write the sidecar. A different known identity is an add operation, not repair.
4. Atomically keep the new credentials on success; restore the backup on failure, cancel, mismatch, or collision.
5. Refresh that account immediately after success.

Rename remains a registry alias update. Remove requires provider-qualified confirmation. Implement remove as an account-root transaction: rename the account directory to a private quarantine path, atomically update registry/default/active links, then delete quarantine; restore the directory if the state update fails. If removing the default and accounts remain, select the first registry account as the new default and update active links. If removing the final account, clear the default and active links; `cld` must then fail with an explicit no-account message.

### Claude OAuth Session

Add one deep helper owned by Claude health, not by the UI:

- read `claudeAiOauth` from the account `.credentials.json`
- send authenticated JSON requests
- refresh an expired access token once through `https://platform.claude.com/v1/oauth/token` using Claude Code client ID `9d1c250a-e61b-44d9-88ed-5944d1962f5e`
- serialize refresh with an account-local lock
- re-read credentials after acquiring the lock and avoid overwriting a token another process already refreshed
- atomically merge refreshed OAuth fields while preserving `mcpOAuth` and unknown keys
- expose typed authentication, protocol, timeout, and transport errors without token material

Never log credentials, authorization headers, response bodies containing identity data, or raw exceptions that embed requests.

### Claude Health Provider

`ClaudeHealthClient.fetch(account)` performs two independent checks:

1. Run `claude auth status --json` under the account `CLAUDE_CONFIG_DIR`, with a bounded timeout. `loggedIn: false`, invalid JSON proving no login, or a verified authentication rejection maps to `BROKEN`. Process launch, timeout, or transient transport failure maps to `UNKNOWN`.
2. When authentication is healthy, fetch `/api/oauth/usage` through `ClaudeOAuthSession`. Parse known fields and ignore unknown fields.

Map usage to the existing snapshot contract:

- `five_hour.utilization` -> primary percent
- `five_hour.resets_at` -> primary reset
- `seven_day.utilization` -> secondary percent
- `seven_day.resets_at` -> secondary reset
- active entries in `limits` -> optional named limit rows
- fetch time -> `checked_at`
- subscription type from auth status -> plan label

Usage endpoint failures retain `HEALTHY` auth state with absent/stale limit fields and a non-secret detail message. A successful auth check with missing limit fields is valid. Cache snapshots in the existing Claude health store under tool-qualified keys.

### Tray And Popup Behavior

- Render Add Account once per provider section.
- Render Set Default, Reload, Re-authenticate, Rename, and Remove for every account card.
- Dispatch every action through the account’s `ProviderServices`; never default to the Codex registry.
- Disable only the account action currently running. Preserve other providers and accounts.
- Show authentication state even when usage is unavailable.
- Keep last successful limit values visible as stale after a usage-only failure, with the prior timestamp.
- Refresh all Codex and Claude accounts concurrently on popup open and on the hourly scheduler.
- Rebuild provider-qualified menu/card state after add, rename, remove, default change, and re-authentication.

## Data Flow

```text
popup action
  -> Indicator resolves ProviderServices by Account.tool
  -> lifecycle operation or health provider runs off GTK thread
  -> typed result returns through idle callback
  -> registry/health store commits atomically
  -> tool-qualified account card and router cache update
```

Health refresh must not hold GTK locks or registry file locks during network/subprocess waits. Registry changes and credential refreshes use separate narrow locks.

## Error Handling

- Auth cancellation: restore/remove staged state; report cancelled, not broken.
- Duplicate Claude identity: preserve both pre-existing account and its default; reject staged credentials.
- Wrong identity during repair: rollback and instruct user to use Add Account.
- Claude binary missing or unsupported `CLAUDE_CONFIG_DIR`: disable Claude add/repair/refresh with the existing explicit unsupported-routing error; Codex stays functional.
- Auth status timeout or malformed output: `UNKNOWN`; keep prior usage as stale.
- OAuth 401 after one locked refresh: usage unavailable; auth status determines health.
- Usage 403/404/schema change: usage unavailable; health remains independently derived.
- Remove failure: leave registry and default unchanged unless directory deletion completed; make filesystem/registry ordering rollback-safe.
- UI callback exception: log a redacted error and re-enable controls.

## Testing

Unit tests:

- provider service lookup uses tool-qualified keys for every action
- Claude add success, cancel, CLI failure, malformed status, and duplicate identity rollback
- Claude repair success, cancel, wrong identity, collision, and backup restoration
- Claude rename/remove/default reassignment/final-account cleanup
- OAuth usage parse for five-hour, seven-day, optional scoped limits, null fields, and unknown fields
- OAuth refresh success, refresh failure, concurrent token replacement, atomic merge, and redaction
- auth/usage independence: usage failure never changes verified healthy auth to broken
- cache preserves stale successful limits after a usage-only failure
- popup/menu render and dispatch all lifecycle actions for both providers
- scheduler and Reload All refresh both providers without slug collisions

Integration tests use fake executables and a local HTTP server. Do not use live credentials in automated tests. Add one opt-in manual smoke script that verifies local `claude auth status --json` and `/api/oauth/usage` schema without printing tokens or identity.

Verification gate:

```text
pytest
ruff check
mypy
manual GTK smoke: add -> default -> rename -> reload -> re-authenticate -> remove
manual Claude limits smoke: healthy auth plus five-hour/seven-day reset display
```

## Migration

No registry JSON format migration is required. Existing Claude accounts gain lifecycle actions immediately. On the first successful identity-bearing health check, create `account_identity.json` atomically if absent. On the first successful refresh, rewrite `claude_health_cache.json` using the current typed snapshot schema. Missing new fields remain backward-compatible.

## Architecture Decisions

- Accepted: shared lifecycle contract with Codex and Claude implementations. Deleting it would scatter provider dispatch and rollback rules through `Indicator`; depth is medium/deep.
- Accepted: shared health-provider contract. Two real implementations already exist conceptually, and callers must not know subprocess versus HTTP details; depth is deep.
- Accepted: `ClaudeOAuthSession` helper. It hides refresh, locking, atomic credential merge, redaction, and HTTP behavior; depth is deep.
- Accepted collapse: keep provider lookup as an immutable record/dictionary inside existing startup wiring, not a provider-registry framework. A standalone registry module would be shallow.
- Accepted collapse: keep Claude usage parsing inside `ClaudeHealthClient`; a one-endpoint parser adapter would have one implementation and a decorative seam.
- Rejected: duplicate Claude tray controller. It would preserve the current drift and duplicate orchestration.
- Rejected: terminal `/usage` scraping. It is unstable, interactive, and can accidentally consume model quota.
- Rejected: treating internal usage failure as broken authentication. The two signals have different failure domains.
