# Claude Code Account Switcher Design

Audience: AI coding agents first.

## Goal

Build Claude Code account switching beside existing Codex switching.

Required outcomes:
- `cdx` keeps current Codex behavior.
- New `cld` wrapper launches real `claude` under selected/routed Claude account state.
- Tray menu shows 2 columns: Codex accounts and Claude Code accounts.
- Codex and Claude have separate registries, defaults, routing files, health caches, and account directories.
- Claude credential rotation remains visible mid-session by using one real per-account credential file and exposing the selected default via symlink, not stale copies.

## Current Facts

Existing repo already has:
- `AccountRegistry` for Codex: `~/.codex-tray/accounts`, `accounts.json`, `default_slug`.
- `cdx.py`: resolves route, execs `codex` with `CODEX_HOME`.
- `Indicator`: single-registry tray menu, default selection, add/repair/rename/remove, Codex health refresh.
- `DeviceAuthFlow`: Codex-specific `codex login --device-auth`.
- `AccountHealthClient`: Codex-specific `codex app-server --stdio` rate-limit health.

Local Claude facts:
- `claude` binary exists at `/home/user/.local/bin/claude`.
- `~/.claude/.credentials.json` exists.
- Credential JSON top-level keys include `mcpOAuth` and `claudeAiOauth`.
- `claude auth` supports `login`, `logout`, `status`.
- `claude --help` exposes `--settings`, `--mcp-config`, safe mode, auth commands, and says safe mode skips permission prompts while auth/model/plugin configuration remains active.
- `strace -f -e trace=file claude auth status` opens `~/.claude/.credentials.json`, `~/.claude.json`, `~/.claude/settings.json`, and project `.claude/settings*.json` paths.
- `~/.claude.json` contains account identity/cache keys: `oauthAccount.accountUuid`, `oauthAccount.emailAddress`, `oauthAccount.organizationUuid`, `oauthAccount.seatTier`, `oauthAccount.organizationRateLimitTier`, `userID`, subscription flags, and per-account access caches.

Known uncertainty:
- Exact Claude config-dir override must be verified locally. Do not assume `CLAUDE_CONFIG_DIR` works until a test or probe proves it. If no supported env var exists, `cld` must use the selected default symlink in `~/.claude/.credentials.json` and route only at default-selection time.
- Exact minimal Claude account-state set must be verified by trace after login/status. Current evidence rejects `.credentials.json`-only switching.

## Approach Options

### Approach 1: Tool-Typed Account Systems

One account-system model with `tool="codex" | "claude"` selects path layout, auth filename, shared links, routing file, health cache, and launch env. Existing Codex behavior stays backward-compatible.

| Dimension | Assessment |
|-----------|------------|
| Robustness | Strong. Separate state prevents cross-tool corruption. Shared tested primitives reduce duplicated bugs. |
| Long-term | Good. Future tools can add another typed config without copying tray logic. |
| Scalability | Holds for many accounts; routing is file reads only. |
| Performance | Same as current; no network on wrappers. |
| Reversibility | Two-way door. Codex paths remain unchanged; Claude can be removed by deleting Claude-specific files. |

Weakness: Requires refactoring `Indicator` away from single-registry assumptions.

### Approach 2: Duplicate Codex Stack for Claude

Copy `AccountRegistry`, `cdx`, and tray menu logic into Claude-specific files.

| Dimension | Assessment |
|-----------|------------|
| Robustness | Medium. Isolation is clear, but duplicated behavior can drift. |
| Long-term | Poor. Every fix in tray/account management needs two edits. |
| Scalability | Fine for two tools only. |
| Performance | Same as current. |
| Reversibility | Two-way door, but duplicated code removal is noisy. |

Weakness: Drift risk is high in token/auth code.

Recommended: Approach 1. It preserves Codex compatibility while making Claude a first-class independent account system. It avoids shared credentials and avoids a parallel copy of complex tray behavior.

## Storage Layout

Codex storage remains backward-compatible:

```text
~/.codex-tray/
  accounts.json
  default_slug
  routing_rules.json
  health_cache.json
  accounts/<slug>/CODEX_HOME/auth.json
```

Claude storage is separate:

```text
~/.codex-tray/
  claude_accounts.json
  claude_default_slug
  claude_routing_rules.json
  claude_health_cache.json
  claude-accounts/<slug>/CLAUDE_HOME/.credentials.json
  claude-accounts/<slug>/CLAUDE_HOME/claude.json
```

Courtesy default:
- Codex default sets `~/.codex/auth.json` to the selected Codex account as current implementation intends.
- Claude default sets `~/.claude/.credentials.json` as a symlink to the selected account credential file.
- Claude default also sets `~/.claude.json` as a symlink to the selected account `CLAUDE_HOME/claude.json`.
- Both Claude symlinks are required because Claude can rotate credentials and rewrite account metadata/caches mid-session; copies would go stale.
- `~/.claude/settings.json`, MCP config, skills, plugins, hooks, and project-local `.claude/settings*.json` are shared configuration by default. Do not place them in per-account storage unless a trace proves they contain account identity.

## Components

### Account Registry

Extend registry with a tool kind, not separate copied classes.

Contract:
- `AccountRegistryKind.CODEX`
- `AccountRegistryKind.CLAUDE`
- Existing constructor defaults to Codex.
- Claude constructor accepts `legacy_claude_home: Path = Path.home() / ".claude"`.
- `Account.home` should remain source-compatible as `codex_home` for existing code, but new code should use a neutral name where practical.

