# Account and fleet operations

## Accounts

```bash
claude-accounts list      # pool at a glance
claude-accounts status    # per-account auth health, per-bucket limits incl. Fable, markers, last pick
claude-accounts expired   # ONLY the accounts that cannot authenticate, and why (exit 1 if any)
claude-accounts relogin   # sign those back in, one at a time (ids or --all to pick your own)
```

Every account in the pool is authenticated — `add` cannot register one otherwise.

### When an account stops working

Subscription logins expire (the refresh token has a finite life), can be revoked from
claude.ai, and an organization can disable Claude Code for one of its accounts entirely.
Such an account is **never selected** — `claude` keeps working on the healthy ones — and
shows up here:

```bash
claude-accounts expired
#   acct-01   support@example.com   EXPIRED   refresh token expired 2026-08-05 (2d ago)
#                                             fix: claude-accounts relogin acct-01
#   acct-08   dev@example.com       BLOCKED   this account's organization has disabled
#                                             Claude Code subscription access
#                                             fix: claude-accounts relogin acct-08

claude-accounts relogin              # re-authenticate everything a sign-in can fix
claude-accounts relogin acct-01      # or just one
claude-accounts relogin acct-01 --token   # portable token instead (Mac + server)
```

`relogin` covers `BLOCKED` accounts too — a fresh sign-in re-issues the grant and usually
clears the block. Use `claude-accounts verify` (a real `-p` call per account) to re-test
one at any time; a PASS clears the park automatically.

`relogin` runs the same verified sign-in as `login` (it refuses to save a credential for
the wrong email), one account at a time, and syncs to the server once at the end.
`expired` exits **1** when anything needs a human, so it drops straight into a cron or
monitoring check. `list` tags such accounts `EXPIRED-LOGIN`, and `status` says
`selectable: NO` with the reason.

### Adopt this machine's existing login (no browser needed)

```bash
claude-accounts import you@example.com --id acct-01 --home mac
claude-accounts adopt acct-01
```

`adopt` makes the account dir a **symlink to `~/.claude`** — the machine's existing
login joins the pool with a single credential file (no copy of the OAuth grant exists
anywhere else, so refreshes can never race). This is how the two current accounts run:
acct-01 = the Mac's login, acct-02 = the server's login (adopted server-side).

### Add a fresh account (~1 min, one browser sign-in)

```bash
claude-accounts add               # no email needed — read back from the sign-in
# or name it explicitly (fast-fails on a duplicate, and pre-fills the login page):
claude-accounts add new@example.com
```

This runs the **normal full Claude Code login** — the exact same
`claude auth login` flow the app itself uses (`claude.com/cai/oauth/authorize`, full
Claude Code scopes). A browser opens (or a sign-in link is printed); sign in to the
account you're adding, approve, and if a code is shown, paste it back. It draws on the
subscription — never API keys.

