Profile
Back to NewsBack
GitHub Trending 35 min
Reader Mode
Dicklesworthstone/coding_agent_account_manager: Sub-100ms auth switching for AI coding CLIs (Claude Code, Codex, Gemini): swap subscription accounts instantly when you hit usage limits

Dicklesworthstone/coding_agent_account_manager: Sub-100ms auth switching for AI coding CLIs (Claude Code, Codex, Gemini): swap subscription accounts instantly when you hit usage limits

15 hours ago

caam - Coding Agent Account Manager

caam - Coding Agent Account Manager

!Release !Go Version !License

Sub-100ms account switching for AI coding CLIs with fixed-cost subscription plans. When you hit usage limits on Claude Max, GPT Pro, or Gemini Ultra, don't wait 60 seconds for browser OAuth—just swap to another account instantly.
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/coding_agent_account_manager/main/install.sh?$(date +%s)" | bash

Usage:

caam backup claude [email protected]      # Save current auth
caam activate claude [email protected]      # Switch instantly

🤖 Agent Quickstart (JSON)

Use --json in agent contexts. stdout = data, stderr = diagnostics, exit 0 = success.

# List available profiles (machine-readable)
caam list --json

Show current status for all tools

caam status --json

Switch accounts

caam activate claude [email protected] --json

The Problem

You're paying $200-275/month for fixed-cost AI coding subscriptions (Claude Max, GPT Pro, Gemini Ultra). These plans have usage limits—not billing caps, but rate limits that reset over time. When you hit them mid-flow, the official way to switch accounts:

/login → browser opens → sign out of Google → sign into different Google →
authorize app → wait for redirect → back to terminal

That's 30-60 seconds of friction. Multiply by 5+ switches per day across multiple tools.

The Solution

Each AI CLI stores OAuth tokens in plain files. caam backs them up and restores them:

caam activate claude [email protected]   # ~50ms, done

No browser. No OAuth dance. No interruption to your flow state.


How It Works

flowchart LR
    subgraph System["Your System"]
        A["~/.claude.json"]
        B["~/.codex/auth.json"]
        C["~/.gemini/settings.json"]
    end

subgraph Vault["~/.local/share/caam/vault/"] D["claude/[email protected]/"] E["claude/[email protected]/"] F["codex/[email protected]/"] end

A <-->|"backup / activate"| D A <-->|"backup / activate"| E B <-->|"backup / activate"| F

style System fill:#1a1a2e,stroke:#4a4a6a,color:#fff style Vault fill:#16213e,stroke:#4a4a6a,color:#fff

That's it. No external database servers (uses embedded SQLite), no required daemons (optional background service available). Just cp with extra steps.

Why This Works

OAuth tokens are bearer tokens—possession equals access. The CLI tools don't fingerprint your machine beyond what's already in the token file. Swapping files is equivalent to "being" that authenticated session.

Profile Detection

caam status uses content hashing to detect the active profile:

  1. SHA-256 hash current auth files
  2. Compare against all vault profiles
  3. Match = that's what's active
This means:
  • Profiles are detected even if you switched manually
  • No hidden state files that can desync
  • Works correctly after reboots

Three Operating Modes

1. Vault Profiles (Simple Switching)

Swap auth files in place. One account active at a time per tool. Instant switching.

caam backup claude [email protected]
caam activate claude [email protected]

Use when: You want to switch between accounts sequentially (most common use case).

2. Isolated Profiles (Parallel Sessions)

Run multiple accounts simultaneously with full directory isolation.

caam profile add codex [email protected]
caam profile add codex [email protected]
caam exec codex [email protected] -- "implement feature X"
caam exec codex [email protected] -- "review code"

Each profile gets its own $HOME and $CODEX_HOME with symlinks to your real .ssh, .gitconfig, etc.

What is and isn't isolated inside a profile:

