# Claudex Design

Audience: AI coding agents first.

Regression guard for the rules below: `docs/claudex-invariants.md`. Read it before editing `claudex.py`, `claudex_router.py`, or `statusline.py`.

## Goal

Add `claudex`: launch Claude Code against Systray AI current default Codex account, current default Claude account, or both. Bare `claudex` runs hybrid: one session whose `/model` picker offers real Anthropic Fable, Opus, and Sonnet plus GPT-5.6 Sol, Terra, and Luna.

## Hybrid Contract

`claudex` without `--model` MUST:

1. Resolve both the default Codex account and the default Claude account; fail closed when either is missing.
2. Start the account-scoped `claude-code-proxy` child exactly as the GPT-only path does.
3. Start a loopback router on an ephemeral port, owned by the wrapper process, and set `ANTHROPIC_BASE_URL` to it.
4. Route each request by request-body `model`: `gpt-*` and `anthropic.gpt-*` to the proxy with the `anthropic.` prefix stripped; everything else, including bodyless requests, to `https://api.anthropic.com`.
5. Relay responses without buffering; SSE bodies MUST stream chunk by chunk.
6. Leave `ANTHROPIC_AUTH_TOKEN` and `ANTHROPIC_API_KEY` unset so Claude Code sends its own OAuth credentials, which the router forwards verbatim; the router MUST NEVER read, mint, or refresh credentials.
7. Write `claudex-hybrid-settings.json` `availableModels` as `fable`, `opus`, `sonnet`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`. Anthropic aliases keep the built-in Fable, Opus, and Sonnet entries; the Codex rows come from the gateway catalog in item 9, so Codex IDs MUST stay unprefixed and appear verbatim in `/model`, `/status`, and the statusline.
8. Set `ANTHROPIC_MODEL=gpt-5.6-sol` and `ANTHROPIC_SMALL_FAST_MODEL=gpt-5.6-luna` so background traffic bills Codex, not the Anthropic subscription.
9. Write `<CLAUDE_CONFIG_DIR>/cache/gateway-models.json` as `{baseUrl, fetchedAt, models[{id, display_name}]}` for the router base URL and set `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1`, so every Codex entry becomes a `/model` row labelled `GPT-5.6 Sol|Terra|Luna`. Claude Code filters IDs to `^(claude|anthropic)` only when it fetches `/v1/models` itself; the cache-read path applies no filter, so a catalog this wrapper writes carries unprefixed Codex IDs. The session model (`ANTHROPIC_MODEL`) MUST be the LAST catalog entry: Claude Code drops every entry listed after it. Claude Code reads that catalog with no credential; only its discovery fetch requires `ANTHROPIC_AUTH_TOKEN` or an `apiKeyHelper` key, and either of those would replace the Anthropic OAuth bearer on real requests.
10. Verify `CLAUDE_CONFIG_DIR` support with a base-URL-free environment.
11. Stop the router before terminating the proxy and reconciling Codex credentials.

## Account Selection

`--account|--profile SLUG` (space or `=` form) pins both providers to one slug when both exist. When the Codex account exists but no Claude account matches that slug, launch GPT-only proxy mode (Sol/Terra/Luna picker only). `--codex-account|--codex-profile SLUG` and `--claude-account|--claude-profile SLUG` pin one provider each and override the shared flag regardless of order. Codex takes the account slug under either tray root; Claude goes through `CommandRouter`, so it accepts every profile token `cld --profile` accepts. When a flag is absent, that provider resolves the Systray AI default. An unknown Codex account or an explicit `--claude-account|--claude-profile` with no matching Claude account MUST fail closed with exit `1` and name the account. Both flags are removed before Claude Code sees the argument list.

## Process Contract

`claudex --model sol|terra|luna [claude args...]` MUST:

1. Resolve newest valid Systray AI Codex `default_slug` across current and legacy registry roots.
2. Read selected account `CODEX_HOME/auth.json`; NEVER read courtesy copy as source of truth.
3. Reconcile native Codex auth with account-scoped proxy auth without logging secrets.
4. Start dedicated `claude-code-proxy serve --no-monitor` child on loopback-only ephemeral port.
5. Wait for `/healthz`; fail closed on missing binary, invalid auth, early proxy exit, or readiness timeout.
6. Launch real `claude` with:
   - `--dangerously-skip-permissions` injected once by default
   - `ANTHROPIC_BASE_URL=http://127.0.0.1:<port>`
   - `ANTHROPIC_AUTH_TOKEN=unused`
   - `ANTHROPIC_MODEL=gpt-5.6-sol` by default
   - `ANTHROPIC_SMALL_FAST_MODEL=gpt-5.6-luna`
   - `CLAUDE_CODE_SUBAGENT_MODEL` removed from inherited environment; NEVER force one model across all subagents
   - GPT session `/model` entries come from `ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL` slots mapped to Sol, Terra, Luna with `_NAME`/`_DESCRIPTION` labels; gateway model discovery removed from inherited environment because it accepts only `claude-*`/`anthropic*` IDs, which the proxy would serve from Codex
   - effort, concurrency, tool-search, and streaming safeguards from approved alias contract
   - proxy priority service tier