Codex behavior:
- Preserve current file names and existing tests.
- Preserve `auth.json` decode behavior.

Claude behavior:
- Account home path: `base_dir / "claude-accounts" / slug / "CLAUDE_HOME"`.
- Account-state files: `.credentials.json` and `claude.json` (active path `~/.claude.json`).
- Registry file: `claude_accounts.json`.
- Default file: `claude_default_slug`.
- `list()` may return `email=None`, `plan=None`, `account_id=None` until Claude credential decoding is deliberately implemented. Do not parse secrets speculatively.
- `set_default()` atomically repoints both active Claude symlinks:
  - `~/.claude/.credentials.json -> <account>/CLAUDE_HOME/.credentials.json`
  - `~/.claude.json -> <account>/CLAUDE_HOME/claude.json`

### `cld` Wrapper

New `cld.py` mirrors `cdx.py` CLI shape:
- `cld [claude args...]`
- `cld --account <slug> [claude args...]`
- `cld --account=<slug> [claude args...]`

Resolution:
- Explicit `--account` bypasses routing.
- Otherwise route by project name using `claude_routing_rules.json`.
- Health fallback uses `claude_health_cache.json` if available.
- Missing/stale health cache prints `cld: health cache missing or stale; proceeding without health data`.

Launch:
- Exec real `claude`, not shell wrapper.
- If local probe proves a Claude config-dir env var, set it to account `CLAUDE_HOME`.
- If no config-dir override exists, `cld` must select the default symlink before launch and reject non-default per-invocation routing with a clear error. Do not pretend per-invocation routing works without isolation.

### Tray Indicator

Render two columns in one click menu:
- Left column: `Codex`
- Right column: `Claude Code`

Each column has:
- Radio account entries.
- Current default checked within that tool only.
- Broken account activation routes to that tool's repair flow if repair is supported.
- Manage submenu actions scoped to that tool: add, refresh login, rename, remove.

Title:
- Format: `Codex: <alias> (<plan>) · Claude: <alias-or-unknown>`
- If a tool has no valid default: `<Tool>: unknown account`.

State isolation:
- Selecting Claude default must not call Codex registry.
- Selecting Codex default must not call Claude registry.
- Health snapshots are keyed by `(tool, slug)` or stored per account system to avoid slug collisions.

### Health

Codex health remains `AccountHealthClient`.

Claude health starts conservative:
- Use `claude auth status` only if it can be run against isolated Claude account state without mutating credentials.
- If isolation cannot be proven, Claude health is `UNKNOWN`; no fallback decisions should treat `UNKNOWN` as broken.
- Do not call `claude logout`.

### Auth Repair/Add

Codex repair/add continues to use `DeviceAuthFlow`.

Claude repair/add:
- Must be a separate flow if implemented.
- Must use `claude auth login` or `claude setup-token` only after local verification of prompt and file mutation behavior.
- If not implemented in first pass, UI must disable or omit Claude Add/Refresh rather than showing a broken action.

## Error Handling

- Unknown explicit account: wrapper exits `1`, message `cld: unknown account '<slug>'`.
- Missing Claude credential for selected account: wrapper exits `1`, message names missing path.
- Missing Claude account metadata file (`claude.json`) for selected account: wrapper exits `1`, message names missing path.
- Missing Claude routing file: seed default file on tray startup.
- Stale health cache never blocks launch.
- No healthy route: wrapper exits `1`, lists candidates and statuses.
- Symlink replacement must be atomic: create temp symlink in target dir, `os.replace`.

## Testing

Required tests:
- Registry kind paths and default files are separate.
- Claude default symlinks update atomically and see both credential and `~/.claude.json` account metadata rotation.
- `cld --account` bypasses routing.
- `cld` project routing reads `claude_routing_rules.json`.
- `cld` rejects unknown account.
- Tray renders two columns with independent checked defaults.
- Selecting Claude default does not change Codex registry.
- Selecting Codex default does not change Claude registry.
- Slug collision across tools does not collide snapshots or menu items.
- Startup wires both registries and seeds both routing files.

Regression tests:
- Existing Codex tests must continue passing.
- Existing `cdx` env must still set `CODEX_HOME` and exec `codex`.

## Migration

Do not migrate Claude automatically from backups.

First implementation should support:
- Creating Claude account dirs manually via tests.
- Importing current `~/.claude/.credentials.json` and `~/.claude.json` into a named Claude account only if an explicit add/import path is implemented.

Reason: Claude credential/account-state file formats are not decoded here, and automatic backup import risks copying stale or mismatched account state.

## Architecture Decisions

- Accepted: tool-typed account systems. Reason: separate state with shared primitives.
- Accepted: Claude `.credentials.json` and `~/.claude.json` symlinks for default. Reason: local trace proves account auth/identity state is not confined to `.credentials.json`, and mid-session writes must stay live.
- Accepted: conservative Claude health. Reason: do not mutate or probe auth beyond verified local behavior.
- Rejected: shared Codex/Claude registry. Reason: user explicitly requires independent registries/defaults.
- Rejected: duplicate full Claude stack. Reason: high drift risk in token-management code.
- Deferred: Claude add/repair UX if local login mutation behavior is not verified. Reason: safe omission beats broken auth UI.