Using the full login on purpose: it does **not** hit the *"Sign in again to continue"*
step-up wall that the narrow long-lived-token grant triggers, and it stores an
auto-refreshing login — `.credentials.json`, or on macOS the login Keychain whenever
the session can open it (Claude Code migrates the file into the Keychain on the first
refresh from a keychain-capable session; ssh sessions then see the account as
`KEYCHAIN LOCKED` and cannot run it, while the Mac's own session uses it normally).
That credential is **machine-local** (never synced),
which is what keeps two machines from invalidating each other's refresh token — so an
account you `add` on the Mac runs on the Mac, and you `add` it on the server (over SSH) if
you want it there too. The account is registered **only after the signed-in email is read
back**; an aborted login leaves nothing behind, and duplicate emails are refused.

```bash
claude-accounts login acct-NN         # complete/refresh an existing account (full login)
claude-accounts add new@example.com --token   # portable setup-token instead: works on BOTH
                                              # machines and syncs to the server, but needs a
                                              # recent sign-in (may show the step-up screen)
claude-accounts login acct-NN --token         # same, for an existing account
```

Use `--token` when you specifically want one account usable on the server via the Mac's
sync (the ~1-year inference-only token that mirrors over). For everyday adds, the default
full login is smoother. `status` warns when a token nears end of life; re-run `login`.

### A setup token has no identity — name the account yourself

A setup token is minted with scope `user:inference` **alone**, so `claude auth status`
answers `{loggedIn, authMethod, apiProvider}` and no email — only an OAuth login reports
`email`/`orgId`/`subscriptionType`. Nothing on this side can therefore learn which account
approved a `setup-token` grant, and no future check will change that.

What follows, and what the CLI does about it:

- **`add <email> --token` requires the email** — there is nothing to read the account back
  from. It registers under the name you gave and says so; naming no email is refused
  outright rather than registering an anonymous slot.
- **`mint <acct-NN>` names the account in its prompt** (`approve in a browser signed in as
  …`) and warns afterwards that the identity is unverifiable. Approving in a window signed
  into a *different* account silently binds that account's subscription to the slot —
  every machine then runs it under the wrong name. Sign in to the right account **first**,
  in a fresh private window, then paste the link.
- `login <acct-NN> --token` compares against the manifest when an identity is readable
  (it is not, today) and otherwise says what it is trusting.

Both accounts currently in the pool are live and verified: acct-01 (support@gowalkae.com,
Mac) and acct-02 (hasan@gowalkqa.com, server) — each adopted from its machine's existing
login, so each runs on the machine that holds its credential.

### Remove

```bash
claude-accounts remove acct-NN          # deletes locally, propagates to the server
```

## Sync (multi-machine)

One machine is the **source of truth**; every other machine is a **target**. Sync is
one-way, fired automatically by `add`/`import`/`remove`/`mint` on the source and
manually via:

```bash
claude-accounts sync      # and codex-accounts sync for the codex pool
```

Account creation and sync seed missing settings from the machine's default provider
directory and link its global instructions: `~/.claude/CLAUDE.md` for Claude,
`~/.codex/AGENTS.md` for Codex. Existing account settings (including models) and
instruction files or links are preserved. The addon does not generate instructions
or enable/disable agents. To share newly created global instructions with existing
accounts, run `claude-accounts sync --no-server` and/or `codex-accounts sync --no-server`.
This only repairs accounts in the manifest, leaving retired directories untouched.
Global instruction files remain machine-local; sync does not copy them between hosts.

Targets are the resolved sync target — env override, else the manifest's
`server`/`server_root`/`server_repo` (the Linux box), see *Pointing sync somewhere else*
below — plus an optional `peers` array for additional machines (e.g. a second Mac):

```json
"peers": [{"target": "gas@gas-mini", "root": "/Users/gas/.claude-accounts",
           "repo": "/Users/gas/claude-multiacc"}]
```

Every target gets the manifest + per-account `server.token` (0600, claude pool) +
one-time seeds + advisory limit state; **never** `.credentials.json` or codex
`auth.json` in either direction (their refresh tokens rotate — two machines
refreshing one grant invalidate each other; codex accounts are signed in per
machine with the device-code flow instead). Removals propagate to every target; an
empty/corrupt manifest — or one with an invalid target/peer shape — refuses to sync
before anything is pushed anywhere. After every push each target re-seeds dirs and
re-runs its quick verification matrix (`post-sync` hook). Everything logs to
`sync.log`; failures are loud and non-zero.

A target machine's pool carries a `sync-role` file containing `replica`: such a pool
**never pushes** (manually or via auto-sync) — the source machine owns the account
set, which is what prevents two machines from overwriting each other's manifests.
Make account changes on the source; sign-ins (`login`/`relogin`) still happen on
whichever machine needs the credential.

### MCP servers for every account

MCP servers live in the config dir, i.e. per account. Never register one with a stock
`claude mcp add` on a pool Mac and expect it everywhere — under the shim that command is
mirrored into the registry (see below), but the registry is the source of truth, and it is
edited on the **source** machine (a replica's own changes land in its machine-local overlay
and are not pushed anywhere):

```bash
claude-accounts mcp add appinspire-mcp -- npx -y appinspire-mcp@latest serve
    # writes ~/.claude-accounts/mcp-servers.json AND ~/.codex-accounts/mcp-servers.json,
    # reconciles every account of both pools, auto-syncs the registry to server + peers
claude-accounts mcp add --provider claude --scope project --project "$HOME/design-lab/run1" appinspire -e APPINSPIRE_LIBRARY_DIR="$HOME/.appinspire-mcp/library" -- node "$HOME/appinspire-mcp/bin/appinspire-mcp.mjs" serve
    # a checkout, registered for ONE project (claude only: codex reads no project config);
    # one -e per variable, and $HOME rather than ~ inside a value
claude-accounts mcp remove adspower-local-api   # retire EVERYWHERE: user scope + every project entry, hand-added copies too
claude-accounts mcp remove foo --user-only      # user scope only
claude-accounts mcp list                        # effective registry (+ machine-local overlay)
claude-accounts mcp apply                       # re-apply to every account now (exit 3 from add/remove = do this after fixing the account it named)
codex-accounts mcp list                         # the codex pool's view
```

How a fleet Mac receives it: `sync` (and every claude limits pass) pushes the claude
`mcp-servers.json` next to the manifest to the server and every peer; the target's
`post-sync` re-seeds its account dirs, which applies the registry; and the shim there
re-applies it to the picked account right before exec whenever the file changed. The codex
registry travels only to codex sync targets (the server, gas-mini) — runner Macs' codex pools
are local-only and are fed by app-robot's runner overlay (`codex-accounts mcp import-local
--owner app-robot`) or a direct `codex-accounts mcp add --provider codex …` on that Mac. A
runner daemon's overlay (`<provider>-accounts mcp import-local --owner NAME`, JSON on stdin)
is applied on the Mac and never synced. `--help` on `mcp` lists every option;
`CLAUDE_MULTIACC_MCP=0` / `CODEX_MULTIACC_MCP=0` stop the reconcile at seed and launch and the
shim's mirror (the CLI, sync and fan-out keep working).

### Pointing sync somewhere else (or nowhere)

The target is no longer baked in. In precedence order:

1. `CLAUDE_MULTIACC_SYNC_TARGET` / `CODEX_MULTIACC_SYNC_TARGET` (or the provider-neutral
   `MULTIACC_SYNC_TARGET`), plus `…_SYNC_ROOT` and `…_SYNC_REPO` — env, so a daemon can
   point one instance at its own target without rewriting a manifest it does not own;
2. the manifest's `server` / `server_root` / `server_repo` (what `install.sh --server`
   wrote);
3. the historical default — unchanged, so existing installs keep pushing where they did.

A pool whose `sync-role` file says `replica` still never pushes, in any mode — the
marker is honored and reported before anything else, and local-only mode below only
narrows it further (a local pool pushes nothing whether or not it is a replica).

The value **`none`** (also `local`, `off`, `disabled`, empty) selects **local-only** mode:
there is no ssh target at all because something else — the app-robot panel and its runner
daemon — distributes the pool. `sync` then validates the manifest, re-seeds account dirs,
fixes permissions and pushes nowhere; mutations stop auto-pushing (and stop warning about
a server they are not supposed to reach). One run can force it with `sync --no-server`,
and a whole install can start that way:

```bash
./install.sh --no-server          # manifest server = none
claude-accounts sync              # "sync ok (local-only: … nothing pushed)"
claude-accounts sync --no-server  # same, whatever the manifest says
```

## Fleet integration (panels, daemons, multiple instances)

Everything below is non-interactive and machine-readable: it is what a supervising panel
(app-robot) or any other daemon needs to run a pool across a fleet of Macs.

### `--json` on `list`, `status` and `limits`

```bash
claude-accounts list --json      # pool + accounts, no refresh
claude-accounts status --json    # the same, plus last_picked and credential detail
claude-accounts limits --json    # refresh usage first, then emit the same document
codex-accounts  list --json      # identical schema, provider: "codex"
```

Stdout is the JSON document and nothing else. One shape for every verb and both
providers (`lib/report.py`), so a consumer writes one parser:

```jsonc
{
  "schema": "claude-multiacc/pool.v1",   // bumped only on a breaking change
  "provider": "claude", "kind": "list", "generated_at": "…Z",
  "machine": "mac", "host": "my-mini",
  "pool": {
    "root": "/Users/gas/.claude-accounts", "manifest": "…/accounts.json",
    "threshold": 90,
    "sync": {"mode": "server|local", "target": "root@…", "root": "…", "repo": "…",
             "role": "source|replica", "peers": [...]}
  },
  "accounts": [{
    "id": "acct-05", "email": "…", "home": "mac", "added_at": "…Z",
    "home_dir": "/Users/gas/.claude-accounts/acct-05", "adopted": false,
    "status": "active",            // active|limited|expired|token-invalid|blocked
                                   // missing|remote
    "state": "ok", "label": "OK", "reason": "…", "fix": "…",
    "selectable": true, "needs_login": false,
    "credential_class": "portable", // portable|machine-local|none
    "portable": true,
    "credentials": {"oauth": true, "token": true, "oauth_expires_at": "…Z",
                    "oauth_refresh_expires_at": "…Z", "token_minted_at": "…Z",
                    "token_age_days": 39},
    "limited": false, "limit_reset_at": null, "limit_detail": null,
    "usage": {"fetched_at": "…Z", "age_seconds": 41, "source": "oauth",
              // a null percent = that signal had no informative bucket; all three
              // null = limits.json "no_data": true
              "max_percent": 62, "weekly_percent": 62, "session_percent": 18,
              "buckets": [{"name": "weekly_scoped:Fable", "group": "weekly",
                           "percent": 62, "resets_at": "…Z"}]}
  }],
  "summary": {"total": 3, "active": 2, "limited": 1, "needs_login": 0,
              "portable": 3, "selectable": 2,
              // pool-wide ranking verdict: fresh | degraded | blind | none.
              // "fresh" needs ONE candidate with both percentages inside the window;
              // "blind" = every account scores the same and selection is random.
              "telemetry": "fresh", "ranking_blind": false},
  "warnings": []
}
```

`status` values mean exactly what the shim does: only `active` accounts are selected;
`limited` authenticates but is parked until `limit_reset_at`; `expired`/`blocked`/
`missing` need a human (`fix` says what); `remote` is not logged in here on purpose.
A consumer that only distinguishes *usable / parked / needs-a-human* can read
`selectable` and `needs_login` and ignore the rest; `state` keeps the raw audit verdict
without the limit overlay. `limited` follows the shim's marker rule exactly — a marker
whose reset time has passed does not count, an unreadable one does.

A percent field is **null** when that signal had no informative bucket to aggregate. The
writers keep the signals separate: `weekly_percent` comes from informative weekly buckets,
`session_percent` from informative session ones, and each is omitted on its own when its
group has nothing informative — a payload with a real session bucket and an all-zero weekly
one reports the session and leaves `weekly_percent` null rather than borrowing the session
number. `"no_data": true` appears only when NOTHING in the payload is informative (every
bucket 0% with no *parseable* reset window — the `buckets` array is kept verbatim for
diagnostics and, on codex, may still show a synthesized epoch), and then all three
percentages are absent together; a
weekly-only or session-only document carries no `no_data` flag. The rule behind it: 0% with
no window is the endpoint declining to answer rather than an idle account (2026-09-04 —
reading it as 0% ranked two weekly-exhausted accounts first). Either way the account ranks
*unknown*, not empty — selection needs BOTH percentages — and its `buckets` are still
emitted for diagnostics.

### Which accounts are portable

| Credential | Where it lives | Class | Can it be copied to another machine? |
| --- | --- | --- | --- |
| claude setup-token | `<acct>/server.token` | **portable** | **Yes**; the fleet-distributed credential |
| claude OAuth login | `<acct>/.credentials.json`, or a macOS Keychain item | machine-local | **No**; refresh tokens rotate |
| codex ChatGPT login | `<acct>/auth.json` | machine-local | **No**; Codex has no portable credential |

Non-portable accounts are not a dead end: they show up as `credential_class:
"machine-local"`, and the way to run one somewhere else is one interactive sign-in there
(`claude-accounts login <id>`, `codex-accounts login <id>` — the codex device-code flow
works over SSH). A claude account can be *converted* to the portable class with one
`claude-accounts mint <id>` on a machine that is already signed in.

### `export-credential` / `import-credential`

```bash
claude-accounts export-credential acct-05                 # blob to stdout
claude-accounts export-credential acct-05 --out cred.json # …or to a 0600 file
claude-accounts export-credential acct-05 --identity-only # registry entry, no secret
cat cred.json | claude-accounts import-credential         # install it here
claude-accounts import-credential --in cred.json --home mac --no-sync
```

`export-credential` is **read-only on the source pool** — nothing is refreshed, re-minted
or marked, so exporting a live account cannot disturb it. It refuses anything that is not
portable, with the reason and the fix, and its exit code is the contract a daemon branches
on:

| Exit | Meaning |
| --- | --- |
| 0 | blob written |
| 2 | usage error / unknown account / malformed blob |
| 3 | the credential is machine-local — copying it would break both machines |
| 4 | the account has no credential material here |
| 5 | credential present but unusable (corrupt, or an API key rather than a setup-token) |

The blob is self-contained JSON (`format: "claude-multiacc/credential"`, version 1):
identity metadata (`id`, `email`, `home`, `added_at`), where it came from, and — for
class `portable` — the credential itself. **Treat a `portable` blob exactly like the
credential it contains.**

`import-credential` reads it on stdin (or `--in PATH`), creates the account dir and the
manifest entry as needed, writes the credential 0600, and clears any dead-auth marker.
It is idempotent **by email**: re-importing an account that is already in the pool
refreshes it in place rather than adding a second entry, so a daemon can push the same
pool to a machine over and over. It refuses a blob from the other provider and refuses an
API key. It also refuses to land an email in a second slot, or to write into a slot that
belongs to a different account — `--force` settles exactly those identity conflicts, and
nothing else: an **adopted** (symlinked) account is refused unconditionally, `--force`
included, because writing a credential through that link would put it outside the pool.
An `identity` blob registers the account and says which sign-in it still needs. The
imported account keeps the `added_at` it had on the source machine, so the same account
reads identically everywhere.

For codex, `export-credential` always refuses with exit 3 (there is nothing portable to
export) and points at the device-code login; `--identity-only` is how a codex account list
reaches another machine.

### Instance-scoped pools

`CLAUDE_ACCOUNTS_ROOT` / `CODEX_ACCOUNTS_ROOT` relocate a pool, so several app-robot
instances can share one machine and user account without ever seeing each other's
accounts. Both the CLIs **and the shims** resolve the same root, so a task launched with
the env set runs under that instance's pool. (The older `…_ACCOUNTS_DIR` spelling still
works; `…_ROOT` wins when both are set.)

```bash
export CLAUDE_ACCOUNTS_ROOT=/opt/app-robot/teamA/claude-accounts
export CODEX_ACCOUNTS_ROOT=/opt/app-robot/teamA/codex-accounts
./install.sh --no-server --instance teamA
```

An install whose roots are not the defaults is an **instance install**: its LaunchAgents
(and cron lines on Linux) are labelled `com.claude-multiacc.<instance>.*` so instances
never overwrite each other's agents, and they carry the pool roots in their environment.
It deliberately does **not** touch the shell rc PATH block, `/etc/profile.d`, or the
`/usr/local/bin` shims — one interactive PATH cannot serve two pools, and those belong to
the default install. `--instance NAME` labels it explicitly; otherwise the label is
derived from the pool root. Uninstalling an instance removes only that instance's agents.

## Pinning & env switches

| Env | Effect |
| --- | --- |
| `CLAUDE_ACCOUNT=acct-03` | pin this invocation to one account (wins over markers) |
| `CLAUDE_CONFIG_DIR=...` | shim passes straight through (scripts can pin the old way) |
| `CLAUDE_MULTIACC_DISABLE=1` | bypass selection entirely |
| `CLAUDE_MULTIACC_HEADROOM_BAND=0..100` | weekly points treated as peers; default `30`, `0` is strict |
| `CLAUDE_MULTIACC_SESSION_GATE=0..100` | max 5h-session usage that still gets ranked; default `50`, `100` disables the gate |
| `CLAUDE_MULTIACC_CLIENT_LIMITS=0` | ignore the client's own rate-limit records |
| `CLAUDE_MULTIACC_CLIENT_SCAN_TTL=<s>` | clean client-limit scan cache; default 20s |
| `CLAUDE_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY=<s>` | five-hour recovery grace; default 300s (weekly never clears early) |
| `CLAUDE_SHIM_RETRY=0` | disable the `-p` auto-retry |
| `CLAUDE_MULTIACC_AUTORESUME=0` | no [auto-resume](AUTORESUME.md) watcher for launches from this environment. `touch <pool>/autoresume.off` also stops running watchers |
| `CLAUDE_ACCOUNTS_ROOT=...` | relocate the pool; legacy spelling is `CLAUDE_ACCOUNTS_DIR` |
| `CLAUDE_MULTIACC_SYNC_TARGET=...` | sync target, overriding the manifest; `none` = local-only |
| `CLAUDE_MULTIACC_SYNC_ROOT=... / _SYNC_REPO=...` | remote pool root / addon repo that goes with it |

The codex shim honors the same switches spelled `CODEX_*`: `CODEX_ACCOUNT`,
`CODEX_HOME` (passthrough), `CODEX_MULTIACC_DISABLE`, `CODEX_SHIM_RETRY`,
`CODEX_MULTIACC_AUTORESUME`, `CODEX_SHIM_SELECT`, `CODEX_MULTIACC_HEADROOM_BAND`, `CODEX_MULTIACC_SESSION_GATE`,
`CODEX_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY` (5h client-marker recovery grace, default
300s — weekly markers never clear early), `CODEX_ACCOUNTS_ROOT` (legacy
`CODEX_ACCOUNTS_DIR`), `CODEX_MULTIACC_SYNC_TARGET`, `CODEX_MULTIACC_THRESHOLD`.

## Verification and troubleshooting

See [verification commands and troubleshooting](VERIFICATION.md), including the
packed npm command checks, account health, shell PATH setup, and security notes.