| Path | Treatment | Why | |---|---|---| | Provider auth dirs (~/.claude credentials, ~/.config/claude-code, $CODEX_HOME, ~/.gemini, ~/.config/opencode, ...) | isolated (real dirs) | The whole point: per-account credentials. | | ~/.ssh, ~/.gitconfig, ~/.gnupg, ~/.aws, ~/.cargo, ~/.npm, ~/.local/bin | symlink → real home | Dev tooling passes through. | | Other $XDG_CONFIG_HOME entries (gh, atuin, uv, shopify-*, ...) | per-entry symlink → real ~/.config | XDG-based CLIs keep their credentials — without this, gh silently logs out and git push fails with could not read Username for 'https://github.com' (issue #69). | | Other ~/.local/share and ~/.local/state entries (com.vercel.cli, supabase, ...) | per-entry symlink → real home | Same: HOME redirection silently relocates the XDG data/state dirs. | | ~/.local/share/caam | never passed through | Contains the vault and every profile's credentials. | | Claude ~/.claude/skills, plugins, commands, agents | symlink → real home, in both home/.claude and $CLAUDE_CONFIG_DIR (xdg_config/claude-code) | User tooling, not account state — shared so sessions inside a profile keep their skills. XDG-aware Claude Code reads them only from CLAUDE_CONFIG_DIR (issue #90). |

Passthrough symlinks (and the Claude asset links) are refreshed on every caam exec, so tools installed after profile creation are picked up automatically.

Use when: You need two accounts running at the same time in different terminals.

3. Shallow Profiles (Concurrent Multi-Account Multiplexing)

A "shallow" $HOME per identity: only the auth-bearing files are real, everything else is a symlink back to your real ~/. Designed for orchestrators that fan N parallel agent sessions across N accounts on the same machine.

Supported providers: claude, codex, and agy (Antigravity). Each provider keeps only its own identity files real and private; everything else symlinks back to your real ~/. The provider is inferred from --from-vault /, or set explicitly with --tool claude|codex|agy (defaults to claude). On spawn, caam repoints HOME at the shallow profile and pins the provider's home var (CODEX_HOME / GEMINI_HOME) so a stray inherited value can't pull the real identity back in.

# Stage credentials in caam's vault first (one-time per account).
caam backup claude [email protected]
caam backup codex  bob
caam backup agy    carol

Create a shallow profile per identity, copying the credential out of the vault.

--tool is inferred from the <tool>/<profile> part of --from-vault.

caam shallow-profile create alice --from-vault claude/[email protected] caam shallow-profile create bob --from-vault codex/bob caam shallow-profile create carol --from-vault agy/carol

Spawn concurrent sessions, each pinned to its own identity and provider.

With no -- <cmd> the profile's own CLI (claude / codex / agy) is run.

caam shallow-spawn alice & # session 1, alice's Claude quota caam shallow-spawn bob & # session 2, bob's Codex identity caam shallow-spawn carol & # session 3, carol's Antigravity identity wait

Layout under ~/orch-homes//claude (the codex and agy real-file sets are listed below):

| Path | Real or symlink? | Why | |------|------------------|-----| | .claude/.credentials.json | real file | The whole point: per-identity OAuth token. | | .claude/.credentials.lock | real file | Per-identity flock target so two sessions don't serialize on a shared lock. | | .claude.json | real file | Claude Code rewrites this on every run (it holds the login identity); a symlink would mutate the user's real settings under the shallow identity. Seeded from your real ~/.claude.json minus the account-bound keys (oauthAccount, usage/entitlement caches), and the shared preference keys (theme, editor mode, notification channel, user/project mcpServers, project trust and allowedTools) are refreshed from the real file on every shallow-spawn — the main lane is the source of truth for configuration; pass --no-sync-config to keep a profile's own values. | | .claude/projects/, .claude/todos/, .claude/shell-snapshots/ | symlink → ~/.claude/... | Conversation history is shared. | | .bashrc, .zshrc, .gitconfig, .ssh/, .cargo/, .bun/, .config/, .docker/, ... | symlink → ~/... | Dev tooling, shell, git, ssh — all pass through. |

Per-provider real (private) files — everything else under the provider's home is symlinked through, so non-auth state (sessions, history, caches) stays shared:

| Provider | Real / private files | Spawn pins | |----------|----------------------|------------| | claude | .claude/.credentials.json, .claude/.credentials.lock, .claude.json | scrubs CLAUDE_CONFIG_DIR | | codex | .codex/auth.json, .codex/config.toml (file credential store enforced; shared tables refreshed from your real config on every spawn, hook/project/notice state kept private) | CODEX_HOME=/.codex | | agy | .gemini/antigravity-cli/antigravity-oauth-token (+ optional .gemini/google_accounts.json, .gemini/oauth_creds.json, .gemini/antigravity-cli/settings.json) | GEMINI_HOME=/.gemini |

Smart fallback: if a candidate (e.g. ~/.cargo) doesn't exist in your real ~/, no symlink is created — no broken links for users who don't have a given tool installed.

Use when: Your orchestrator runs N Claude Code sessions in parallel and each one must hit a different account simultaneously. caam profile add would also work, but each profile gets a blank shell history, blank git config, and blank Claude conversation history — painful for real dev work. Shallow profiles preserve everything you'd want to share and isolate only the auth identity.

Subcommands:

caam shallow-profile create <name> [--tool claude|codex|agy] [--from-vault <tool>/<profile>] [--from-file <path>] [--force] [--json]
caam shallow-profile list [--json]
caam shallow-profile delete <name> [--force] [--json]
caam shallow-profile sync-config <name>|--all [--json]   # reconcile shared config with your real HOME
caam shallow-spawn <name>                     # open the profile's own provider CLI (claude / codex / agy) in this terminal
caam shallow-spawn <name> --create            # first run of a NEW identity: provision an empty profile, then start it
caam shallow-spawn <name> --create --tool codex   # ...with a codex layout instead of claude
caam shallow-spawn <name> -- <cmd> [args...]  # or run any other command under the profile
caam shallow-spawn <name> --print-env         # print HOME=... (and CODEX_HOME/GEMINI_HOME) without exec
caam shallow-spawn <name> --allow-agent-view -- claude   # keep Claude Code Agent View enabled (see note below)
caam shallow-spawn <name> --no-sync-config    # don't refresh shared config from your real HOME before starting
caam shallow-profile sync-config <name>       # ...or reconcile it on demand (--all for every profile)
caam shallow-spawn <name> --effort xhigh -- codex ...    # codex only: injects -c model_reasoning_effort=xhigh (codex has no --effort flag)

The base directory defaults to ~/orch-homes/. Override with $CAAM_SHALLOW_HOMES_DIR or the --base flag (per-command, useful for tests).

Worked example — 3-way Claude orchestration on a VPS:

# One-time setup: log in once on each account through the normal Claude flow,

back each one up to caam's vault.

for who in alice bob charlie; do /login # in claude → $who's google account caam backup claude "$who" done

Create three shallow identities pointing at those vault profiles.

for who in alice bob charlie; do caam shallow-profile create "$who" --from-vault "claude/$who" done

Fan three concurrent claude sessions. Each lands on its own quota,

but all three share your real ~/.bashrc, ~/.gitconfig, ~/.ssh, AND

~/.claude/projects (so any session can see/resume any conversation).

caam shallow-spawn alice -- claude --print "audit pkg/auth for race conditions" & caam shallow-spawn bob -- claude --print "write tests for internal/shallow" & caam shallow-spawn charlie -- claude --print "draft release notes for v0.4.0" & wait

Starting a new identity: --create

An unknown name is an error, not a new profile. Creating implicitly would turn caam shallow-spawn alise into a fresh empty identity plus a login prompt for the wrong account, with the mistyped profile then lingering on disk. The error instead names the closest existing profile and the flag that would have created this one:

shallow profile "alise" does not exist; did you mean "alice"?
  create it and start a session:  caam shallow-spawn alise --create [--tool claude|codex|agy]
  or set it up explicitly:        caam shallow-profile create alise

--create provisions the profile with empty credentials and starts the session, so the first run of a new identity is a login prompt. Credentials are deliberately never copied from the vault here: two homes sharing one refresh-token family invalidate each other, so seeding stays an explicit caam shallow-profile create --from-vault / decision. --print-env remains a strict dry run and never creates anything, and --tool on a profile that already exists under another provider is an error rather than a silent no-op.

Keeping shared configuration in sync

A shallow profile's provider configuration is a real, private file — it has to be, because the provider writes identity and per-home state into it — so it diverges from your real HOME the moment you change something there. The most common casualty is an MCP server: change a real-home entry from the stdio transport to streamable HTTP and every codex profile keeps the old command/args block, after which codex refuses to parse its config at all (url is not supported for stdio in mcp_servers.).

Every spawn therefore refreshes the shared configuration from your real HOME, and caam shallow-profile sync-config [--all] does it on demand:

| Provider | Refreshed | Never touched | |----------|-----------|---------------| | claude (.claude.json) | preferences (theme, editor mode, notification channel, auto-updates), user-scope mcpServers, per-project trust / allowedTools / MCP settings | oauthAccount, usage caches, prompt history, per-project session state | | codex (.codex/config.toml) | root settings (model, model_reasoning_effort, personality, notify, …) and whole tables: [mcp_servers.], [features], [skills], [hooks], [model_providers.] | [hooks.state.] (hook trust), [projects.] (workspace trust), [notice.*] (dismissed notices), and auth.json |

Two rules keep it safe to run on every spawn:

  • Sections are replaced as a unit, never merged key by key. For an MCP
server that is the whole point: [mcp_servers.kernel] and its subtables are dropped and re-inserted together, so a stale command/args pair cannot survive beside a new url.
  • Nothing is deleted. A table your profile has and your real HOME does not
is left alone; the real side wins only where it has an opinion.

cli_auth_credentials_store = "file" is re-enforced on every codex sync, so a profile can never be talked into a shared keychain. The edit is a structural splice over the raw file rather than a parse-and-rewrite, so comments, key order and formatting survive and an untouched region stays byte-identical — and a second sync writes nothing. Pass --no-sync-config to skip it.

Claude Agent View is disabled by default in shallow sessions (issue #49). Claude Code's Agent View feature (the --bg background-supervisor daemon) runs a long-lived, cross-session supervisor process that is not bound to the shallow profile's HOME. On resume, a shallow claude session would reconnect to an already-running supervisor bound to a different identity (typically the VM's primary Claude auth), silently bypassing shallow-spawn's per-identity auth isolation and using the wrong account. caam cannot control that daemon's lifecycle, so caam shallow-spawn -- claude injects CLAUDE_CODE_DISABLE_AGENT_VIEW=1 into the child environment by default. This keeps the session foreground and honoring the per-identity ~/.claude/.credentials.json.
> Escape hatches (both opt back into Agent View, accepting the auth-isolation caveat above):
- Pass --allow-agent-view on shallow-spawn — caam will not inject the disable flag for that invocation.
- Export CLAUDE_CODE_DISABLE_AGENT_VIEW yourself (to any value) before spawning — caam never overrides an explicit user setting.
> This only affects the claude provider; codex and agy shallow sessions have no Agent View feature and are unchanged.
Note: caam shallow-profile does not (yet) call any reverse-engineered Anthropic endpoints to display per-account live usage data. That's a separate concern tracked in the original report (issue #16) and intentionally deferred.

Supported Tools

| Tool | Auth Location | Login Command | |------|--------------|---------------| | Claude Code | OAuth: ~/.claude/.credentials.json + ~/.claude.json + ~/.config/claude-code/auth.json + (macOS) ~/Library/Application Support/Claude/config.json • API key: ~/.claude/settings.json | /login in CLI | | Codex CLI | ~/.codex/auth.json (file store enforced) | codex login (or --device-auth) | | Antigravity CLI | OAuth: ~/.gemini/antigravity-cli/antigravity-oauth-token (+ ~/.gemini/google_accounts.json) | agy interactive (Google OAuth) | | Gemini CLI (legacy) | OAuth: ~/.gemini/settings.json (+ oauth_creds.json) • API key: ~/.gemini/.env | gemini interactive | | Grok Build (xAI) | OAuth/OIDC: ~/.grok/auth.json (+ ~/.grok/config.toml); respects GROK_HOME | grok login (browser OIDC) |

Claude Code (Claude Max)

Subscription: Claude Max ($200/month)

Auth Files:

  • ~/.claude/.credentials.json — Claude Code OAuth credentials (primary)
  • ~/.claude.json — Session/account state
  • ~/.config/claude-code/auth.json — Secondary auth data
  • ~/.claude/settings.json — API key mode via apiKeyHelper
  • ~/Library/Application Support/Claude/config.json — macOS: Claude Desktop's encrypted OAuth token cache (only its oauth:tokenCache* fields are tracked, so recent Claude Code builds can't reassert the previous account after a switch)
macOS login keychain: on a Mac, Claude Code keeps the OAuth blob as a generic password in the login keychain (service Claude Code-credentials) and only falls back to ~/.claude/.credentials.json when the keychain is unreachable. caam treats the keychain as authoritative and that file as its 0600 mirror: backup reads the item into the profile, activate writes the profile's token back into it, and logout removes it. A locked keychain, or a denied access prompt, fails backup and activate loudly rather than reporting a switch that did not happen. caam doctor reports the item's readability; CAAM_KEYCHAIN=0 turns the bridge off and falls back to the file. Shallow profiles are unaffected — security derives the keychain from HOME, so a shallow lane has no login keychain and Claude Code uses that lane's own credentials file.

Login Command: Inside Claude Code, type /login

Notes: Claude Max has a 5-hour rolling usage window. When you hit it, you'll see rate limit messages. Switch accounts to continue.

Limitations:

  • Email/Identity Detection: Claude's current auth format does not expose email or account ID. Profile names default to timestamp-based auto-names (auto-YYYYMMDD-HHMMSS) unless you specify a name when backing up.
  • Automatic Token Refresh: Claude Code manages token refresh internally. CAAM cannot refresh Claude tokens—use /login in Claude Code if tokens expire.
  • Usage API: Claude's usage API is undocumented and may not be reliable.

Codex CLI (GPT Pro)

Subscription: GPT Pro ($200/month unlimited)

Auth Files:

  • ~/.codex/auth.json (or $CODEX_HOME/auth.json)
Login Command: codex login (or codex login --device-auth for headless)

Notes: Respects CODEX_HOME. CAAM enforces file-based auth storage by writing cli_auth_credentials_store = "file" to ~/.codex/config.toml inside the profile.

Running a codex app-server daemon? Codex can run as a long-lived daemon (codex app-server, also codex mcp-server) that caches auth.json in memory at startup. Swapping the auth file on disk does not change the account that daemon serves until it is restarted. After caam activate/switch/next codex, CAAM detects a running daemon and prints a warning. Pass --reload-daemon to have CAAM SIGTERM the daemon (it respawns with the new auth on next use) — it never kills a daemon silently.

Gemini CLI (Google One AI Premium)

Subscription: Gemini Ultra ($275/month)

Auth Files:

  • ~/.gemini/settings.json
  • ~/.gemini/oauth_creds.json (OAuth cache)
  • ~/.gemini/.env (API key mode)
Login Command: Start gemini, select "Login with Google" or use /auth to switch modes

Notes: For CAAM, Gemini Ultra behaves like Claude Max and GPT Pro: OAuth tokens are stored locally and can be swapped instantly.

Grok Build (xAI)

Auth Files:

  • ~/.grok/auth.json — login credential written by grok login (required)
  • ~/.grok/config.toml — CLI configuration (optional, travels with the account)
Login Command: grok login (browser OIDC via xAI accounts)

Notes: Respects GROK_HOME (documented override for the config directory, default ~/.grok). Grok Build tokens expire after 7 days; run grok login to refresh — CAAM cannot refresh them.

Caveats:

  • GROK_DEPLOYMENT_KEY precedence: in enterprise/deployment setups this environment variable takes precedence over auth.json, so a swapped profile is silently ignored while it is set.
  • ~/.grok collision: the unaffiliated community CLI superagent-ai/grok-cli (npm grok-dev) also uses ~/.grok/ but stores its state in grok.db / user-settings.json. CAAM touches only the official Grok Build files (auth.json, config.toml), so the two CLIs can coexist.

Quick Start

1. Backup Your Current Account

# After logging into Claude normally
caam backup claude [email protected]

2. Add Another Account

caam clear claude                        # Remove current auth
claude                                   # Login as [email protected] via /login
caam backup claude [email protected]         # Save it

3. Switch Instantly

caam activate claude [email protected]     # Back to Alice
caam activate claude [email protected]       # Back to Bob

4. Check Status

$ caam status
claude: [email protected] (active)
codex:  [email protected] (active)
gemini: (no auth files)

$ caam ls claude [email protected] [email protected] [email protected]


Command Reference

Auth File Swapping (Primary Use Case)

| Command | Description | |---------|-------------| | caam backup | Save current auth files to vault | | caam activate | Restore auth files from vault (instant switch!) | | caam status [tool] | Show which profile is currently active | | caam ls [tool] | List all saved profiles in vault | | caam delete | Remove a saved profile | | caam paths [tool] | Show auth file locations for each tool | | caam clear | Remove auth files (logout state) | | caam alias | Create a short alias for a profile | | caam rename | Copy profile to a new name (non-destructive) | | caam uninstall | Restore originals from _original and remove caam data/config |

Aliases: caam switch is the activation alias and works like caam activate. Note that caam use is a separate command that sets the default profile for a provider (it does not switch active auth files).

Quick Switch: pick + aliases

Use caam pick when you want the fastest possible profile swap:

caam pick claude           # fzf if installed; numbered prompt otherwise
caam pick                  # uses your default_provider if set

Set a default provider so you can omit the tool name:

caam config set default_provider claude

Aliases make long emails painless (works for pick and activate):

caam alias claude work-account-1 work
caam pick claude            # type "work" at the prompt
caam activate claude work   # alias resolution works here too

Rename auto-generated profiles to friendly names (non-destructive copy):

caam rename claude auto-20260121-143022 work   # Copy profile to "work"
caam rename claude old-name new-name           # Original preserved by default
caam rename claude temp main --delete-old -y   # Delete old after copying

SSH-safe fallback (no fzf, no TTY): use direct activation:

caam activate claude work-account-1

fzf one-liner (if you prefer piping):

sel=$(caam ls claude | fzf --prompt 'claude> ') && [ -n "$sel" ] && caam activate claude "$sel"

Smart Profile Management

Claude reports a separate weekly allowance per model (Opus, Fable) alongside the 5-hour and weekly windows, and an account can exhaust one of those while its general windows still read as idle. caam treats a spent per-model allowance as a ceiling like any other, so such an account is not offered for work on that model. Pass --model to caam limits or caam precheck — or just run caam run claude --precheck -- --model opus …, which reads the model off the passed-through arguments — and only that model's own allowance constrains the choice; with no model given, every per-model allowance counts.

| Command | Description | |---------|-------------| | caam activate --auto | Auto-select the best profile using rotation algorithm | | caam next | Switch to the next profile in rotation (use --dry-run to preview without switching) | | caam run [-- args] | Wrap CLI execution with automatic failover on rate limits | | caam limits [--model ] | Live rate-limit usage, including each account's per-model allowance | | caam limits claude --cached | The same view offline, from the snapshot Claude Code caches on disk (no network, no token presented) | | caam limits --profile --source vault\|isolated\|shallow | Read a specific credential namespace | | caam limits --rank earliest-reset-headroom | Rank seats for new work: spend the included quota that refreshes soonest, preserve the rest | | caam cooldown set | Mark profile as rate-limited (default: 60min cooldown) | | caam cooldown list | List active cooldowns with remaining time | | caam cooldown clear | Clear cooldown for a specific profile | | caam cooldown clear --all | Clear all active cooldowns | | caam project set | Associate current directory with a profile | | caam project show [tool] | Show resolved associations for current directory (get is an alias; --json for machine-readable output) | | caam project list | List all project associations (--json supported) |

Offline usage: caam limits --cached

caam limits answers "which account still has headroom" by querying the provider. Claude Code also caches the figures it last received in each account's own .claude.json, and --cached reads those files instead: no request is made and no token is presented.

caam limits claude --cached
caam limits claude --cached --best        # only accounts caam actually has data for
caam limits claude --cached --format json

The trade-off is freshness. A profile's snapshot only moves when that profile itself runs a session, so an account you are not currently using may be hours or days stale - or have no snapshot at all. The offline table is explicit about both:

  • an AS OF column per row (the snapshot's own timestamp, or unknown when
it carries none - never 0s ago);
  • a profile with nothing cached reads no cached data, not 0%, and is
excluded from --best and from the recommendations. An account caam knows nothing about is never offered as the one with room;
  • a window whose reset time had already passed when the snapshot was written
reads 0% (rolled), so a stale zero is not mistaken for a measured one.

In --format json these appear as source: "cache", the window-level rolled flag, and fetched_at set to the snapshot's own timestamp rather than the time caam read it. Only Claude keeps such a cache; --cached on another provider is an error rather than an empty table.

Picking a seat for new work: caam limits --rank

--best answers "which seat is idlest". That is the right question when you are rotating away from a seat you are burning, and the wrong one when you are handing a seat to a brand-new session: on a pool of subscription seats the idlest one is usually the reserve you meant to keep, while the seat whose included allowance expires tomorrow goes unspent.

--rank earliest-reset-headroom answers the second question:

caam limits codex --rank earliest-reset-headroom --format json
caam limits claude --rank earliest-reset-headroom --model fable
caam limits codex --rank earliest-reset-headroom --headroom 80

The ordering is:

  1. Included allowance with headroom, earliest refresh first — spend quota
that is about to be lost, and so preserve the later-resetting seats.
  1. Paid credits (included allowance already spent, credits remain) — usable,
always last.
  1. Not eligible: spent with no credits, limits that could not be read, no
future reset time to order by, or a missing model-scoped allowance.

It sorts on the reset time of the longest allowance a seat reports — its weekly cap, not the five-hour window that rolls over on its own several times a day, which is the quota actually at risk of expiring unused. (This is where it differs from --policy drain, which ranks on the soonest reset of any window.)

An ineligible seat stays in the output with the reason it was passed over, and when nothing is selectable the command exits non-zero with selected: null and a populated error. That matters for a caller that spawns sessions: the failure this mode exists to prevent is falling through to a static pin when the live numbers could not be read, so it never answers confidently on missing data.

A named --model tightens this further. An account can exhaust its weekly Fable or Opus allowance while its general windows still read idle, so that allowance counts as the binding window; and if the provider did not report a row for that model at all, the seat is unknown, not spare capacity. Pass --require-model-window=false to rank it anyway.

The headroom ceiling defaults to stealth.rotation.drain_headroom_ceiling (95% used if unset) — the same setting the drain policy uses, because it is the same concept — and --headroom N overrides it for one call. 95 rather than 100 because a seat that is 99% spent has enough left to accept a session and not enough to finish one.

--rank availability names the historical --best ordering explicitly. --best itself is unchanged.

This is a read. It ranks; it does not activate anything, swap a credential, or touch a running session.

JSON shape

{
  "rank": "earliest-reset-headroom",
  "provider": "codex",
  "model": "",
  "headroom_ceiling_percent": 95,
  "require_model_window": false,
  "generated_at": "2026-09-10T12:24:45Z",
  "selected": { "...": "the top-ranked eligible profile, or null" },
  "profiles": [
    {
      "provider": "codex",
      "profile": "work",
      "rank": 1,
      "eligible": true,
      "tier": "included_headroom",
      "reason": "included allowance 34% used (under the 95% ceiling), secondary resets in 20h0m",
      "used_percent": 34,
      "binding_window": "secondary",
      "headroom_percent": 66,
      "governing_window": "secondary",
      "resets_at": "2026-09-11T08:00:00Z",
      "resets_in_seconds": 72000,
      "availability_score": 74,
      "has_credits": false,
      "plan_type": "pro"
    }
  ],
  "error": ""
}

tier is one of included_headroom, paid_credits, exhausted, unknown. rank is 1-based over the eligible profiles and 0 for ineligible ones. error is non-empty exactly when selected is null.

Credential namespaces: caam limits --profile ... --source

One profile name can exist in three unrelated stores at once:

| Namespace | Where | Written by | |-----------|-------|------------| | vault | /// | caam backup / caam activate | | isolated | the profile's own HOME and XDG config dir | caam login, or an in-app /login under caam exec | | shallow | ~/orch-homes// | signing in inside a shallow-spawn session |

--profile NAME still reads the vault by default, but it no longer stays quiet about it. Claude is the case that made this matter: Claude cannot use caam login, its supported isolated-profile flow is caam exec claude plus an in-app /login, and that flow never touches the vault - so the one provider whose login path cannot refresh the vault copy was being reported purely from the vault copy, and a healthy account came back unauthorized: token expired or invalid.

Now:

  • output names the namespace and path actually read, in the table and as
credential_source in --format json;
  • other namespaces holding the same name are listed with their state
(healthy / expired / unknown);
  • if an unselected namespace holds a strictly healthier credential and you
did not choose one, the lookup fails with the exact commands that disambiguate it, rather than emitting a routing verdict drawn from the stale copy. A controller can fail closed on that;
  • --source vault|isolated|shallow is the explicit override, and also works
without --profile to list every profile in one namespace.

Credentials are never copied between namespaces: rotating OAuth credentials copied behind your back is how two lanes end up invalidating each other.

Options for caam run:

  • --max-retries N — Maximum retry attempts on rate limit (default: 1)
  • --cooldown DURATION — Cooldown duration after rate limit (default: 60m)
  • --algorithm NAME — Rotation algorithm: smart, round_robin, random
  • --policy NAME — Rotation policy: availability (default), drain
  • --quiet — Suppress profile switch notifications
Rotation policies (--policy on caam precheck, caam next, and caam run, or stealth.rotation.policy in ~/.caam/spm_config.yaml):

  • availability (default) — maximize immediate headroom; this is the existing behavior and remains unchanged unless you opt in to another policy.
  • drain (opt-in) — prefer the profile whose included quota resets soonest, among profiles under a headroom ceiling (default: 95% used; configurable via stealth.rotation.drain_headroom_ceiling). This drains expiring subscription quota before it is lost instead of leaving it unused while a fresher account is consumed. Profiles at/above the ceiling or without a known reset time are held in reserve, ranked by availability. Selections include an explanation, e.g. chose work: resets in 42m, 91% used; fallback personal held in reserve. Pair with --usage-aware on caam next so reset times are fetched.
Rotation policies decide which profile caam switches the host to. To rank seats for a new session without switching anything, use caam limits --rank instead — it is a read, and it ranks on the weekly allowance rather than the soonest window.

Options for caam activate:

  • --auto — Use rotation algorithm to pick best profile
  • --backup-current — Backup current auth before switching
  • --force — Activate even if profile is in cooldown
When stealth.cooldown.enabled is true in config, caam activate warns if the target profile is in cooldown and prompts for confirmation. Use --force to bypass.

When stealth.rotation.enabled is true, caam activate automatically falls back to rotation if the default profile is in cooldown.

Uninstall Notes

caam uninstall restores auth from any available _original backups first, then removes caam’s data/config. Useful flags:

  • --dry-run shows what would be restored/removed
  • --keep-backups keeps the vault after restoring originals
  • --force skips the confirmation prompt

Profile Isolation (Advanced)

| Command | Description | |---------|-------------| | caam profile add | Create isolated profile directory | | caam profile ls [tool] | List isolated profiles | | caam profile delete | Delete isolated profile | | caam profile status | Show isolated profile status | | caam login | Run login flow for isolated profile | | caam exec [-- args] | Run CLI with isolated profile |


Smart Profile Management

When you have multiple accounts across multiple providers, manually tracking which account has headroom, which one just hit a limit, and which one you used recently becomes tedious. Smart Profile Management automates this decision-making so you can focus on coding instead of account juggling.

Profile Health Scoring

Each profile displays a health indicator showing its current state at a glance:

| Icon | Status | Meaning | |------|--------|---------| | 🟢 | Healthy | Token valid for >1 hour, no recent errors | | 🟡 | Warning | Token expiring within 1 hour, or minor issues | | 🔴 | Critical | Token expired, or repeated errors in the last hour | | ⚪ | Unknown | No health data available yet |

Health scoring combines multiple factors:

  • Token expiry: How long until the OAuth token expires
  • Error history: Recent authentication or rate limit errors
  • Penalty score: Accumulated issues with automatic decay over time
  • Plan type: Enterprise/Pro plans get slight scoring boosts
The penalty system uses exponential decay (20% reduction every 5 minutes) so temporary issues don't permanently mark a profile as unhealthy. After about 30 minutes of no errors, a profile's penalty score returns to near zero.

Refreshable tokens are not expired accounts

A short-lived access token that can be renewed without a human is not an unhealthy account, and caam does not report it as one. Every provider's credential carries a refresh token or it does not, and that — not the raw expiry timestamp — decides the verdict. Codex is the case that forced the distinction: its access token routinely sits expired for days while the CLI renews it from the refresh token on next use, and three live accounts were reading warning in caam ls from an expiry months in the past.

Two questions used to share one flag, and they have different answers:

| Question | Consumer | Claude | Codex / Grok / Gemini (with a refresh token) | |----------|----------|--------|-----------------------------------------------| | "Should caam refresh this soon?" | warnings, the refresh daemon | no — Claude Code renews itself and caam's Claude refresh is disabled | yes — caam has a refresher and runs off this signal | | "Must a human log in again?" | caam ls status, rotation eligibility | no | no |

caam ls --json and caam status --json therefore carry three additive signals per profile alongside the composite status:

| Field | Meaning | |-------|---------| | refresh_due | caam should renew this credential soon. false for a self-refreshing Claude credential (caam must leave it alone) and for one with no refresh token (there is nothing to renew from — it needs a login, not a scheduler). | | launch_usable | a new session can start on this account right now — this is what a rotation controller should route on, not warning severity | | login_required | a human must re-authenticate: the credential has lapsed and carries nothing to renew itself with |

Each is null when caam has no evidence either way. Unknown stays unknown; it is never promoted to healthy or to login-required. An active rate-limit cooldown sets launch_usable to false on its own, since nothing can start until the cap clears — but it is not a login problem, so login_required stays false.

A lapsed-but-renewable credential shows as Auto-refresh rather than Expired, and its recommendation is caam refresh , never caam login (a login is disruptive and would fix nothing).

Smart Rotation Algorithms

When you run caam activate claude --auto, the rotation system picks the best profile for you. Three algorithms are available:

Smart (Default): Multi-factor scoring that considers:

  • Cooldown state (profiles in cooldown are excluded)
  • Health status (prefers healthy profiles)
  • Recency (avoids profiles used in the last 30 minutes)
  • Plan type (slight preference for higher-tier plans)
  • Random jitter (breaks ties unpredictably)
Round Robin: Simple sequential rotation through profiles, skipping any in cooldown. Predictable and even distribution.

Random: Purely random selection among non-cooldown profiles. Least predictable but may cluster usage.

Configure the algorithm in ~/.caam/config.yaml:

stealth:
  rotation:
    enabled: true
    algorithm: smart  # smart | round_robin | random

Cooldown Tracking

When an account hits a rate limit, you can mark it as "in cooldown" so rotation algorithms skip it:

# Mark current Claude profile as rate-limited (default: 60 min cooldown)
caam cooldown set claude

Or specify a profile and duration

caam cooldown set claude/[email protected] --minutes 120

View active cooldowns

caam cooldown list

Clear a cooldown early

caam cooldown clear claude/[email protected]

When cooldown enforcement is enabled (stealth.cooldown.enabled: true), attempting to activate a profile in cooldown will warn you and prompt for confirmation. This prevents accidentally switching back to an account that just hit limits.

Automatic Failover with caam run

The caam run command wraps your AI CLI execution and automatically handles rate limits:

# Instead of running claude directly:
caam run claude -- "explain this code"

If Claude hits a rate limit mid-session:

1. Current profile goes into cooldown

2. Next best profile is automatically selected

3. Command is re-executed with new account

For seamless integration, add shell aliases:

alias claude='caam run claude --'
alias codex='caam run codex --'
alias gemini='caam run gemini --'

Now you can use claude "explain this code" and rate limits are handled transparently.

Configuration options:

caam run claude --max-retries 2 --cooldown 90m --algorithm smart -- "your prompt"

Project-Profile Associations

Link specific profiles to project directories so you don't have to remember which account to use where:

# In your work project directory
cd ~/projects/work-app
caam project set claude [email protected]

Now whenever you're in this directory (or subdirectories)

caam activate claude # Automatically uses [email protected]

The TUI also shows the project association

caam tui

Status bar shows: Project: ~/projects/work-app → [email protected]

Associations cascade: if you set an association on /home/user/projects, it applies to all subdirectories unless a more specific association exists.

In the TUI, press p to set the current profile as the default for your current directory.

Preview Rotation Selection

Before committing to a rotation selection, preview what the algorithm would pick:

$ caam next claude
Recommended: [email protected]
  + Healthy token (expires in 4h 32m)
  + Not used recently (2h ago)

Alternatives: [email protected] - Used recently (15m ago)

In cooldown: [email protected] - In cooldown (45m remaining)

This is useful for understanding why rotation is making certain choices, or for scripting conditional logic around account selection.


Workflow Examples

Daily Workflow

# Morning: Check what's active
caam status

claude: [email protected] (active)

codex: [email protected] (active)

gemini: [email protected] (active)

Afternoon: Hit Claude usage limit

caam activate claude [email protected]

Activated claude profile '[email protected]'

claude # Continue working immediately with new account

Initial Multi-Account Setup

# 1. Login to first account using normal flow
claude

Inside Claude: /login → authenticate with [email protected]

2. Backup the auth using the email as the profile name

caam backup claude [email protected]

3. Clear and login to second account

caam clear claude claude

Inside Claude: /login → authenticate with [email protected]

4. Backup that too

caam backup claude [email protected]

5. Now you can switch instantly forever!

caam activate claude [email protected] # < 100ms caam activate claude [email protected] # < 100ms

Parallel Sessions Setup

# Create isolated profiles
caam profile add codex [email protected]
caam profile add codex [email protected]

Login to each (one-time, uses browser)

caam login codex [email protected] # Opens browser for work account caam login codex [email protected] # Opens browser for personal account

Run simultaneously in different terminals

caam exec codex [email protected] -- "implement auth system" caam exec codex [email protected] -- "review PR #123"

Smart Rotation Workflow

# Let rotation pick the best profile automatically
caam activate claude --auto

Using rotation: claude/[email protected]

Recommended: [email protected]

+ Healthy token (expires in 4h 32m)

+ Not used recently (2h ago)

Hit a rate limit during your session? Mark it

caam cooldown set claude

Recorded cooldown for claude/[email protected] until 14:30 (58m remaining)

Next activation automatically picks another profile

caam activate claude --auto

Using rotation: claude/[email protected]

Recommended: [email protected]

+ Healthy status

In cooldown:

[email protected] - In cooldown (57m remaining)

Zero-Friction Mode with caam run

# Add aliases to your .bashrc/.zshrc
alias claude='caam run claude --'
alias codex='caam run codex --'

Now just use the tool normally

claude "explain this authentication flow"

If you hit a rate limit mid-session, caam automatically:

1. Marks current profile as in cooldown

2. Selects next best profile via rotation

3. Re-runs your command with the new profile

All transparent - you just see the output


Vault Structure

~/.local/share/caam/
├── vault/                          # Saved auth profiles
│   ├── claude/
│   │   ├── [email protected]/
│   │   │   ├── .claude.json        # Backed up auth
│   │   │   ├── auth.json           # From ~/.config/claude-code/
│   │   │   └── meta.json           # Timestamp, original paths
│   │   └── [email protected]/
│   │       └── ...
│   ├── codex/
│   │   └── [email protected]/
│   │       └── auth.json
│   └── gemini/
│       └── [email protected]/
│           └── settings.json
│
└── profiles/                       # Isolated profiles (advanced)
    └── codex/
        └── [email protected]/
            ├── profile.json        # Profile metadata
            ├── codex_home/         # Isolated CODEX_HOME
            │   └── auth.json
            └── home/               # Pseudo-HOME with symlinks
                ├── .ssh -> ~/.ssh
                └── .gitconfig -> ~/.gitconfig

TUI Configuration

Customize the TUI appearance and behavior through ~/.caam/config.yaml:

tui:
  theme: auto          # auto | dark | light
  high_contrast: false # Enable high-contrast colors for accessibility
  reduced_motion: false # Disable animated UI effects (spinners)
  toasts: true         # Show transient notification messages
  mouse: true          # Enable mouse support
  show_key_hints: true # Show keyboard shortcuts in status bar
  density: cozy        # cozy | compact
  no_tui: false        # Disable TUI, use CLI-only mode

Environment Variable Overrides

Environment variables take precedence over config file settings:

| Variable | Values | Description | |----------|--------|-------------| | CAAM_TUI_THEME | auto, dark, light | Color scheme | | CAAM_TUI_CONTRAST | high, hc, 1, true | High contrast mode | | CAAM_TUI_REDUCED_MOTION | true, false | Disable animations | | REDUCED_MOTION | 1 | Standard accessibility env var | | CAAM_TUI_TOASTS | true, false | Toast notifications | | CAAM_TUI_MOUSE | true, false | Mouse support | | CAAM_TUI_KEY_HINTS | true, false | Keyboard hints | | CAAM_TUI_DENSITY | cozy, compact | UI spacing | | CAAM_NO_TUI or NO_TUI | true, 1 | Disable TUI entirely |

Managing TUI Config via CLI

```bash

... (README truncated for length)

Chat with me