claude-multiacc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE_ACCS_TASK.md +236 -0
- package/README.md +263 -0
- package/bin/claude +359 -0
- package/bin/claude-accounts +1127 -0
- package/bin/cli.mjs +112 -0
- package/install.sh +277 -0
- package/lib/common.sh +200 -0
- package/package.json +55 -0
- package/scripts/postinstall.mjs +32 -0
- package/tests/run-tests.sh +643 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# claude-multiacc — Multi-Account Claude Code Addon (random account + server sync)
|
|
2
|
+
|
|
3
|
+
**Goal:** every `claude` / `claude -p` invocation runs under a randomly picked account
|
|
4
|
+
(~10 Claude **subscription** accounts — no API keys, ever). Account set mirrors
|
|
5
|
+
automatically to the deploy server `root@138.197.36.107` on every add/remove.
|
|
6
|
+
`claude -p` must work in 100% of cases on both machines.
|
|
7
|
+
|
|
8
|
+
**Packaging requirement (added 2026-07-13):** ship as an **installable addon from this
|
|
9
|
+
git repo** for macOS + Linux that **never touches the original Claude Code app**, so the
|
|
10
|
+
app keeps working and updating exactly as-is.
|
|
11
|
+
|
|
12
|
+
**Limit-aware selection requirement (added 2026-07-13):** the shim must be smart about
|
|
13
|
+
usage limits — pick only accounts that still have limit headroom; when ANY limit bucket
|
|
14
|
+
of an account reaches **≥90% utilization**, the account gets marked and excluded from the
|
|
15
|
+
random pool until that bucket resets. **Fable model limits must be tracked as their own
|
|
16
|
+
bucket** (not just the 5-hour session limit): a Fable weekly bucket at ≥90% marks the
|
|
17
|
+
account even if session limit is fine. Subscription accounts only — limits are read via
|
|
18
|
+
each account's own OAuth usage endpoint, never via API keys.
|
|
19
|
+
|
|
20
|
+
**Status:** IMPLEMENTED & VERIFIED 2026-07-13 — all phases complete on both machines.
|
|
21
|
+
See README.md (tested reality). Deltas from plan, decided during implementation:
|
|
22
|
+
- **Account discovery result:** exactly 2 unique accounts exist across the fleet —
|
|
23
|
+
support@gowalkae.com (Mac default login) and hasan@gowalkqa.com (server default
|
|
24
|
+
login, found in /root/.claude). No dead credentials found anywhere.
|
|
25
|
+
- **`adopt` mechanism (new):** an existing machine login joins the pool as a dir
|
|
26
|
+
symlink to ~/.claude — single credential file, zero grant-fork/refresh-race risk.
|
|
27
|
+
Used for acct-01 (Mac) and acct-02 (server).
|
|
28
|
+
- **Asymmetric pools:** each machine's pool = accounts valid on THAT machine. Adding a
|
|
29
|
+
fresh account (`claude-accounts add`) now shows a sign-in link and captures a
|
|
30
|
+
subscription setup-token that works on BOTH machines, so new accounts are symmetric;
|
|
31
|
+
the two adopted accounts stay home-bound by design.
|
|
32
|
+
- **Auto-retry decision:** ON by default for -p, gated to finite stdin (tty/regular
|
|
33
|
+
file//dev/null) so service-spawned pipes can never hang; CLAUDE_SHIM_RETRY=0 disables.
|
|
34
|
+
- **Server call sites fixed:** CLAUDE_BIN=/usr/local/bin/claude in
|
|
35
|
+
vpn-server-management, inapp-onboarding, vpn-seo (.env backups kept), services
|
|
36
|
+
restarted healthy. /root/poll.sh pgrep kept working (shim execs the symlink path).
|
|
37
|
+
- Adversarially reviewed (11-agent workflow): fail-open on unset HOME, sync
|
|
38
|
+
mass-delete guards, atomic manifest writes, cooldown persistence all fixed + tested.
|
|
39
|
+
|
|
40
|
+
Original plan below. Verified facts from planning:
|
|
41
|
+
- This Mac stores Claude Code credentials per-config-dir in files (`.credentials.json`),
|
|
42
|
+
NOT in macOS Keychain (tested 2026-07-13, Claude Code 2.1.207) → per-dir logins coexist.
|
|
43
|
+
- `CLAUDE_CONFIG_DIR` isolation confirmed working on this Mac (isolated dir = "Not logged in").
|
|
44
|
+
- Real binary: `~/.local/bin/claude`. Interactive `claude` is a zsh alias adding
|
|
45
|
+
`--dangerously-skip-permissions` (kept; it composes with the shim via PATH).
|
|
46
|
+
- Current login: support@gowalkae.com (Claude Max) → becomes `acct-01`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Addon requirement — analysis & design decisions
|
|
51
|
+
|
|
52
|
+
**Feasibility:** clean. The whole mechanism uses only documented, version-stable
|
|
53
|
+
interfaces — PATH resolution, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_OAUTH_TOKEN` — so the
|
|
54
|
+
addon needs zero modifications to the app and is agnostic to Claude Code versions.
|
|
55
|
+
|
|
56
|
+
**Non-invasive guarantees (hard rules):**
|
|
57
|
+
1. Never writes to the Claude Code binary, its install dir, npm/native update machinery,
|
|
58
|
+
or the contents of `~/.claude` app files. (Exception: the shared-history design
|
|
59
|
+
symlinks *into* `~/.claude/projects` — user data, not app files.)
|
|
60
|
+
2. Shim resolves the real binary **dynamically at exec time** (next `claude` on PATH
|
|
61
|
+
after itself; fallback to known locations `~/.local/bin/claude`,
|
|
62
|
+
`/usr/local/bin/claude.real-candidates`) → survives `claude update`, reinstalls, and
|
|
63
|
+
install-location changes. No hardcoded paths baked at install time.
|
|
64
|
+
3. Everything the addon creates lives in exactly three places, all reversible:
|
|
65
|
+
- the cloned repo itself (code)
|
|
66
|
+
- `~/.claude-accounts/` (account data: manifest, per-acct config dirs, tokens, logs)
|
|
67
|
+
- one marked PATH block in shell rc (`~/.zshenv` on macOS, `~/.bashrc` +
|
|
68
|
+
`/etc/profile.d/claude-multiacc.sh` on Linux) and, on Linux servers,
|
|
69
|
+
a shim at `/usr/local/bin/claude` (shadows via PATH order; original untouched)
|
|
70
|
+
4. `install.sh --uninstall` removes all of the above (accounts dir only with
|
|
71
|
+
`--purge-data`), restoring stock behavior byte-for-byte.
|
|
72
|
+
|
|
73
|
+
**Install / update UX:**
|
|
74
|
+
```
|
|
75
|
+
git clone <repo> && cd claude-multiacc && ./install.sh # macOS or Linux, idempotent
|
|
76
|
+
git pull && ./install.sh # addon update (data untouched)
|
|
77
|
+
./install.sh --uninstall [--purge-data] # full removal
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Portability:** shim + CLI written for bash 3.2 (macOS stock) and bash 4+ (Linux);
|
|
81
|
+
no GNU-only flags; server mode auto-detected (root/Linux → /usr/local/bin shim,
|
|
82
|
+
systemd-compatible PATH placement).
|
|
83
|
+
|
|
84
|
+
## Repo layout
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
claude-multiacc/
|
|
88
|
+
install.sh idempotent installer/uninstaller, macOS + Linux
|
|
89
|
+
bin/claude the shim (PATH-shadows the real binary, never replaces it)
|
|
90
|
+
bin/claude-accounts helper CLI: list|status|add|import|remove|mint|sync|verify|limits
|
|
91
|
+
lib/common.sh shared functions (account discovery, manifest, rsync filters)
|
|
92
|
+
CLAUDE_ACCS_TASK.md this plan
|
|
93
|
+
README.md REQUIRED DELIVERABLE — written when implementation completes
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Architecture
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
MAC (source of truth) SERVER 138.197.36.107 (mirror)
|
|
100
|
+
~/.claude-accounts/ /root/.claude-accounts/
|
|
101
|
+
accounts.json <- manifest accounts.json (synced)
|
|
102
|
+
acct-01/ .. acct-NN/ acct-NN/server.token (synced, 600)
|
|
103
|
+
.credentials.json (per-dir /login) acct-NN/ (= CLAUDE_CONFIG_DIR, own history)
|
|
104
|
+
.claude.json, settings.json (seeded) /usr/local/bin/claude -> repo shim
|
|
105
|
+
server.token (setup-token) /root/claude-multiacc/ (addon repo, git pull to update)
|
|
106
|
+
projects/ -> ~/.claude/projects
|
|
107
|
+
selection.log, sync.log
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **Mac auth:** normal `/login` per config dir (subscription OAuth; files coexist per-dir,
|
|
111
|
+
auto-refresh). NO API keys anywhere.
|
|
112
|
+
- **Server auth:** per-account long-lived token from `claude setup-token` (subscription
|
|
113
|
+
feature, headless-safe, no refresh rotation → no token races between machines;
|
|
114
|
+
inference-only — fine, server only runs `claude -p`). We deliberately do NOT copy
|
|
115
|
+
`.credentials.json` to the server: two machines refreshing one OAuth refresh token can
|
|
116
|
+
invalidate each other → violates the 100% rule.
|
|
117
|
+
- **Sync:** one-way Mac → server (Mac = source of truth): rsync `accounts.json` +
|
|
118
|
+
`acct-*/server.token` with delete-propagation, fired automatically at the end of
|
|
119
|
+
`claude-accounts add|remove` (and runnable manually via `claude-accounts sync`).
|
|
120
|
+
Addon code on the server updates via `git pull`, not via sync.
|
|
121
|
+
|
|
122
|
+
## Shim selection logic (identical file on both machines)
|
|
123
|
+
|
|
124
|
+
1. `CLAUDE_CONFIG_DIR` already set → pass straight through (scripts can pin).
|
|
125
|
+
2. `CLAUDE_ACCOUNT=acct-03` set → use that account (even if limit-marked — explicit wins).
|
|
126
|
+
3. Else pick uniformly at random among accounts with valid auth
|
|
127
|
+
(`.credentials.json` on Mac; `server.token` on server → also exports
|
|
128
|
+
`CLAUDE_CODE_OAUTH_TOKEN`) **that are not limit-marked** (no `.limited` marker).
|
|
129
|
+
If EVERY account is marked, fall back to the least-utilized account and log a warning —
|
|
130
|
+
degraded service beats a hard failure (the 100% rule).
|
|
131
|
+
4. Append `timestamp account cwd` to `selection.log` (never prompt text); print NOTHING;
|
|
132
|
+
`exec` the dynamically-resolved real binary — stdin/stdout/exit codes pass through
|
|
133
|
+
byte-identically → `-p` pipe-safe.
|
|
134
|
+
|
|
135
|
+
## Limit-aware selection (smart marking)
|
|
136
|
+
|
|
137
|
+
- `claude-accounts limits` refreshes per-account usage by calling the account's OAuth
|
|
138
|
+
usage endpoint with that account's own subscription token (the same data `/usage`
|
|
139
|
+
shows: 5-hour session bucket, weekly bucket, and per-model-tier weekly buckets —
|
|
140
|
+
**Fable tracked as its own bucket**). Results cached in `acct-NN/limits.json`
|
|
141
|
+
(utilization % + reset time per bucket).
|
|
142
|
+
- Marking rule: any bucket ≥ **90%** → create `acct-NN/.limited` (contains reason bucket
|
|
143
|
+
+ reset timestamp). Marker auto-clears when the bucket's reset time passes or a refresh
|
|
144
|
+
shows utilization back under threshold.
|
|
145
|
+
- Refresh cadence: cron/launchd every **1 min** on both machines (each machine queries
|
|
146
|
+
independently — usage is per-account global, so both see the same numbers), plus
|
|
147
|
+
opportunistic refresh when the shim notices a stale (>3 min) limits.json.
|
|
148
|
+
- The usage endpoint is the one Claude Code itself uses for `/usage` (undocumented,
|
|
149
|
+
subscription-OAuth only — no API keys involved). It must be captured and validated
|
|
150
|
+
during implementation; if it ever breaks, the shim degrades gracefully: no fresh limit
|
|
151
|
+
data → treat account as available, rely on error-retry instead (never block work on
|
|
152
|
+
missing telemetry).
|
|
153
|
+
|
|
154
|
+
## Phases
|
|
155
|
+
|
|
156
|
+
### Phase 0 — Audit call sites (Mac + server)
|
|
157
|
+
- Mac: grep all projects for `claude` invocations (shell scripts, package.json, Makefiles,
|
|
158
|
+
cron, launchd plists). Fix absolute-path calls and bare-`sh` minimal-PATH contexts to
|
|
159
|
+
resolve the shim. IDE/desktop integrations spawn the binary directly → list them
|
|
160
|
+
(they'll use the default account).
|
|
161
|
+
- Server: same audit over /root, systemd units, crontabs, deploy scripts. Check whether
|
|
162
|
+
Claude Code is installed; if not, install it (official installer — the addon never
|
|
163
|
+
bundles or patches the app).
|
|
164
|
+
|
|
165
|
+
### Phase 1 — Build the addon (this repo)
|
|
166
|
+
Shim, `claude-accounts` CLI, `install.sh` (+ uninstall), lib. Bash 3.2-compatible.
|
|
167
|
+
Commit to the repo; this repo IS the distribution artifact.
|
|
168
|
+
|
|
169
|
+
### Phase 2 — Install on Mac + discover existing accounts
|
|
170
|
+
- `./install.sh` → PATH block, `~/.claude-accounts/` skeleton, acct-01 = existing login.
|
|
171
|
+
- **Local account discovery:** search for every credential this Mac has ever held —
|
|
172
|
+
`~/.claude/backups/`, `~/.claude.json*` (oauthAccount history), any `.credentials.json`
|
|
173
|
+
on disk, stray `CLAUDE_CONFIG_DIR` dirs. Dedupe by account email keeping the **latest**
|
|
174
|
+
auth per unique account; `claude-accounts import` each; validate each with
|
|
175
|
+
`claude auth status` + minimal `-p "reply OK"`. Report accounts found-but-dead
|
|
176
|
+
(need re-login) separately.
|
|
177
|
+
- Seed each acct dir: copy current `.claude.json` (strip `oauthAccount`) + `settings.json`
|
|
178
|
+
→ preserves project trust/onboarding so headless mode never prompts; symlink
|
|
179
|
+
`projects/` → `~/.claude/projects` (shared history/memory; `--continue`/`--resume`
|
|
180
|
+
work regardless of picked account).
|
|
181
|
+
|
|
182
|
+
### Phase 3 — Auth ceremony (user, ~3-4 min per account, one-time)
|
|
183
|
+
Per account, in ONE incognito browser window signed into that claude.ai account:
|
|
184
|
+
1. `CLAUDE_ACCOUNT=acct-NN claude` → `/login` (press `c` to copy URL into incognito) →
|
|
185
|
+
`/status` confirms email.
|
|
186
|
+
2. `claude-accounts mint acct-NN` (wraps `claude setup-token`) → second OAuth click →
|
|
187
|
+
token saved to `acct-NN/server.token` (600). This is the server credential.
|
|
188
|
+
3. CLI verifies and records the email in `accounts.json`.
|
|
189
|
+
|
|
190
|
+
### Phase 4 — Server bootstrap + sync
|
|
191
|
+
- On server: `git clone` this repo → `./install.sh` (detects Linux/root → shim at
|
|
192
|
+
`/usr/local/bin/claude`, which precedes the real binary in login-shell AND systemd
|
|
193
|
+
default PATH; original binary untouched).
|
|
194
|
+
- Mac: `claude-accounts sync` pushes manifest + tokens; add/remove auto-sync
|
|
195
|
+
(removals propagate). All syncs log to `sync.log`; non-zero exit = loud failure.
|
|
196
|
+
- Post-sync hook re-runs the server verification matrix automatically.
|
|
197
|
+
|
|
198
|
+
### Phase 5 — Verification (the 100% requirement)
|
|
199
|
+
- Mac matrix: per acct dir → `claude auth status` + `claude -p "reply OK"` all pass.
|
|
200
|
+
- Server matrix: per token → `claude -p "reply OK"` all pass.
|
|
201
|
+
- Shim spread: ~20 invocations per machine → all accounts hit, zero failures.
|
|
202
|
+
- Real-world: run one actual project `-p` command through the shim on each machine.
|
|
203
|
+
- Non-invasiveness check: `claude update` / version check still works; uninstall on a
|
|
204
|
+
scratch account restores stock behavior.
|
|
205
|
+
|
|
206
|
+
### Phase 6 — Ops + documentation
|
|
207
|
+
- `claude-accounts status`: per-account email, Mac auth health, server token presence/age,
|
|
208
|
+
last-picked time.
|
|
209
|
+
- Weekly health check (launchd on Mac, cron on server): full auth matrix; alert on any
|
|
210
|
+
failure so a dead account can't silently eat ~10% of `-p` calls.
|
|
211
|
+
- Limits refresh every 1 min (launchd/cron) → `.limited` markers per the 90% rule;
|
|
212
|
+
`claude-accounts status` shows per-bucket utilization incl. the Fable bucket.
|
|
213
|
+
- OAuth logins auto-refresh on use; `setup-token` tokens live ~1 year → health check
|
|
214
|
+
warns ahead; renewal = re-run `claude-accounts mint` for that account.
|
|
215
|
+
- **README.md (required, written only once everything above is implemented AND tested):**
|
|
216
|
+
what it is, install (macOS/Linux), auth ceremony walkthrough, adding/removing accounts,
|
|
217
|
+
pinning (`CLAUDE_ACCOUNT`), server sync setup, verification commands, uninstall,
|
|
218
|
+
troubleshooting. README must reflect tested reality, not the plan.
|
|
219
|
+
|
|
220
|
+
## Open decisions
|
|
221
|
+
- [ ] **Auto-retry** (recommended): for `-p` invocations only, on auth/rate-limit error
|
|
222
|
+
patterns, retry once with a different random account; legit non-zero exits pass
|
|
223
|
+
through. Default on/off TBD (`CLAUDE_SHIM_RETRY`).
|
|
224
|
+
- [ ] Final account count/names (user adds ~10 subscription accounts).
|
|
225
|
+
- [ ] Server: does anything call `claude` on it today? (Phase 0 answers.)
|
|
226
|
+
|
|
227
|
+
## Risks
|
|
228
|
+
- Rotating ~10 subscriptions to spread usage is a pattern Anthropic anti-abuse may flag;
|
|
229
|
+
accounts can be banned for limit circumvention. Raised with user 2026-07-13.
|
|
230
|
+
- Tokens on server are plain files under /root (600/700) — server compromise = account access.
|
|
231
|
+
- `setup-token` tokens are inference-only (no Remote Control) — acceptable for `-p` usage.
|
|
232
|
+
- macOS installs that store creds in Keychain (not this Mac) would break per-dir `/login`
|
|
233
|
+
isolation; the addon must detect this at install time and warn (token-based fallback).
|
|
234
|
+
- The usage/limits endpoint is internal (what `/usage` consumes) and may change without
|
|
235
|
+
notice; limit tracking must fail OPEN (account treated available, retry logic as backstop)
|
|
236
|
+
so limits telemetry can never break the 100% `-p` guarantee.
|
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# claude-multiacc
|
|
2
|
+
|
|
3
|
+
Multi-account addon for Claude Code: every `claude` / `claude -p` invocation runs under a
|
|
4
|
+
randomly picked **subscription** account with limit headroom (no API keys, ever). The
|
|
5
|
+
account set mirrors automatically from the Mac (source of truth) to the deploy server.
|
|
6
|
+
|
|
7
|
+
Installs as a PATH shim + helper CLI from this repo. **Never touches the Claude Code app**:
|
|
8
|
+
no binary edits, no install-dir writes, survives `claude update` — the real binary is
|
|
9
|
+
resolved dynamically at exec time.
|
|
10
|
+
|
|
11
|
+
Tested on: macOS (bash 3.2, zsh, Claude Code 2.1.207) and Ubuntu 24.04
|
|
12
|
+
(bash 5.2, Claude Code 2.1.201). Test suite: `tests/run-tests.sh` (35 sandboxed tests,
|
|
13
|
+
no network / no quota — passes on both platforms).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## How it works
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
MAC (source of truth) SERVER (mirror)
|
|
21
|
+
~/.claude-accounts/ /root/.claude-accounts/
|
|
22
|
+
accounts.json <- manifest accounts.json (synced)
|
|
23
|
+
acct-01 -> ~/.claude ("adopted") acct-01/server.token (synced, 600, once minted)
|
|
24
|
+
acct-02/ (registered) acct-02 -> /root/.claude ("adopted")
|
|
25
|
+
server.token / .credentials.json
|
|
26
|
+
selection.log sync.log health.log /usr/local/bin/claude -> repo shim
|
|
27
|
+
repo bin/ first on PATH (rc-file block) /root/claude-multiacc/ (addon repo)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Shim selection order** (identical file on both machines, `bin/claude`):
|
|
31
|
+
|
|
32
|
+
1. `CLAUDE_CONFIG_DIR` or `CLAUDE_CODE_OAUTH_TOKEN` already set → pass straight through.
|
|
33
|
+
2. `CLAUDE_ACCOUNT=acct-NN` → use that account (even if limit-marked — explicit wins).
|
|
34
|
+
3. Among accounts valid **on this machine** (`.credentials.json` or `server.token`
|
|
35
|
+
present) and not excluded, pick the one with the **most remaining headroom** (see
|
|
36
|
+
below). Ties break randomly to spread load. `CLAUDE_SHIM_SELECT=random` restores plain
|
|
37
|
+
uniform-random selection.
|
|
38
|
+
4. If every account is excluded → the highest-headroom valid account anyway + a warning
|
|
39
|
+
in `selection.log` (degraded beats down: the 100% rule).
|
|
40
|
+
|
|
41
|
+
**Which account has "the most headroom"?** A Max subscription has three limits that a
|
|
42
|
+
Fable request consumes at once: the 5-hour **session** bucket, the **weekly all-models**
|
|
43
|
+
bucket, and the **weekly Fable** bucket. Anthropic's docs confirm these reset on very
|
|
44
|
+
different horizons — the session bucket refills every ~5 hours, but weekly buckets only
|
|
45
|
+
refill on the account's fixed weekly reset (days away). So the picker ranks primarily on
|
|
46
|
+
**weekly headroom** (the peak of the durable buckets) and uses the self-healing session
|
|
47
|
+
bucket only as a tiebreaker. An account sitting at 85% session / 10% weekly outranks one
|
|
48
|
+
at 20% session / 70% weekly: the first only needs a few hours to recover, the second is
|
|
49
|
+
burning headroom that's gone until next week. (`score = weekly% × 1000 + session%`,
|
|
50
|
+
lowest wins.) **Exclusion** still fires on *any* bucket ≥ 90% — a full session bucket
|
|
51
|
+
really does block right now — but that marker expires when the session resets, not days
|
|
52
|
+
later.
|
|
53
|
+
|
|
54
|
+
The shim prints nothing, logs `timestamp account cwd` (never prompt text) to
|
|
55
|
+
`selection.log`, and `exec`s the real binary — stdin/stdout/exit codes pass through
|
|
56
|
+
byte-identically. If anything is missing (no manifest, no accounts, unreadable state,
|
|
57
|
+
even an unset `HOME`) it fails **open** into plain passthrough.
|
|
58
|
+
|
|
59
|
+
**Limit-aware marking.** `claude-accounts limits` (every 60s via launchd on the Mac,
|
|
60
|
+
cron on the server, plus an opportunistic non-blocking kick from the shim when data is
|
|
61
|
+
>3 min stale) reads each account's own OAuth usage endpoint — the same data `/usage`
|
|
62
|
+
shows. Every bucket the endpoint returns is tracked separately: `session`, `weekly_all`,
|
|
63
|
+
and per-model scoped buckets — **`weekly_scoped:Fable` is its own bucket**. Any bucket
|
|
64
|
+
≥ 90% ⇒ the account gets a `.limited` marker (with the bucket's reset time) and leaves the
|
|
65
|
+
random pool until that bucket resets or a refresh shows it back under threshold.
|
|
66
|
+
|
|
67
|
+
Bucket parsing is **shape-agnostic**: buckets are derived from whatever the endpoint
|
|
68
|
+
returns (`kind`, plus `scope.model` when present), so if Anthropic renames a bucket,
|
|
69
|
+
reshapes the payload, or drops the per-model Fable separation entirely, the remaining
|
|
70
|
+
buckets keep being tracked and the ≥90% rule keeps working. Unparseable entries are
|
|
71
|
+
skipped, an all-legacy payload falls back to `five_hour`/`seven_day`, and a payload the
|
|
72
|
+
code cannot read at all degrades that one account (fail open), never the run.
|
|
73
|
+
|
|
74
|
+
Telemetry failures never block work: no fresh data ⇒ account treated as available. The
|
|
75
|
+
endpoint rate-limits per account, so the refresher skips accounts fetched in the last 45s
|
|
76
|
+
and backs off exponentially (honoring `Retry-After`) on a 429 — `limits --force` overrides
|
|
77
|
+
both. If an account's OAuth access token has expired (quiet machine, nothing ran claude for
|
|
78
|
+
hours), the refresher first runs a zero-cost `claude auth status` under that account to let
|
|
79
|
+
the app refresh its own credentials, then fetches; if that fails it logs and fails open.
|
|
80
|
+
|
|
81
|
+
**Auto-retry** (`-p`/`--print` only, default on, `CLAUDE_SHIM_RETRY=0` disables): on an
|
|
82
|
+
auth/rate-limit-looking failure the shim marks the account with a 10-minute error
|
|
83
|
+
cooldown and retries once on a different account. Output is buffered so a retried call
|
|
84
|
+
never double-emits. Only engages when stdin is finite (tty / regular file / `/dev/null`)
|
|
85
|
+
and ≥2 accounts are eligible; service-spawned pipes take the plain exec path untouched.
|
|
86
|
+
|
|
87
|
+
## Install / update / uninstall
|
|
88
|
+
|
|
89
|
+
### npm (recommended)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm install -g claude-multiacc # postinstall wires up the shim, CLI, and scheduled jobs
|
|
93
|
+
# or run it once without installing globally:
|
|
94
|
+
npx claude-multiacc
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`claude-multiacc` then wraps everything:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
claude-multiacc # install or update the addon (idempotent)
|
|
101
|
+
claude-multiacc self-update # npm i -g @latest + re-install
|
|
102
|
+
claude-multiacc uninstall [--purge-data]
|
|
103
|
+
claude-multiacc list | status | add <email> | ... # passthrough to claude-accounts
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Automatic updates:** a globally-installed package re-runs `install.sh` from its own
|
|
107
|
+
postinstall, so `npm i -g claude-multiacc@latest` fully updates the shim, CLI, and jobs.
|
|
108
|
+
A weekly job (launchd on macOS, cron on Linux) runs `claude-accounts self-update` to pull
|
|
109
|
+
the latest npm release on its own; set `CLAUDE_MULTIACC_AUTOUPDATE=0` before install to
|
|
110
|
+
opt out. `update-notifier` also prints a one-line nudge when a newer version is on npm.
|
|
111
|
+
(Note: newer npm versions gate install scripts; if postinstall is blocked, just run
|
|
112
|
+
`claude-multiacc install` once.)
|
|
113
|
+
|
|
114
|
+
New versions publish to npm automatically from `main` via GitHub Actions
|
|
115
|
+
(`.github/workflows/publish.yml`, OIDC Trusted Publishing — no long-lived token after the
|
|
116
|
+
first bootstrap publish; `scripts/auto-version.mjs` bumps the patch above the last release).
|
|
117
|
+
|
|
118
|
+
### From a git checkout
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
git clone <repo> && cd claude-multiacc && ./install.sh # macOS or Linux, idempotent
|
|
122
|
+
./install.sh --server root@1.2.3.4 # override sync target
|
|
123
|
+
git pull && ./install.sh # update (data untouched)
|
|
124
|
+
./install.sh --uninstall # restores stock behavior, keeps account data
|
|
125
|
+
./install.sh --uninstall --purge-data # also deletes ~/.claude-accounts
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`self-update` auto-detects which of these you used (npm global vs git checkout).
|
|
129
|
+
|
|
130
|
+
What install does (all reversible, nothing else):
|
|
131
|
+
|
|
132
|
+
- **macOS:** marked PATH block at the END of `~/.zshenv`, `~/.zprofile`, `~/.zshrc`
|
|
133
|
+
(+ bash rc files if present) — end-of-file placement matters because those files
|
|
134
|
+
re-prepend `~/.local/bin`; launchd agents `com.claude-multiacc.limits` (60s) and
|
|
135
|
+
`.health` (weekly Mon 09:17). Warns if this Mac stores credentials in the Keychain
|
|
136
|
+
(file-based credentials are required for per-dir logins).
|
|
137
|
+
- **Linux (root):** PATH block in `~/.bashrc` + `/etc/profile.d/claude-multiacc.sh`,
|
|
138
|
+
shim symlink at `/usr/local/bin/claude` (shadows via PATH order — on the systemd
|
|
139
|
+
default PATH too; the original binary is untouched), cron entries for limits/health.
|
|
140
|
+
- Both: `~/.claude-accounts/` skeleton + `accounts.json` manifest.
|
|
141
|
+
|
|
142
|
+
## Accounts
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
claude-accounts list # pool at a glance
|
|
146
|
+
claude-accounts status # per-account auth health, per-bucket limits incl. Fable, markers, last pick
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Every account in the pool is authenticated — `add` cannot register one otherwise.
|
|
150
|
+
|
|
151
|
+
### Adopt this machine's existing login (no browser needed)
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
claude-accounts import you@example.com --id acct-01 --home mac
|
|
155
|
+
claude-accounts adopt acct-01
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`adopt` makes the account dir a **symlink to `~/.claude`** — the machine's existing
|
|
159
|
+
login joins the pool with a single credential file (no copy of the OAuth grant exists
|
|
160
|
+
anywhere else, so refreshes can never race). This is how the two current accounts run:
|
|
161
|
+
acct-01 = the Mac's login, acct-02 = the server's login (adopted server-side).
|
|
162
|
+
|
|
163
|
+
### Add a fresh account (~1 min, one browser sign-in)
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
claude-accounts add new@example.com
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
It prints a **clickable sign-in link**. Open it in a browser signed into that account,
|
|
170
|
+
approve, and paste the code back into the same prompt. The account is registered **only
|
|
171
|
+
after the sign-in is verified** — an aborted or failed sign-in leaves nothing behind, and
|
|
172
|
+
a duplicate email is refused outright (`--force` overrides). The captured token has a
|
|
173
|
+
~1-year lifetime, is inference-only, and does not rotate → it works on the Mac **and**
|
|
174
|
+
the server with no cross-machine refresh races, so the account is usable everywhere the
|
|
175
|
+
moment `add` returns.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
claude-accounts login acct-NN # re-run the same flow for an existing account
|
|
179
|
+
# (complete missing auth, or refresh an aging token)
|
|
180
|
+
claude-accounts add x@y.com --tui # alternative: interactive /login instead, storing
|
|
181
|
+
# auto-refreshing OAuth creds (that machine only)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`status` warns when a token nears end of life; re-run `login` for that account.
|
|
185
|
+
Both accounts currently in the pool are live and verified: acct-01 (support@gowalkae.com,
|
|
186
|
+
Mac) and acct-02 (hasan@gowalkqa.com, server) — each adopted from its machine's existing
|
|
187
|
+
login, so each runs on the machine that holds its credential.
|
|
188
|
+
|
|
189
|
+
### Remove
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
claude-accounts remove acct-NN # deletes locally, propagates to the server
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Server sync
|
|
196
|
+
|
|
197
|
+
Mac → server, one-way (Mac = source of truth), fired automatically by
|
|
198
|
+
`add`/`import`/`remove`/`mint` and manually via:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
claude-accounts sync
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Pushes the manifest + per-account `server.token` (0600) + one-time seeds; **never**
|
|
205
|
+
syncs `.credentials.json` in either direction (two machines refreshing one OAuth grant
|
|
206
|
+
invalidate each other). Removals propagate; an empty/corrupt manifest refuses to sync
|
|
207
|
+
(guard against wiping the server pool). After every push the server re-seeds dirs and
|
|
208
|
+
re-runs its quick verification matrix (`post-sync` hook). Everything logs to `sync.log`;
|
|
209
|
+
failures are loud and non-zero.
|
|
210
|
+
|
|
211
|
+
## Pinning & env switches
|
|
212
|
+
|
|
213
|
+
| Env | Effect |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| `CLAUDE_ACCOUNT=acct-03` | pin this invocation to one account (wins over markers) |
|
|
216
|
+
| `CLAUDE_CONFIG_DIR=...` | shim passes straight through (scripts can pin the old way) |
|
|
217
|
+
| `CLAUDE_MULTIACC_DISABLE=1` | bypass selection entirely |
|
|
218
|
+
| `CLAUDE_SHIM_RETRY=0` | disable the `-p` auto-retry |
|
|
219
|
+
| `CLAUDE_ACCOUNTS_DIR=...` | relocate the pool (used by the test suite) |
|
|
220
|
+
|
|
221
|
+
## Verification
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
tests/run-tests.sh # 35 sandboxed tests, no quota
|
|
225
|
+
claude-accounts verify # real matrix: `claude -p "reply OK"` per authed account
|
|
226
|
+
claude-accounts verify --quick# auth presence/expiry only, no inference
|
|
227
|
+
claude-accounts limits # live per-bucket usage incl. the Fable bucket
|
|
228
|
+
claude-accounts health # limits + full verify, logs to health.log, notifies on failure (Mac)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Verified end-to-end on both machines (2026-07-13): full matrix PASS, 20-invocation shim
|
|
232
|
+
spread clean, real project `-p` through the shim OK on Mac (`~/pipeline`) and server
|
|
233
|
+
(`/opt/inapp-onboarding`), uninstall→stock→reinstall roundtrip byte-identical, three
|
|
234
|
+
server systemd services (`vpn-server-management`, `inapp-onboarding`, `vpn-seo-panel`)
|
|
235
|
+
repointed via `CLAUDE_BIN=/usr/local/bin/claude` and restarted healthy.
|
|
236
|
+
|
|
237
|
+
## Troubleshooting
|
|
238
|
+
|
|
239
|
+
- **`claude` resolves to the real binary, not the shim** — open a new shell, or check
|
|
240
|
+
that the marked block is the LAST PATH manipulation in your rc file
|
|
241
|
+
(`grep -A2 'claude-multiacc >>>' ~/.zshrc`).
|
|
242
|
+
- **An account never gets picked** — `claude-accounts status`: no auth on this machine,
|
|
243
|
+
or an active `.limited` marker (shows bucket + minutes to reset).
|
|
244
|
+
- **Everything marked limited** — the shim still runs (least-utilized fallback);
|
|
245
|
+
check `selection.log` for `all-limited` lines.
|
|
246
|
+
- **Sync fails** — `tail ~/.claude-accounts/sync.log`; it's ssh/rsync to the manifest's
|
|
247
|
+
`server` (BatchMode — needs key auth).
|
|
248
|
+
- **A service bypasses the shim** — it spawns an absolute path. Point its env
|
|
249
|
+
(`CLAUDE_BIN` etc.) at `/usr/local/bin/claude`. Docker-internal `claude` installs
|
|
250
|
+
(e.g. openclaw live-test containers) are out of scope for a host shim.
|
|
251
|
+
- **Limits look stale** — launchd: `launchctl list | grep claude-multiacc`;
|
|
252
|
+
cron: `crontab -l | grep claude-multiacc`. The shim also kicks a refresh when data
|
|
253
|
+
is >3 min old.
|
|
254
|
+
|
|
255
|
+
## Security notes & known risks
|
|
256
|
+
|
|
257
|
+
- Tokens and credentials live as 0600 files under `~/.claude-accounts` (and
|
|
258
|
+
`/root/.claude-accounts` on the server). Server compromise = account access.
|
|
259
|
+
- `selection.log` records timestamps/account/cwd only — never prompt text.
|
|
260
|
+
- Rotating multiple subscriptions to spread usage may be flagged by anti-abuse systems;
|
|
261
|
+
accounts can be banned for limit circumvention. Known and accepted by the operator.
|
|
262
|
+
- The usage endpoint is the internal one `/usage` consumes; if it changes shape, limit
|
|
263
|
+
tracking fails open (accounts stay available, error-retry is the backstop).
|