claude-multiacc 2.0.27 → 2.0.29
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/README.md +11 -138
- package/bin/claude-accounts +26 -5
- package/bin/claude-multiacc +3 -0
- package/bin/cli.mjs +16 -18
- package/bin/codex-accounts +35 -5
- package/bin/multiacc-select +4 -0
- package/docs/ACCOUNT_OPERATIONS.md +3 -61
- package/docs/CODEX.md +83 -0
- package/docs/INSTALLATION.md +91 -0
- package/docs/UNIFIED_SELECTOR.md +2 -0
- package/docs/VERIFICATION.md +68 -0
- package/install.sh +9 -284
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/install-help.txt +20 -0
- package/lib/install_actions.sh +238 -0
- package/lib/install_calendar.sh +59 -0
- package/package.json +5 -2
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/packaged_command_support.py +81 -0
- package/tests/run-tests.sh +30 -0
- package/tests/test_packaged_commands.py +170 -0
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ completely independent — separate manifests, credentials, telemetry, and logs
|
|
|
16
16
|
either provider can be used, re-authenticated, or emptied without touching the other.
|
|
17
17
|
|
|
18
18
|
Tested on: macOS (bash 3.2, zsh, Claude Code 2.1.207+, Codex CLI 0.147) and Ubuntu 24.04
|
|
19
|
-
(bash 5.2). The compatibility suite
|
|
20
|
-
|
|
19
|
+
(bash 5.2). The compatibility suite exercises both providers with no network/quota
|
|
20
|
+
use; the unified selector and packed npm commands have their own contract suites.
|
|
21
21
|
|
|
22
22
|
## Unified selector for app-robot
|
|
23
23
|
|
|
@@ -366,149 +366,22 @@ and ≥2 accounts are eligible; service-spawned pipes take the plain exec path u
|
|
|
366
366
|
|
|
367
367
|
## Codex support (OpenAI Codex CLI)
|
|
368
368
|
|
|
369
|
-
|
|
370
|
-
above about the claude pool holds for the codex pool with these translations:
|
|
371
|
-
|
|
372
|
-
| Claude side | Codex side |
|
|
373
|
-
| --- | --- |
|
|
374
|
-
| `bin/claude` shim, `claude-accounts` CLI | `bin/codex` shim, `codex-accounts` CLI |
|
|
375
|
-
| pool `~/.claude-accounts` | pool `~/.codex-accounts` |
|
|
376
|
-
| `CLAUDE_CONFIG_DIR` per-account dirs | `CODEX_HOME` per-account dirs |
|
|
377
|
-
| `.credentials.json` / macOS Keychain item (OAuth, machine-local) | `auth.json` (ChatGPT OAuth, machine-local) |
|
|
378
|
-
| `claude -p` auto-retry | `codex exec` auto-retry |
|
|
379
|
-
| Anthropic OAuth usage and Fable buckets | Codex usage endpoint and per-model buckets |
|
|
380
|
-
| `CLAUDE_*` pool controls | equivalent `CODEX_*` controls |
|
|
381
|
-
|
|
382
|
-
Same selection rule (session gate first — `CODEX_MULTIACC_SESSION_GATE`, default 50 —
|
|
383
|
-
then the 30-point weekly-headroom band with random spread, ≥90% any-bucket
|
|
384
|
-
exclusion, peers rotate), same marker semantics (`.limited` cooldowns, `.expired` parks with
|
|
385
|
-
credential/policy scoping and soft expiry), same fail-open guarantees, same sync
|
|
386
|
-
safety guards. The `codex` shim engages the buffered auto-retry only for
|
|
387
|
-
`codex exec` runs with finite stdin, exactly like `-p` on the claude side.
|
|
388
|
-
|
|
389
|
-
Codex-specific notes:
|
|
390
|
-
|
|
391
|
-
- **Auth is the Codex device-code sign-in by default**: `codex-accounts add` prints
|
|
392
|
-
a URL + one-time code you can open in ANY browser (this machine, your laptop, a
|
|
393
|
-
phone), so it works identically on a local Mac, over SSH, and on servers — there
|
|
394
|
-
is no portable setup-token equivalent for Codex, so a server account is signed in
|
|
395
|
-
ON the server the same way. `--browser` opts into the localhost browser-callback
|
|
396
|
-
flow instead (only works when the browser runs on the same machine — the callback
|
|
397
|
-
goes to `localhost:1455`). Either way the login runs with `CODEX_HOME` pointed at
|
|
398
|
-
the account dir, and identity is verified offline from the id-token before
|
|
399
|
-
anything is registered.
|
|
400
|
-
- **`auth.json` is never synced** in either direction, for the same reason
|
|
401
|
-
`.credentials.json` never is: the refresh token rotates, and two machines
|
|
402
|
-
refreshing one grant strand each other. Sync pushes manifest + `config.toml`
|
|
403
|
-
seeds + advisory limit state only.
|
|
404
|
-
- **Windows are classified by length**, not by name: any window ≤6h counts as the
|
|
405
|
-
self-healing session signal, anything longer as durable/weekly. A hard
|
|
406
|
-
`limit_reached`/`allowed:false` verdict marks the account even if no window shows
|
|
407
|
-
≥90%, and a reshaped payload is still scanned recursively for window-shaped
|
|
408
|
-
objects (fail open if nothing parses).
|
|
409
|
-
- **Idle-account telemetry** stays fresh the same way: a long-expired access token is
|
|
410
|
-
renewed via the OAuth refresh-token grant (`auth.openai.com/oauth/token`, the CLI's
|
|
411
|
-
own public client id) and the rotated credential is persisted 0600. Overrides for
|
|
412
|
-
tests: `CODEX_MULTIACC_TOKEN_URL`, `CODEX_MULTIACC_CLIENT_ID`,
|
|
413
|
-
`CODEX_MULTIACC_USAGE_URL`.
|
|
414
|
-
- **Earned usage resets are redeemed automatically at 95% used** (5% remaining) or
|
|
415
|
-
when the backend reports the limit finished. The limits refresher checks the reset
|
|
416
|
-
credits belonging to that same account, uses the soonest-expiring available credit,
|
|
417
|
-
and writes a per-account/window idempotency key before redemption so a lost response
|
|
418
|
-
or simultaneous fleet poll cannot spend a second reset. Set
|
|
419
|
-
`CODEX_MULTIACC_AUTO_RESET=0` only for emergency rollback/testing.
|
|
420
|
-
A finished-limit verdict takes precedence over a conflicting percentage. The
|
|
421
|
-
redemption key excludes unused model windows whose reset clocks move between reads.
|
|
422
|
-
Eligible accounts, including spent ones, check the credit endpoint every five
|
|
423
|
-
minutes even if the usage summary still says zero credits. A newly granted reset
|
|
424
|
-
is redeemed during that pass and clears the old limit; no-credit results are
|
|
425
|
-
checked again next pass, without waiting for the weekly reset date.
|
|
426
|
-
All authenticated accounts also report `usage.reset_credits_available` and
|
|
427
|
-
`usage.reset_credits_fetched_at` (ISO 8601) in `list/status/limits --json`.
|
|
428
|
-
The count comes from the credit endpoint, including below 95% used and with
|
|
429
|
-
automatic redemption disabled. The scheduled usage cadence bounds these reads.
|
|
430
|
-
Redemption shares that read, then reads back the remaining count after a POST;
|
|
431
|
-
no subtraction or embedded usage summary substitutes for provider evidence.
|
|
432
|
-
Zero means no available resets; missing fields mean the read was unavailable.
|
|
433
|
-
Credit identifiers remain local and selection does not use these fields.
|
|
434
|
-
- **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
|
|
435
|
-
addon's no-API-keys rule.
|
|
436
|
-
|
|
437
|
-
```bash
|
|
438
|
-
codex-accounts add # sign in a new ChatGPT account (device code —
|
|
439
|
-
# open the URL in any browser, enter the code)
|
|
440
|
-
codex-accounts add --browser # localhost browser-callback flow instead
|
|
441
|
-
codex-accounts import <email> --id acct-01 && codex-accounts adopt acct-01
|
|
442
|
-
# adopt this machine's existing ~/.codex login
|
|
443
|
-
codex-accounts list | status | expired | relogin | verify | limits | health | sync
|
|
444
|
-
claude-multiacc codex <cmd> # same commands via the npm wrapper
|
|
445
|
-
```
|
|
369
|
+
Both pools use the same selection policy. See [Codex account commands and behavior](docs/CODEX.md).
|
|
446
370
|
|
|
447
371
|
## Install / update / uninstall
|
|
448
372
|
|
|
449
|
-
### npm (recommended)
|
|
450
|
-
|
|
451
373
|
```bash
|
|
452
|
-
npm install -g claude-multiacc
|
|
453
|
-
|
|
454
|
-
npx claude-multiacc
|
|
374
|
+
npm install -g claude-multiacc@latest
|
|
375
|
+
codex-accounts verify
|
|
455
376
|
```
|
|
456
377
|
|
|
457
|
-
`claude-multiacc`
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
claude-multiacc
|
|
461
|
-
claude-multiacc self-update # npm i -g @latest + re-install
|
|
462
|
-
claude-multiacc uninstall [--purge-data]
|
|
463
|
-
claude-multiacc list | status | add <email> | ... # passthrough to claude-accounts
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
**Automatic updates:** a globally-installed package re-runs `install.sh` from its own
|
|
467
|
-
postinstall, so `npm i -g claude-multiacc@latest` fully updates the shim, CLI, and jobs.
|
|
468
|
-
A daily job (launchd on macOS, cron on Linux, 04:07) runs `claude-accounts self-update`
|
|
469
|
-
to pull the latest release on its own — `npm i -g @latest` for npm installs, `git pull +
|
|
470
|
-
reinstall` for git checkouts; set `CLAUDE_MULTIACC_AUTOUPDATE=0` before install to
|
|
471
|
-
opt out. `update-notifier` also prints a one-line nudge when a newer version is on npm.
|
|
472
|
-
(Note: newer npm versions gate install scripts; if postinstall is blocked, just run
|
|
473
|
-
`claude-multiacc install` once.)
|
|
474
|
-
|
|
475
|
-
New versions publish to npm automatically from `main` via GitHub Actions
|
|
476
|
-
(`.github/workflows/publish.yml`, OIDC Trusted Publishing — no long-lived token after the
|
|
477
|
-
first bootstrap publish; `scripts/auto-version.mjs` bumps the patch above the last release).
|
|
478
|
-
|
|
479
|
-
### From a git checkout
|
|
480
|
-
|
|
481
|
-
```bash
|
|
482
|
-
git clone <repo> && cd claude-multiacc && ./install.sh # macOS or Linux, idempotent
|
|
483
|
-
./install.sh --server root@1.2.3.4 # override sync target
|
|
484
|
-
git pull && ./install.sh # update (data untouched)
|
|
485
|
-
./install.sh --uninstall # restores stock behavior, keeps account data
|
|
486
|
-
./install.sh --uninstall --purge-data # also deletes ~/.claude-accounts
|
|
487
|
-
```
|
|
378
|
+
npm installs `claude-multiacc`, `claude-accounts`, `codex-accounts`, and `multiacc-select`
|
|
379
|
+
into its global bin directory, so account commands work in the current shell. Open a
|
|
380
|
+
new shell for the `claude` and `codex` shims. If auto-setup was skipped, run
|
|
381
|
+
`claude-multiacc install` to initialize the pools and shell setup.
|
|
488
382
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
**Servers / machines without repo access:** install via **npm** — the registry is
|
|
492
|
-
public, so the daily self-update needs no credentials at all (no rsync, no keys).
|
|
493
|
-
A git checkout self-updates too, but only if `git pull` can authenticate; for a
|
|
494
|
-
private repo on a server that means a read-only deploy key (the 138 server runs
|
|
495
|
-
this way: `core.sshCommand` pinned to its deploy key). A plain copied tree is the
|
|
496
|
-
one layout that can NOT self-update — don't ship the addon that way.
|
|
497
|
-
|
|
498
|
-
What install does (all reversible, nothing else):
|
|
499
|
-
|
|
500
|
-
- **macOS:** marked PATH block at the END of `~/.zshenv`, `~/.zprofile`, `~/.zshrc`
|
|
501
|
-
(+ bash rc files if present) — end-of-file placement matters because those files
|
|
502
|
-
re-prepend `~/.local/bin`; launchd agents `com.claude-multiacc.limits` +
|
|
503
|
-
`.codex-limits` (5m), `.health` + `.codex-health` (weekly Mon morning), and
|
|
504
|
-
`.update` (daily 04:07). Notes when this Mac keeps Claude Code logins in the
|
|
505
|
-
Keychain (the pool reads them; ssh sessions cannot — mint portable tokens for
|
|
506
|
-
accounts that must work from everywhere).
|
|
507
|
-
- **Linux (root):** PATH block in `~/.bashrc` + `/etc/profile.d/claude-multiacc.sh`,
|
|
508
|
-
shim symlinks at `/usr/local/bin/claude` and `/usr/local/bin/codex` (shadow via
|
|
509
|
-
PATH order — on the systemd default PATH too; the original binaries are untouched),
|
|
510
|
-
cron entries for limits/health (both providers) + the daily auto-update.
|
|
511
|
-
- Both: `~/.claude-accounts/` skeleton + `accounts.json` manifest.
|
|
383
|
+
See [installation, updates, removal, and command-not-found recovery](docs/INSTALLATION.md)
|
|
384
|
+
for npm, npx, git checkout, and server instructions.
|
|
512
385
|
|
|
513
386
|
Account lifecycle, synchronization, fleet integration, environment switches,
|
|
514
387
|
verification, troubleshooting, and security guidance continue in
|
package/bin/claude-accounts
CHANGED
|
@@ -86,8 +86,7 @@ USAGE
|
|
|
86
86
|
Skips accounts fetched in the last 45s and honors 429/refresh backoff;
|
|
87
87
|
--force ignores all three.
|
|
88
88
|
claude-accounts health limits + full verify; logs to health.log
|
|
89
|
-
claude-accounts self-update update
|
|
90
|
-
pull + reinstall); logs to update.log
|
|
89
|
+
claude-accounts self-update update this npm/git install; logs to update.log
|
|
91
90
|
claude-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
92
91
|
|
|
93
92
|
ENV
|
|
@@ -103,7 +102,7 @@ ENV
|
|
|
103
102
|
EOF
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run install
|
|
105
|
+
require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run claude-multiacc install first"; }
|
|
107
106
|
|
|
108
107
|
next_id() {
|
|
109
108
|
local n=1 id
|
|
@@ -2483,6 +2482,21 @@ AUTH_ERR = re.compile(
|
|
|
2483
2482
|
ORG_ERR = re.compile(
|
|
2484
2483
|
r'organization has disabled|subscription access.*disabl|disabled claude subscription'
|
|
2485
2484
|
r'|ask your admin to enable|not authorized to use claude code', re.I)
|
|
2485
|
+
# EXCULPATORY, tested FIRST — the twin of BROKEN_CLI in bin/codex-accounts, and
|
|
2486
|
+
# it matters MORE here: AUTH_ERR above matches a bare `401` or `403`, so any
|
|
2487
|
+
# machine fault whose output happens to contain those three digits parks a live
|
|
2488
|
+
# login. `mark_expired` writes no `soft_until`, so that verdict is permanent
|
|
2489
|
+
# until a person clears it. These patterns say the CLI never reached the API,
|
|
2490
|
+
# which is a fact about the machine and never about the login.
|
|
2491
|
+
# See health.log 2026-08-24: `env: node: No such file or directory` failed
|
|
2492
|
+
# every account in one pass.
|
|
2493
|
+
BROKEN_CLI = re.compile(
|
|
2494
|
+
r'missing optional dependency|unsupported (platform|target triple):'
|
|
2495
|
+
r'|cannot find module|ERR_MODULE_NOT_FOUND|ERR_DLOPEN_FAILED'
|
|
2496
|
+
r'|env: node: no such file or directory|node: command not found'
|
|
2497
|
+
r'|spawn .{0,80}(ENOENT|EACCES|EPERM)|^dyld\[[0-9]+\]:'
|
|
2498
|
+
r'|cannot execute binary file|bad CPU type in executable|exec format error',
|
|
2499
|
+
re.I | re.M)
|
|
2486
2500
|
|
|
2487
2501
|
def mark_expired(d, slug, detail=''):
|
|
2488
2502
|
"""Park an account the shim must stop selecting. Verify is the strongest signal
|
|
@@ -2608,9 +2622,16 @@ for acct in manifest.get('accounts', []):
|
|
|
2608
2622
|
pass
|
|
2609
2623
|
print(f'{aid} {acct["email"]}: PASS ({dt:.1f}s) -> {out[:60]!r}')
|
|
2610
2624
|
else:
|
|
2611
|
-
|
|
2625
|
+
raw = (r.stderr or '').strip()
|
|
2626
|
+
err = raw[:200]
|
|
2612
2627
|
hint = ''
|
|
2613
|
-
|
|
2628
|
+
# STDERR only, and the WHOLE of it: stdout is the model's own answer,
|
|
2629
|
+
# and a session that merely discusses "Cannot find module" must not be
|
|
2630
|
+
# able to excuse a real dead login.
|
|
2631
|
+
if BROKEN_CLI.search(raw):
|
|
2632
|
+
hint = (' — the Claude CLI cannot run on this machine; not a login '
|
|
2633
|
+
'problem, nothing was parked')
|
|
2634
|
+
elif ORG_ERR.search(out) or ORG_ERR.search(err):
|
|
2614
2635
|
# Not an auth problem: the account authenticates fine, its organization
|
|
2615
2636
|
# has simply turned Claude Code subscription access off. Park it — a
|
|
2616
2637
|
# re-login changes nothing — and say what actually helps.
|
package/bin/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// claude-multiacc npm entrypoint. Thin dispatcher over the bash addon:
|
|
3
3
|
// claude-multiacc [install] run install.sh (install or update, idempotent)
|
|
4
4
|
// claude-multiacc uninstall [--purge-data]
|
|
5
|
-
// claude-multiacc self-update
|
|
5
|
+
// claude-multiacc self-update update the running npm/git installation
|
|
6
6
|
// claude-multiacc doctor environment + auth summary
|
|
7
7
|
// claude-multiacc <accounts...> passthrough to claude-accounts (list/status/add/...)
|
|
8
8
|
// A globally-installed package auto-runs install.sh on (re)install via postinstall, so
|
|
@@ -59,10 +59,10 @@ mirror to a deploy server. No API keys — Claude and ChatGPT subscription login
|
|
|
59
59
|
USAGE
|
|
60
60
|
claude-multiacc [install] install or update the addon (idempotent)
|
|
61
61
|
claude-multiacc uninstall [--purge-data]
|
|
62
|
-
claude-multiacc self-update update
|
|
62
|
+
claude-multiacc self-update update this npm/git installation + re-install
|
|
63
63
|
claude-multiacc doctor show environment + account health (both pools)
|
|
64
64
|
claude-multiacc <cmd> [args...] run a claude-accounts command, e.g.
|
|
65
|
-
list | status | add
|
|
65
|
+
list | status | add [email] | login <acct-NN> |
|
|
66
66
|
remove <acct-NN> | sync | verify | limits | health
|
|
67
67
|
claude-multiacc codex <cmd> [args...] same commands for the Codex pool
|
|
68
68
|
(runs codex-accounts <cmd>)
|
|
@@ -71,6 +71,12 @@ USAGE
|
|
|
71
71
|
claude-multiacc -v | --version
|
|
72
72
|
claude-multiacc -h | --help
|
|
73
73
|
|
|
74
|
+
ACCOUNT COMMANDS (also installed directly by npm)
|
|
75
|
+
claude-accounts verify verify the Claude pool
|
|
76
|
+
codex-accounts verify verify the Codex pool
|
|
77
|
+
claude-accounts --help all Claude account commands and options
|
|
78
|
+
codex-accounts --help all Codex account commands and options
|
|
79
|
+
|
|
74
80
|
INSTALL
|
|
75
81
|
npm install -g claude-multiacc # postinstall wires up the PATH shims + schedules
|
|
76
82
|
# or, no global install:
|
|
@@ -91,23 +97,14 @@ switch (cmd) {
|
|
|
91
97
|
case 'uninstall':
|
|
92
98
|
status = runInstall(['--uninstall', ...argv.slice(1)]);
|
|
93
99
|
break;
|
|
94
|
-
case 'self-update':
|
|
95
|
-
|
|
96
|
-
const up = sh('npm', ['install', '-g', `${pkg.name}@latest`]);
|
|
97
|
-
if (up !== 0) {
|
|
98
|
-
console.error(
|
|
99
|
-
'npm update failed — is this a global npm install? '
|
|
100
|
-
+ '(git checkouts update with: git pull && ./install.sh)',
|
|
101
|
-
);
|
|
102
|
-
status = up;
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
// The freshly-installed global package re-runs its own postinstall, so the addon
|
|
106
|
-
// is already reconciled; nothing else to do here.
|
|
107
|
-
console.log('Updated. Open a new shell to pick up any PATH changes.');
|
|
100
|
+
case 'self-update':
|
|
101
|
+
status = runAccounts(argv);
|
|
108
102
|
break;
|
|
109
|
-
}
|
|
110
103
|
case 'doctor': {
|
|
104
|
+
if (argv.slice(1).some(arg => arg === '--help' || arg === '-h')) {
|
|
105
|
+
console.log(HELP);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
111
108
|
console.log('== claude pool ==');
|
|
112
109
|
const claudeStatus = runAccounts(['status']);
|
|
113
110
|
console.log('\n== codex pool ==');
|
|
@@ -128,6 +125,7 @@ switch (cmd) {
|
|
|
128
125
|
break;
|
|
129
126
|
case '-h':
|
|
130
127
|
case '--help':
|
|
128
|
+
case 'help':
|
|
131
129
|
console.log(HELP);
|
|
132
130
|
break;
|
|
133
131
|
default:
|
package/bin/codex-accounts
CHANGED
|
@@ -90,8 +90,7 @@ USAGE
|
|
|
90
90
|
stay selectable. Skips accounts fetched in the last 45s and honors
|
|
91
91
|
429/refresh backoff; --force ignores all three.
|
|
92
92
|
codex-accounts health limits + full verify; logs to health.log
|
|
93
|
-
codex-accounts self-update update
|
|
94
|
-
pull + reinstall); logs to update.log
|
|
93
|
+
codex-accounts self-update update this npm/git install; logs to update.log
|
|
95
94
|
codex-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
96
95
|
|
|
97
96
|
ENV
|
|
@@ -108,7 +107,7 @@ ENV
|
|
|
108
107
|
EOF
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run install
|
|
110
|
+
require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run claude-multiacc install first"; }
|
|
112
111
|
|
|
113
112
|
next_id() {
|
|
114
113
|
local n=1 id
|
|
@@ -1769,6 +1768,30 @@ ORG_ERR = re.compile(
|
|
|
1769
1768
|
r'disabled by (your )?(workspace )?admin|admin (has )?disabled'
|
|
1770
1769
|
r'|(workspace|organization) has disabled (codex|chatgpt)'
|
|
1771
1770
|
r'|codex.{0,20}disabled for (your|this) (workspace|organization)', re.I)
|
|
1771
|
+
# EXCULPATORY, and therefore tested FIRST. These say the CLI never reached the
|
|
1772
|
+
# API at all — a missing native package, a missing node, an unrunnable binary —
|
|
1773
|
+
# so they are facts about this MACHINE, not about the login. Parking on one
|
|
1774
|
+
# converts an indeterminate probe into "this login is dead", and that verdict
|
|
1775
|
+
# does not self-heal: `mark_expired` here writes no `soft_until`, so the shim
|
|
1776
|
+
# reads it as proven dead and keeps it, while the panel refuses to re-import
|
|
1777
|
+
# the token until its fingerprint changes.
|
|
1778
|
+
#
|
|
1779
|
+
# Not hypothetical. health.log 2026-08-24T06:37:02Z: all four accounts
|
|
1780
|
+
# `FAIL rc=127 err='env: node: No such file or directory'` — one broken PATH
|
|
1781
|
+
# reddened the entire matrix, and it escaped parking only because that exact
|
|
1782
|
+
# string happens not to match AUTH_ERR. `Missing optional dependency
|
|
1783
|
+
# @openai/codex-<platform>` (my-mini, 2026-09-10) is the same class.
|
|
1784
|
+
#
|
|
1785
|
+
# Keep it NARROW. It must never contain a word a genuine auth failure uses
|
|
1786
|
+
# ("permission denied", "forbidden", a bare "not found"), or a real dead login
|
|
1787
|
+
# would be excused as a broken machine. Widen only with a test.
|
|
1788
|
+
BROKEN_CLI = re.compile(
|
|
1789
|
+
r'missing optional dependency|unsupported (platform|target triple):'
|
|
1790
|
+
r'|cannot find module|ERR_MODULE_NOT_FOUND|ERR_DLOPEN_FAILED'
|
|
1791
|
+
r'|env: node: no such file or directory|node: command not found'
|
|
1792
|
+
r'|spawn .{0,80}(ENOENT|EACCES|EPERM)|^dyld\[[0-9]+\]:'
|
|
1793
|
+
r'|cannot execute binary file|bad CPU type in executable|exec format error',
|
|
1794
|
+
re.I | re.M)
|
|
1772
1795
|
|
|
1773
1796
|
def mark_expired(d, slug, detail=''):
|
|
1774
1797
|
"""Park an account the shim must stop selecting. Verify is the strongest signal
|
|
@@ -1846,9 +1869,16 @@ for acct in manifest.get('accounts', []):
|
|
|
1846
1869
|
pass
|
|
1847
1870
|
print(f'{aid} {acct["email"]}: PASS ({dt:.1f}s) -> {(last or out)[:60]!r}')
|
|
1848
1871
|
else:
|
|
1849
|
-
|
|
1872
|
+
raw = (r.stderr or '').strip()
|
|
1873
|
+
err = raw[:200]
|
|
1850
1874
|
hint = ''
|
|
1851
|
-
|
|
1875
|
+
# STDERR only, and the WHOLE of it: stdout is the model's own answer,
|
|
1876
|
+
# and a session that merely discusses "Cannot find module" must not be
|
|
1877
|
+
# able to excuse a real dead login.
|
|
1878
|
+
if BROKEN_CLI.search(raw):
|
|
1879
|
+
hint = (' — the Codex CLI cannot run on this machine; not a login '
|
|
1880
|
+
'problem, nothing was parked')
|
|
1881
|
+
elif ORG_ERR.search(out) or ORG_ERR.search(err):
|
|
1852
1882
|
# Not an auth problem: the account authenticates fine, a workspace admin
|
|
1853
1883
|
# has simply turned Codex access off. Park it — a re-login changes
|
|
1854
1884
|
# nothing — and say what actually helps.
|
package/bin/multiacc-select
CHANGED
|
@@ -30,6 +30,10 @@ def _strict_object(pairs: list[tuple[str, object]]) -> dict:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def main() -> int:
|
|
33
|
+
if sys.argv[1:] in (["--help"], ["-h"]):
|
|
34
|
+
print("usage: multiacc-select --request-json - --response-json -\n"
|
|
35
|
+
" multiacc-select --version")
|
|
36
|
+
return 0
|
|
33
37
|
if sys.argv[1:] == ["--version"]:
|
|
34
38
|
print(SELECTOR_VERSION)
|
|
35
39
|
return 0
|
|
@@ -370,65 +370,7 @@ The codex shim honors the same switches spelled `CODEX_*`: `CODEX_ACCOUNT`,
|
|
|
370
370
|
300s — weekly markers never clear early), `CODEX_ACCOUNTS_ROOT` (legacy
|
|
371
371
|
`CODEX_ACCOUNTS_DIR`), `CODEX_MULTIACC_SYNC_TARGET`, `CODEX_MULTIACC_THRESHOLD`.
|
|
372
372
|
|
|
373
|
-
## Verification
|
|
373
|
+
## Verification and troubleshooting
|
|
374
374
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
claude-accounts verify # real matrix: `claude -p "reply OK"` per authed account
|
|
378
|
-
claude-accounts verify --quick# auth presence/expiry only, no inference
|
|
379
|
-
claude-accounts limits # live per-bucket usage incl. the Fable bucket
|
|
380
|
-
claude-accounts health # limits + full verify, logs to health.log, notifies on failure (Mac)
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
Verified end-to-end on both machines (2026-07-13): full matrix PASS, 20-invocation shim
|
|
384
|
-
spread clean, real project `-p` through the shim OK on Mac (`~/pipeline`) and server
|
|
385
|
-
(`/opt/inapp-onboarding`), uninstall→stock→reinstall roundtrip byte-identical, three
|
|
386
|
-
server systemd services (`vpn-server-management`, `inapp-onboarding`, `vpn-seo-panel`)
|
|
387
|
-
repointed via `CLAUDE_BIN=/usr/local/bin/claude` and restarted healthy.
|
|
388
|
-
|
|
389
|
-
## Troubleshooting
|
|
390
|
-
|
|
391
|
-
- **`claude` resolves to the real binary, not the shim** — open a new shell, or check
|
|
392
|
-
that the marked block is the LAST PATH manipulation in your rc file
|
|
393
|
-
(`grep -A2 'claude-multiacc >>>' ~/.zshrc`).
|
|
394
|
-
- **An account never gets picked** — `claude-accounts status`: no auth on this machine,
|
|
395
|
-
a dead login (`selectable: NO`, see `claude-accounts expired`), or an active `.limited`
|
|
396
|
-
marker (shows bucket + minutes to reset).
|
|
397
|
-
- **"Failed to authenticate: OAuth session expired and could not be refreshed"** — the
|
|
398
|
-
login behind that run is dead. Run `claude-accounts expired` to see which, then
|
|
399
|
-
`claude-accounts relogin`. Accounts in that state are excluded from selection
|
|
400
|
-
automatically, so this only surfaces when *every* account needs a re-login (the shim
|
|
401
|
-
then falls back to the machine's own `~/.claude` login) or when it was pinned with
|
|
402
|
-
`CLAUDE_ACCOUNT`.
|
|
403
|
-
- **"Please run /login · API Error: 401 OAuth access token is invalid"** — the portable
|
|
404
|
-
setup-token is rejected even though its presence and `claude auth status` look healthy.
|
|
405
|
-
The shim now runs a private first-use inference, parks a rejected token, and reselects
|
|
406
|
-
before direct, TUI, or `--resume` work sees the 401. Run `claude-accounts verify` to
|
|
407
|
-
check every token immediately; `claude-accounts expired` reports token-only accounts
|
|
408
|
-
as `UNVERIFIED` until that proof exists.
|
|
409
|
-
- **Everything marked limited** — the shim still runs: the still-serving limited accounts
|
|
410
|
-
go through the same two cuts (session gate, then strict best-weekly) and one is handed
|
|
411
|
-
out anyway; check `selection.log` for `all-limited fallback=` lines.
|
|
412
|
-
- **Sync fails** — `tail ~/.claude-accounts/sync.log`; it's ssh/rsync to the manifest's
|
|
413
|
-
`server` (BatchMode — needs key auth).
|
|
414
|
-
- **A service bypasses the shim** — it spawns an absolute path. Point its env
|
|
415
|
-
(`CLAUDE_BIN` etc.) at `/usr/local/bin/claude`. Docker-internal `claude` installs
|
|
416
|
-
(e.g. openclaw live-test containers) are out of scope for a host shim.
|
|
417
|
-
- **`add` sign-in says "Sign in again to continue"** — that's Claude's step-up
|
|
418
|
-
authentication, not a tool error: granting a long-lived token needs a *recent* sign-in.
|
|
419
|
-
Open a fresh incognito window, sign in to the target account at claude.ai **first**,
|
|
420
|
-
then paste the link and approve. The approval scope is `user:inference`
|
|
421
|
-
(*"Contribute to your Claude subscription usage"*) — that's correct and minimal.
|
|
422
|
-
- **Limits look stale** — launchd: `launchctl list | grep claude-multiacc`;
|
|
423
|
-
cron: `crontab -l | grep claude-multiacc`. The shim also kicks a refresh when data
|
|
424
|
-
is >3 min old.
|
|
425
|
-
|
|
426
|
-
## Security notes & known risks
|
|
427
|
-
|
|
428
|
-
- Tokens and credentials live as 0600 files under `~/.claude-accounts` (and
|
|
429
|
-
`/root/.claude-accounts` on the server). Server compromise = account access.
|
|
430
|
-
- `selection.log` records timestamps/account/cwd only — never prompt text.
|
|
431
|
-
- Rotating multiple subscriptions to spread usage may be flagged by anti-abuse systems;
|
|
432
|
-
accounts can be banned for limit circumvention. Known and accepted by the operator.
|
|
433
|
-
- The usage endpoint is the internal one `/usage` consumes; if it changes shape, limit
|
|
434
|
-
tracking fails open (accounts stay available, error-retry is the backstop).
|
|
375
|
+
See [verification commands and troubleshooting](VERIFICATION.md), including the
|
|
376
|
+
packed npm command checks, account health, shell PATH setup, and security notes.
|
package/docs/CODEX.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Codex support (OpenAI Codex CLI)
|
|
2
|
+
|
|
3
|
+
Codex uses the same machinery as the [Claude pool](../README.md), with these translations:
|
|
4
|
+
|
|
5
|
+
| Claude side | Codex side |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `bin/claude` shim, `claude-accounts` CLI | `bin/codex` shim, `codex-accounts` CLI |
|
|
8
|
+
| pool `~/.claude-accounts` | pool `~/.codex-accounts` |
|
|
9
|
+
| `CLAUDE_CONFIG_DIR` per-account dirs | `CODEX_HOME` per-account dirs |
|
|
10
|
+
| `.credentials.json` / macOS Keychain item (OAuth, machine-local) | `auth.json` (ChatGPT OAuth, machine-local) |
|
|
11
|
+
| `claude -p` auto-retry | `codex exec` auto-retry |
|
|
12
|
+
| Anthropic OAuth usage and Fable buckets | Codex usage endpoint and per-model buckets |
|
|
13
|
+
| `CLAUDE_*` pool controls | equivalent `CODEX_*` controls |
|
|
14
|
+
|
|
15
|
+
Same selection rule (session gate first — `CODEX_MULTIACC_SESSION_GATE`, default 50 —
|
|
16
|
+
then the 30-point weekly-headroom band with random spread, ≥90% any-bucket
|
|
17
|
+
exclusion, peers rotate), same marker semantics (`.limited` cooldowns, `.expired` parks with
|
|
18
|
+
credential/policy scoping and soft expiry), same fail-open guarantees, same sync
|
|
19
|
+
safety guards. The `codex` shim engages the buffered auto-retry only for
|
|
20
|
+
`codex exec` runs with finite stdin, exactly like `-p` on the claude side.
|
|
21
|
+
|
|
22
|
+
Codex-specific notes:
|
|
23
|
+
|
|
24
|
+
- **Auth is the Codex device-code sign-in by default**: `codex-accounts add` prints
|
|
25
|
+
a URL + one-time code you can open in ANY browser (this machine, your laptop, a
|
|
26
|
+
phone), so it works identically on a local Mac, over SSH, and on servers — there
|
|
27
|
+
is no portable setup-token equivalent for Codex, so a server account is signed in
|
|
28
|
+
ON the server the same way. `--browser` opts into the localhost browser-callback
|
|
29
|
+
flow instead (only works when the browser runs on the same machine — the callback
|
|
30
|
+
goes to `localhost:1455`). Either way the login runs with `CODEX_HOME` pointed at
|
|
31
|
+
the account dir, and identity is verified offline from the id-token before
|
|
32
|
+
anything is registered.
|
|
33
|
+
- **`auth.json` is never synced** in either direction, for the same reason
|
|
34
|
+
`.credentials.json` never is: the refresh token rotates, and two machines
|
|
35
|
+
refreshing one grant strand each other. Sync pushes manifest + `config.toml`
|
|
36
|
+
seeds + advisory limit state only.
|
|
37
|
+
- **Windows are classified by length**, not by name: any window ≤6h counts as the
|
|
38
|
+
self-healing session signal, anything longer as durable/weekly. A hard
|
|
39
|
+
`limit_reached`/`allowed:false` verdict marks the account even if no window shows
|
|
40
|
+
≥90%, and a reshaped payload is still scanned recursively for window-shaped
|
|
41
|
+
objects (fail open if nothing parses).
|
|
42
|
+
- **Idle-account telemetry** stays fresh the same way: a long-expired access token is
|
|
43
|
+
renewed via the OAuth refresh-token grant (`auth.openai.com/oauth/token`, the CLI's
|
|
44
|
+
own public client id) and the rotated credential is persisted 0600. Overrides for
|
|
45
|
+
tests: `CODEX_MULTIACC_TOKEN_URL`, `CODEX_MULTIACC_CLIENT_ID`,
|
|
46
|
+
`CODEX_MULTIACC_USAGE_URL`.
|
|
47
|
+
- **Earned usage resets are redeemed automatically at 95% used** (5% remaining) or
|
|
48
|
+
when the backend reports the limit finished. The limits refresher checks the reset
|
|
49
|
+
credits belonging to that same account, uses the soonest-expiring available credit,
|
|
50
|
+
and writes a per-account/window idempotency key before redemption so a lost response
|
|
51
|
+
or simultaneous fleet poll cannot spend a second reset. Set
|
|
52
|
+
`CODEX_MULTIACC_AUTO_RESET=0` only for emergency rollback/testing.
|
|
53
|
+
A finished-limit verdict takes precedence over a conflicting percentage. The
|
|
54
|
+
redemption key excludes unused model windows whose reset clocks move between reads.
|
|
55
|
+
Eligible accounts, including spent ones, check the credit endpoint every five
|
|
56
|
+
minutes even if the usage summary still says zero credits. A newly granted reset
|
|
57
|
+
is redeemed during that pass and clears the old limit; no-credit results are
|
|
58
|
+
checked again next pass, without waiting for the weekly reset date.
|
|
59
|
+
All authenticated accounts also report `usage.reset_credits_available` and
|
|
60
|
+
`usage.reset_credits_fetched_at` (ISO 8601) in `list/status/limits --json`.
|
|
61
|
+
The count comes from the credit endpoint, including below 95% used and with
|
|
62
|
+
automatic redemption disabled. The scheduled usage cadence bounds these reads.
|
|
63
|
+
Redemption shares that read, then reads back the remaining count after a POST;
|
|
64
|
+
no subtraction or embedded usage summary substitutes for provider evidence.
|
|
65
|
+
Zero means no available resets; missing fields mean the read was unavailable.
|
|
66
|
+
Credit identifiers remain local and selection does not use these fields.
|
|
67
|
+
- **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
|
|
68
|
+
addon's no-API-keys rule.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
codex-accounts add # sign in a new ChatGPT account (device code —
|
|
72
|
+
# open the URL in any browser, enter the code)
|
|
73
|
+
codex-accounts add --browser # localhost browser-callback flow instead
|
|
74
|
+
codex-accounts import you@example.com --id acct-01
|
|
75
|
+
codex-accounts adopt acct-01
|
|
76
|
+
# adopt this machine's existing ~/.codex login
|
|
77
|
+
codex-accounts list
|
|
78
|
+
codex-accounts status
|
|
79
|
+
codex-accounts verify --quick # check local auth without inference
|
|
80
|
+
codex-accounts verify # real codex exec call per account
|
|
81
|
+
codex-accounts --help # all account commands and options
|
|
82
|
+
claude-multiacc codex status # same status command via the npm wrapper
|
|
83
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Install / update / uninstall
|
|
2
|
+
|
|
3
|
+
### npm (recommended)
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g claude-multiacc # postinstall wires up the shim, CLI, and scheduled jobs
|
|
7
|
+
# or run it once without installing globally:
|
|
8
|
+
npx claude-multiacc
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`claude-multiacc` then wraps everything:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude-multiacc # install or update the addon (idempotent)
|
|
15
|
+
claude-multiacc self-update # update the running installation + re-install
|
|
16
|
+
claude-multiacc list # Claude pool, via claude-accounts
|
|
17
|
+
claude-multiacc codex list # Codex pool, via codex-accounts
|
|
18
|
+
claude-multiacc doctor # status for both pools
|
|
19
|
+
claude-multiacc install --help # installer options
|
|
20
|
+
claude-multiacc uninstall # keep both account pools
|
|
21
|
+
# To delete both pools as well: claude-multiacc uninstall --purge-data
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
After `claude-multiacc uninstall`, `npm uninstall -g claude-multiacc` also removes
|
|
25
|
+
the npm package and its command links.
|
|
26
|
+
|
|
27
|
+
npm links `claude-multiacc`, `claude-accounts`, `codex-accounts`, and `multiacc-select`
|
|
28
|
+
into its global bin directory. If `claude-multiacc` works, both account commands work
|
|
29
|
+
in that same shell, even when postinstall was skipped. The `claude` and `codex` shims
|
|
30
|
+
still need the installer's PATH setup: open a new shell after installation.
|
|
31
|
+
|
|
32
|
+
If an older installation says `codex-accounts: command not found`, update it:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g claude-multiacc@latest
|
|
36
|
+
codex-accounts verify
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Before updating, `claude-multiacc codex verify` runs the same verification through
|
|
40
|
+
the existing npm wrapper. For git/npx installs, open a new shell or run the exact
|
|
41
|
+
`export PATH=...` command printed by the installer. Run `claude-multiacc install`
|
|
42
|
+
if auto-setup was skipped and the pool has no manifest yet.
|
|
43
|
+
|
|
44
|
+
**Automatic updates:** a globally-installed package re-runs `install.sh` from its own
|
|
45
|
+
postinstall, so `npm i -g claude-multiacc@latest` fully updates the shim, CLI, and jobs.
|
|
46
|
+
A daily job (launchd on macOS, cron on Linux, 04:07) runs `claude-accounts self-update`
|
|
47
|
+
to pull the latest release on its own — `npm i -g claude-multiacc@latest` for npm installs,
|
|
48
|
+
`git pull && ./install.sh` for git checkouts; set `CLAUDE_MULTIACC_AUTOUPDATE=0` before install to
|
|
49
|
+
opt out. `update-notifier` also prints a one-line nudge when a newer version is on npm.
|
|
50
|
+
(Note: newer npm versions gate install scripts; if postinstall is blocked, just run
|
|
51
|
+
`claude-multiacc install` once.)
|
|
52
|
+
|
|
53
|
+
New versions publish to npm automatically from `main` via GitHub Actions
|
|
54
|
+
(`.github/workflows/publish.yml`, OIDC Trusted Publishing — no long-lived token after the
|
|
55
|
+
first bootstrap publish; `scripts/auto-version.mjs` bumps the patch above the last release).
|
|
56
|
+
|
|
57
|
+
### From a git checkout
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/gowalk-public/claude-multiacc.git
|
|
61
|
+
cd claude-multiacc
|
|
62
|
+
./install.sh # macOS or Linux, idempotent
|
|
63
|
+
./install.sh --server root@1.2.3.4 # override sync target
|
|
64
|
+
git pull && ./install.sh # update (data untouched)
|
|
65
|
+
./install.sh --uninstall # restores stock behavior, keeps account data
|
|
66
|
+
./install.sh --uninstall --purge-data # also deletes both account pools
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`self-update` auto-detects which of these you used (npm global vs git checkout).
|
|
70
|
+
|
|
71
|
+
**Servers / machines without repo access:** install via **npm** — the registry is
|
|
72
|
+
public, so the daily self-update needs no credentials at all (no rsync, no keys).
|
|
73
|
+
A git checkout self-updates too, but only if `git pull` can authenticate; for a
|
|
74
|
+
private repo on a server that means a read-only deploy key (the 138 server runs
|
|
75
|
+
this way: `core.sshCommand` pinned to its deploy key). A plain copied tree is the
|
|
76
|
+
one layout that can NOT self-update — don't ship the addon that way.
|
|
77
|
+
|
|
78
|
+
What install does (all reversible, nothing else):
|
|
79
|
+
|
|
80
|
+
- **macOS:** marked PATH block at the END of `~/.zshenv`, `~/.zprofile`, `~/.zshrc`
|
|
81
|
+
(+ bash rc files if present) — end-of-file placement matters because those files
|
|
82
|
+
re-prepend `~/.local/bin`; launchd agents `com.claude-multiacc.limits` +
|
|
83
|
+
`.codex-limits` (15m Claude / 5m Codex), `.health` + `.codex-health` (weekly Mon morning), and
|
|
84
|
+
`.update` (daily 04:07). Notes when this Mac keeps Claude Code logins in the
|
|
85
|
+
Keychain (the pool reads them; ssh sessions cannot — mint portable tokens for
|
|
86
|
+
accounts that must work from everywhere).
|
|
87
|
+
- **Linux (root):** PATH block in `~/.bashrc` + `/etc/profile.d/claude-multiacc.sh`,
|
|
88
|
+
shim symlinks at `/usr/local/bin/claude` and `/usr/local/bin/codex` (shadow via
|
|
89
|
+
PATH order — on the systemd default PATH too; the original binaries are untouched),
|
|
90
|
+
cron entries for limits/health (both providers) + the daily auto-update.
|
|
91
|
+
- Both: `~/.claude-accounts/` and `~/.codex-accounts/`, each with an `accounts.json` manifest.
|
package/docs/UNIFIED_SELECTOR.md
CHANGED
|
@@ -13,6 +13,8 @@ multiacc-select --request-json - --response-json -
|
|
|
13
13
|
|
|
14
14
|
The command reads one UTF-8 JSON object from stdin and writes one JSON object to
|
|
15
15
|
stdout. `multiacc-select --version` reports the selector contract version.
|
|
16
|
+
`multiacc-select --help` (or `-h`) prints usage and exits successfully without
|
|
17
|
+
reading stdin. All three forms also work through `claude-multiacc select`.
|
|
16
18
|
|
|
17
19
|
The request schema is `claude-multiacc/pool-selection.v2` and contains:
|
|
18
20
|
|