# claude-multiacc — Multi-Account Claude Code Addon (random account + server sync)

**Goal:** every `claude` / `claude -p` invocation runs under a randomly picked account
(~10 Claude **subscription** accounts — no API keys, ever). Account set mirrors
automatically to the deploy server `root@138.197.36.107` on every add/remove.
`claude -p` must work in 100% of cases on both machines.

**Packaging requirement (added 2026-07-13):** ship as an **installable addon from this
git repo** for macOS + Linux that **never touches the original Claude Code app**, so the
app keeps working and updating exactly as-is.

**Limit-aware selection requirement (added 2026-07-13):** the shim must be smart about
usage limits — pick only accounts that still have limit headroom; when ANY limit bucket
of an account reaches **≥90% utilization**, the account gets marked and excluded from the
random pool until that bucket resets. **Fable model limits must be tracked as their own
bucket** (not just the 5-hour session limit): a Fable weekly bucket at ≥90% marks the
account even if session limit is fine. Subscription accounts only — limits are read via
each account's own OAuth usage endpoint, never via API keys.

**Status:** IMPLEMENTED & VERIFIED 2026-07-13 — all phases complete on both machines.
See README.md (tested reality). Deltas from plan, decided during implementation:
- **Account discovery result:** exactly 2 unique accounts exist across the fleet —
  support@gowalkae.com (Mac default login) and hasan@gowalkqa.com (server default
  login, found in /root/.claude). No dead credentials found anywhere.
- **`adopt` mechanism (new):** an existing machine login joins the pool as a dir
  symlink to ~/.claude — single credential file, zero grant-fork/refresh-race risk.
  Used for acct-01 (Mac) and acct-02 (server).
- **Asymmetric pools:** each machine's pool = accounts valid on THAT machine. Adding a
  fresh account (`claude-accounts add`) now shows a sign-in link and captures a
  subscription setup-token that works on BOTH machines, so new accounts are symmetric;
  the two adopted accounts stay home-bound by design.