7. Terminate proxy and reconcile refreshed proxy auth back into selected native account atomically.
8. Return Claude Code exit status.

`--model sol|terra|luna` MUST translate friendly model name to exact GPT-5.6 model ID and remove wrapper option before launching Claude Code through `claude-code-proxy`. `claudex --model fable|opus|sonnet` MUST launch native Claude Code with exact Anthropic model ID and selected Claude account `CLAUDE_CONFIG_DIR`; it MUST NOT start proxy or read Codex credentials. Write provider-scoped `claude-settings.json` `availableModels`: GPT session `/model` MUST show Sol, Terra, Luna only; Anthropic session `/model` MUST show Fable, Opus, Sonnet only. The GPT allowlist MUST hold the exact GPT-5.6 IDs so the built-in Fable entry — which the proxy would serve from Codex — stays out of the picker. Provider changes require new `claudex --model` launch. Inject settings through `--settings`.

## Credential Bridge

Store proxy auth at `<account>/CLAUDEX_HOME/codex/auth.json`, mode `0600`; directory mode `0700`.

Native shape remains source-compatible:

```text
CODEX_HOME/auth.json
  tokens.access_token
  tokens.refresh_token
  tokens.account_id
  tokens.id_token
  last_refresh
```

Proxy shape:

```text
CLAUDEX_HOME/codex/auth.json
  access
  refresh
  expires
  accountId
```

Reconciliation MUST compare access-token JWT expiry. Newer credential wins. Equal expiry with different refresh tokens MUST fail as ambiguous. Atomic native updates MUST preserve unrelated top-level fields and `tokens.id_token`.

## Isolation

- One proxy child per wrapper invocation; no shared daemon.
- Bind `127.0.0.1` only.
- Select available kernel-assigned port; retry startup race.
- Scope `CCP_CONFIG_DIR` to selected account `CLAUDEX_HOME`.
- NEVER mutate Claude account selection or `CLAUDE_CONFIG_DIR`; the hybrid gateway catalog under `<CLAUDE_CONFIG_DIR>/cache` is the only file the wrapper writes there.
- Set `CLAUDE_SECURESTORAGE_CONFIG_DIR` to the directory holding the account's authoritative `.credentials.json`, which `resolve_credentials` names: the account home for an app-managed grant, `~/.claude` when the vendor's live file holds this account's grant. Claude Code reads, locks, and rotates credentials under that directory alone, so a vendor-managed account never launches against a stale copy. NEVER copy or symlink the file instead: Claude Code writes it through a rename, which replaces a symlink and forks the grant.

## Errors

- No default: exit `1`; identify missing default.
- Missing/invalid auth: exit `1`; identify path, never secret value.
- Missing binaries: exit `127`; name binary.
- Proxy startup failure: exit `1`; include bounded stderr tail.
- Credential conflict: exit `1`; require re-authentication through Systray AI.

## Testing

- Default resolution selects tray default and account home.
- Native-to-proxy conversion preserves required identity and expiry.
- Reconciliation selects newer side and rejects ambiguous divergence.
- Launch env forces GPT-5.6 Sol and loopback proxy.
- Hybrid launch keeps both accounts wired: hybrid allowlist, hybrid default models, no inherited auth token, gateway catalog written for the router base URL with readable Codex labels, base-URL-free `CLAUDE_CONFIG_DIR` verification.
- Router sends `gpt-*`/`anthropic.gpt-*` to the proxy with the prefix stripped, everything else to Anthropic, streams SSE unbuffered, forwards the caller's `Authorization` header, and reports unreachable upstreams as Anthropic-shaped errors.
- Proxy is terminated when Claude exits or wrapper receives interrupt.
- Installer links `claudex.py`.
- Full `pytest`, `ruff check .`, and `mypy .` pass without warnings.

## Architecture Decisions

- Keep `claudex` separate from `cld`; Codex-backed Claude Code is a distinct process/auth contract.
- Use `claude-code-proxy`; current upstream supports GPT-5.6 Sol and isolated `CCP_CONFIG_DIR`.
- Keep proxy lifecycle invocation-scoped; selected-account changes take effect on every launch.
- Keep credential bridge account-scoped; refresh rotation remains attributable to one Systray AI account.
- Keep the hybrid router credential-free: Claude Code owns Anthropic OAuth refresh, so a long session never outlives an injected token.
- Reject third-party hybrid launchers (`@bman654/clodex`, `l3tchupkt/Claudex`): both authenticate against their own credential stores — clodex only through its ChatGPT device-code flow into the OS keyring — so neither can run on Systray AI account credentials without hand-authoring its private registry.
- Run the hybrid router in the wrapper process; a second child would add a lifecycle to leak without adding isolation, since the router holds no credentials.