- **Auto-retry decision:** ON by default for -p, gated to finite stdin (tty/regular
  file//dev/null) so service-spawned pipes can never hang; CLAUDE_SHIM_RETRY=0 disables.
- **Server call sites fixed:** CLAUDE_BIN=/usr/local/bin/claude in
  vpn-server-management, inapp-onboarding, vpn-seo (.env backups kept), services
  restarted healthy. /root/poll.sh pgrep kept working (shim execs the symlink path).
- Adversarially reviewed (11-agent workflow): fail-open on unset HOME, sync
  mass-delete guards, atomic manifest writes, cooldown persistence all fixed + tested.

Original plan below. Verified facts from planning:
- This Mac stores Claude Code credentials per-config-dir in files (`.credentials.json`),
  NOT in macOS Keychain (tested 2026-07-13, Claude Code 2.1.207) → per-dir logins coexist.
  **Superseded 2026-08-28:** that observation was an artefact of testing over ssh. Claude
  Code (verified on 2.1.250) writes each config dir's login to the login Keychain
  (`Claude Code-credentials-<sha256(dir)[:8]>`) from any session that can open it and
  deletes the file; ssh/background sessions fall back to the file. The pool now reads
  both (`lib/keychain.py`); per-dir logins still coexist (one Keychain item per dir).
- `CLAUDE_CONFIG_DIR` isolation confirmed working on this Mac (isolated dir = "Not logged in").
- Real binary: `~/.local/bin/claude`. Interactive `claude` is a zsh alias adding
  `--dangerously-skip-permissions` (kept; it composes with the shim via PATH).
- Current login: support@gowalkae.com (Claude Max) → becomes `acct-01`.

---

## Addon requirement — analysis & design decisions

**Feasibility:** clean. The whole mechanism uses only documented, version-stable
interfaces — PATH resolution, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_OAUTH_TOKEN` — so the
addon needs zero modifications to the app and is agnostic to Claude Code versions.

**Non-invasive guarantees (hard rules):**
1. Never writes to the Claude Code binary, its install dir, npm/native update machinery,
   or the contents of `~/.claude` app files. (Exception: the shared-history design
   symlinks *into* `~/.claude/projects` — user data, not app files.)
2. Shim resolves the real binary **dynamically at exec time** (next `claude` on PATH
   after itself; fallback to known locations `~/.local/bin/claude`,
   `/usr/local/bin/claude.real-candidates`) → survives `claude update`, reinstalls, and
   install-location changes. No hardcoded paths baked at install time.
3. Everything the addon creates lives in exactly three places, all reversible:
   - the cloned repo itself (code)
   - `~/.claude-accounts/` (account data: manifest, per-acct config dirs, tokens, logs)
   - one marked PATH block in shell rc (`~/.zshenv` on macOS, `~/.bashrc` +
     `/etc/profile.d/claude-multiacc.sh` on Linux) and, on Linux servers,
     a shim at `/usr/local/bin/claude` (shadows via PATH order; original untouched)
4. `install.sh --uninstall` removes all of the above (accounts dir only with
   `--purge-data`), restoring stock behavior byte-for-byte.

**Install / update UX:**
```
git clone <repo> && cd claude-multiacc && ./install.sh     # macOS or Linux, idempotent
git pull && ./install.sh                                    # addon update (data untouched)
./install.sh --uninstall [--purge-data]                     # full removal
```

**Portability:** shim + CLI written for bash 3.2 (macOS stock) and bash 4+ (Linux);
no GNU-only flags; server mode auto-detected (root/Linux → /usr/local/bin shim,
systemd-compatible PATH placement).

## Repo layout

```
claude-multiacc/
  install.sh            idempotent installer/uninstaller, macOS + Linux
  bin/claude            the shim (PATH-shadows the real binary, never replaces it)
  bin/claude-accounts   helper CLI: list|status|add|import|remove|mint|sync|verify|limits
  lib/common.sh         shared functions (account discovery, manifest, rsync filters)
  CLAUDE_ACCS_TASK.md   this plan
  README.md             REQUIRED DELIVERABLE — written when implementation completes
```

## Architecture

```
MAC (source of truth)                      SERVER 138.197.36.107 (mirror)
~/.claude-accounts/                        /root/.claude-accounts/
  accounts.json  <- manifest                 accounts.json          (synced)
  acct-01/ .. acct-NN/                       acct-NN/server.token   (synced, 600)
    .credentials.json  (per-dir /login)      acct-NN/               (= CLAUDE_CONFIG_DIR, own history)
    .claude.json, settings.json (seeded)   /usr/local/bin/claude -> repo shim
    server.token       (setup-token)       /root/claude-multiacc/  (addon repo, git pull to update)
    projects/ -> ~/.claude/projects
  selection.log, sync.log
```

- **Mac auth:** normal `/login` per config dir (subscription OAuth; files coexist per-dir,
  auto-refresh). NO API keys anywhere.
- **Server auth:** per-account long-lived token from `claude setup-token` (subscription
  feature, headless-safe, no refresh rotation → no token races between machines;
  inference-only — fine, server only runs `claude -p`). We deliberately do NOT copy
  `.credentials.json` to the server: two machines refreshing one OAuth refresh token can
  invalidate each other → violates the 100% rule.
- **Sync:** one-way Mac → server (Mac = source of truth): rsync `accounts.json` +
  `acct-*/server.token` with delete-propagation, fired automatically at the end of
  `claude-accounts add|remove` (and runnable manually via `claude-accounts sync`).
  Addon code on the server updates via `git pull`, not via sync.

## Shim selection logic (identical file on both machines)

1. `CLAUDE_CONFIG_DIR` already set → pass straight through (scripts can pin).
2. `CLAUDE_ACCOUNT=acct-03` set → use that account (even if limit-marked — explicit wins).
3. Else, among accounts with valid auth (`.credentials.json` on Mac; `server.token` on
   server → also exports `CLAUDE_CODE_OAUTH_TOKEN`) **that are not limit-marked** (no
   `.limited` marker), select in two cuts (operator decision, 2026-09-03): first the
   **session gate** — only accounts whose fresh 5h usage is at most
   `CLAUDE_MULTIACC_SESSION_GATE` (default 50) stay in, unless nobody clears it; then the
   **30-point weekly headroom band** — accounts within `CLAUDE_MULTIACC_HEADROOM_BAND`
   points of the lowest weekly usage are peers, sampled at random with the previous pick
   avoided. If EVERY account is marked, the still-serving ones go through the same two cuts
   with a strict (exact-tie) band and one is handed out with a warning — degraded service
   beats a hard failure (the 100% rule). README.md is the authoritative description.
4. Append `timestamp account cwd` to `selection.log` (never prompt text); print NOTHING;
   `exec` the dynamically-resolved real binary — stdin/stdout/exit codes pass through
   byte-identically → `-p` pipe-safe.

## Limit-aware selection (smart marking)

- `claude-accounts limits` refreshes per-account usage by calling the account's OAuth
  usage endpoint with that account's own subscription token (the same data `/usage`
  shows: 5-hour session bucket, weekly bucket, and per-model-tier weekly buckets —
  **Fable tracked as its own bucket**). Results cached in `acct-NN/limits.json`
  (utilization % + reset time per bucket).
- Marking rule: any bucket ≥ **90%** → create `acct-NN/.limited` (contains reason bucket
  + reset timestamp). Marker auto-clears when the bucket's reset time passes or a refresh
  shows utilization back under threshold.
- Refresh cadence: cron/launchd every **1 min** on both machines (each machine queries
  independently — usage is per-account global, so both see the same numbers), plus
  opportunistic refresh when the shim notices a stale (>3 min) limits.json.
- The usage endpoint is the one Claude Code itself uses for `/usage` (undocumented,
  subscription-OAuth only — no API keys involved). It must be captured and validated
  during implementation; if it ever breaks, the shim degrades gracefully: no fresh limit
  data → treat account as available, rely on error-retry instead (never block work on
  missing telemetry).

## Phases

### Phase 0 — Audit call sites (Mac + server)
- Mac: grep all projects for `claude` invocations (shell scripts, package.json, Makefiles,
  cron, launchd plists). Fix absolute-path calls and bare-`sh` minimal-PATH contexts to
  resolve the shim. IDE/desktop integrations spawn the binary directly → list them
  (they'll use the default account).
- Server: same audit over /root, systemd units, crontabs, deploy scripts. Check whether
  Claude Code is installed; if not, install it (official installer — the addon never
  bundles or patches the app).

### Phase 1 — Build the addon (this repo)
Shim, `claude-accounts` CLI, `install.sh` (+ uninstall), lib. Bash 3.2-compatible.
Commit to the repo; this repo IS the distribution artifact.

### Phase 2 — Install on Mac + discover existing accounts
- `./install.sh` → PATH block, `~/.claude-accounts/` skeleton, acct-01 = existing login.
- **Local account discovery:** search for every credential this Mac has ever held —
  `~/.claude/backups/`, `~/.claude.json*` (oauthAccount history), any `.credentials.json`
  on disk, stray `CLAUDE_CONFIG_DIR` dirs. Dedupe by account email keeping the **latest**
  auth per unique account; `claude-accounts import` each; validate each with
  `claude auth status` + minimal `-p "reply OK"`. Report accounts found-but-dead
  (need re-login) separately.
- Seed each acct dir: copy current `.claude.json` (strip `oauthAccount`) + `settings.json`
  → preserves project trust/onboarding so headless mode never prompts; symlink
  `projects/` → `~/.claude/projects` (shared history/memory; `--continue`/`--resume`
  work regardless of picked account).

### Phase 3 — Auth ceremony (user, ~3-4 min per account, one-time)
Per account, in ONE incognito browser window signed into that claude.ai account:
1. `CLAUDE_ACCOUNT=acct-NN claude` → `/login` (press `c` to copy URL into incognito) →
   `/status` confirms email.
2. `claude-accounts mint acct-NN` (wraps `claude setup-token`) → second OAuth click →
   token saved to `acct-NN/server.token` (600). This is the server credential.
3. CLI verifies and records the email in `accounts.json`.

### Phase 4 — Server bootstrap + sync
- On server: `git clone` this repo → `./install.sh` (detects Linux/root → shim at
  `/usr/local/bin/claude`, which precedes the real binary in login-shell AND systemd
  default PATH; original binary untouched).
- Mac: `claude-accounts sync` pushes manifest + tokens; add/remove auto-sync
  (removals propagate). All syncs log to `sync.log`; non-zero exit = loud failure.
- Post-sync hook re-runs the server verification matrix automatically.

### Phase 5 — Verification (the 100% requirement)
- Mac matrix: per acct dir → `claude auth status` + `claude -p "reply OK"` all pass.
- Server matrix: per token → `claude -p "reply OK"` all pass.
- Shim spread: ~20 invocations per machine → all accounts hit, zero failures.
- Real-world: run one actual project `-p` command through the shim on each machine.
- Non-invasiveness check: `claude update` / version check still works; uninstall on a
  scratch account restores stock behavior.

### Phase 6 — Ops + documentation
- `claude-accounts status`: per-account email, Mac auth health, server token presence/age,
  last-picked time.
- Weekly health check (launchd on Mac, cron on server): full auth matrix; alert on any
  failure so a dead account can't silently eat ~10% of `-p` calls.
- Limits refresh every 1 min (launchd/cron) → `.limited` markers per the 90% rule;
  `claude-accounts status` shows per-bucket utilization incl. the Fable bucket.
- OAuth logins auto-refresh on use; `setup-token` tokens live ~1 year → health check
  warns ahead; renewal = re-run `claude-accounts mint` for that account.
- **README.md (required, written only once everything above is implemented AND tested):**
  what it is, install (macOS/Linux), auth ceremony walkthrough, adding/removing accounts,
  pinning (`CLAUDE_ACCOUNT`), server sync setup, verification commands, uninstall,
  troubleshooting. README must reflect tested reality, not the plan.

## Open decisions
- [ ] **Auto-retry** (recommended): for `-p` invocations only, on auth/rate-limit error
      patterns, retry once with a different random account; legit non-zero exits pass
      through. Default on/off TBD (`CLAUDE_SHIM_RETRY`).
- [ ] Final account count/names (user adds ~10 subscription accounts).
- [ ] Server: does anything call `claude` on it today? (Phase 0 answers.)

## Risks
- Rotating ~10 subscriptions to spread usage is a pattern Anthropic anti-abuse may flag;
  accounts can be banned for limit circumvention. Raised with user 2026-07-13.
- Tokens on server are plain files under /root (600/700) — server compromise = account access.
- `setup-token` tokens are inference-only (no Remote Control) — acceptable for `-p` usage.
- macOS installs that store creds in Keychain (not this Mac) would break per-dir `/login`
  isolation; the addon must detect this at install time and warn (token-based fallback).
  (2026-08-28: wrong on both counts — the Keychain item is per config dir, so isolation
  holds, and every Mac does this from a GUI session; the addon reads the Keychain now.)
- The usage/limits endpoint is internal (what `/usage` consumes) and may change without
  notice; limit tracking must fail OPEN (account treated available, retry logic as backstop)
  so limits telemetry can never break the 100% `-p` guarantee.
