claude-multiacc 2.0.26 → 2.0.28

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 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 currently covers 918 sandboxed cases with no
20
- network/quota use; the unified selector has its own adversarial contract suite.
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
 
@@ -315,6 +315,45 @@ same ownership rule. Note that the **installed layout deliberately shares that t
315
315
  default codex pool this scan stays off and the usage endpoint remains codex's only limit
316
316
  signal. Claude accounts keep a private `sessions/` registry, so it is fully active there.
317
317
 
318
+ A shared tree also gets **one shared rollout index**. codex refuses to start until its
319
+ `state_<schema>.sqlite` has indexed every rollout under `$CODEX_HOME/sessions` (each file
320
+ read whole), so a private index per account meant one full scan of the shared tree per
321
+ account — 10 GB × 12 on a busy Mac — and any launch that met another process's unfinished
322
+ scan died after 30 s with "timed out waiting for state db backfill … (status: running)",
323
+ surfaced as the CLI's "local database appears to be damaged". A scan cut short (a probe with
324
+ a timeout, Ctrl-C) also left its 15-minute worker lease behind, keeping the account
325
+ unstartable long after its killer was gone. Since 2.0.27 `<acct>/state_N.sqlite` is a symlink
326
+ to `~/.codex/state_N.sqlite`, exactly like the tree it describes: SQLite resolves the link
327
+ before naming its `-wal`/`-shm` companions, so every process shares one lock set — the same
328
+ multi-process mode a single `CODEX_HOME` already runs in. The shim applies this at every
329
+ launch, and the index name the installed binary carries is linked ahead of time, so a codex
330
+ schema bump still costs one backfill per machine rather than one per account.
331
+
332
+ An account with **no index of its own** — every newly added one, and the case the change
333
+ exists for — is simply linked. Nothing is renamed and nothing about the shared index's state
334
+ can block it. An account that already has one is only ever moved while **nothing can open
335
+ it** (no `-wal` and no `-shm` beside it). codex holds the state database through an sqlx pool
336
+ whose connections are opened lazily and *by path* (`max_connections(5)`,
337
+ `create_if_missing(true)`), so renaming an index under a live holder would leave its first
338
+ connection on the old inode while every later one follows the new link — one process, two
339
+ databases. A codex killed mid-write leaves the pair behind too, and the next clean session on
340
+ that account removes it, so an account excluded this way rejoins on its own. Once it is
341
+ closed, the account's index is either **retired** beside the link as `*.private` (never
342
+ deleted, never overwritten — a second retirement gets its own suffix) when the home already
343
+ has one, or **promoted** into the home when it does not. The promote goes through `link(2)`,
344
+ which refuses an existing target: two shims racing to be the first to promote could otherwise
345
+ rename one's fresh symlink onto the file the other had just promoted, leaving the shared index
346
+ pointing at itself — `ELOOP`, every account on the machine unable to start, and repaired by
347
+ neither codex nor the shim.
348
+
349
+ Two things stop the sharing entirely. An account whose `sessions/` is a real directory keeps a
350
+ private index — the rule is structural, not a heuristic. And codex's own corruption recovery
351
+ has the last word: when it judges a database damaged it renames it (with its `-wal`/`-shm`)
352
+ into `<CODEX_HOME>/db-backups/sqlite-<ts>-<n>/` and rebuilds, which under a link renames the
353
+ *link*. A link sitting in that folder pointing at the shared file is codex's verdict on that
354
+ file, so the account keeps the index codex rebuilt for it and that name is left alone —
355
+ handing the link back would hand the damage back.
356
+
318
357
  **Auto-retry** (`-p`/`--print` only, default on, `CLAUDE_SHIM_RETRY=0` disables): on an
319
358
  auth- or rate-limit-looking failure the shim retries once on a different account and
320
359
  marks the failed one — a **10-minute cooldown** for a rate limit (it heals on its own),
@@ -327,149 +366,22 @@ and ≥2 accounts are eligible; service-spawned pipes take the plain exec path u
327
366
 
328
367
  ## Codex support (OpenAI Codex CLI)
329
368
 
330
- The exact same machinery, instantiated a second time for Codex. Everything documented
331
- above about the claude pool holds for the codex pool with these translations:
332
-
333
- | Claude side | Codex side |
334
- | --- | --- |
335
- | `bin/claude` shim, `claude-accounts` CLI | `bin/codex` shim, `codex-accounts` CLI |
336
- | pool `~/.claude-accounts` | pool `~/.codex-accounts` |
337
- | `CLAUDE_CONFIG_DIR` per-account dirs | `CODEX_HOME` per-account dirs |
338
- | `.credentials.json` / macOS Keychain item (OAuth, machine-local) | `auth.json` (ChatGPT OAuth, machine-local) |
339
- | `claude -p` auto-retry | `codex exec` auto-retry |
340
- | Anthropic OAuth usage and Fable buckets | Codex usage endpoint and per-model buckets |
341
- | `CLAUDE_*` pool controls | equivalent `CODEX_*` controls |
342
-
343
- Same selection rule (session gate first — `CODEX_MULTIACC_SESSION_GATE`, default 50 —
344
- then the 30-point weekly-headroom band with random spread, ≥90% any-bucket
345
- exclusion, peers rotate), same marker semantics (`.limited` cooldowns, `.expired` parks with
346
- credential/policy scoping and soft expiry), same fail-open guarantees, same sync
347
- safety guards. The `codex` shim engages the buffered auto-retry only for
348
- `codex exec` runs with finite stdin, exactly like `-p` on the claude side.
349
-
350
- Codex-specific notes:
351
-
352
- - **Auth is the Codex device-code sign-in by default**: `codex-accounts add` prints
353
- a URL + one-time code you can open in ANY browser (this machine, your laptop, a
354
- phone), so it works identically on a local Mac, over SSH, and on servers — there
355
- is no portable setup-token equivalent for Codex, so a server account is signed in
356
- ON the server the same way. `--browser` opts into the localhost browser-callback
357
- flow instead (only works when the browser runs on the same machine — the callback
358
- goes to `localhost:1455`). Either way the login runs with `CODEX_HOME` pointed at
359
- the account dir, and identity is verified offline from the id-token before
360
- anything is registered.
361
- - **`auth.json` is never synced** in either direction, for the same reason
362
- `.credentials.json` never is: the refresh token rotates, and two machines
363
- refreshing one grant strand each other. Sync pushes manifest + `config.toml`
364
- seeds + advisory limit state only.
365
- - **Windows are classified by length**, not by name: any window ≤6h counts as the
366
- self-healing session signal, anything longer as durable/weekly. A hard
367
- `limit_reached`/`allowed:false` verdict marks the account even if no window shows
368
- ≥90%, and a reshaped payload is still scanned recursively for window-shaped
369
- objects (fail open if nothing parses).
370
- - **Idle-account telemetry** stays fresh the same way: a long-expired access token is
371
- renewed via the OAuth refresh-token grant (`auth.openai.com/oauth/token`, the CLI's
372
- own public client id) and the rotated credential is persisted 0600. Overrides for
373
- tests: `CODEX_MULTIACC_TOKEN_URL`, `CODEX_MULTIACC_CLIENT_ID`,
374
- `CODEX_MULTIACC_USAGE_URL`.
375
- - **Earned usage resets are redeemed automatically at 95% used** (5% remaining) or
376
- when the backend reports the limit finished. The limits refresher checks the reset
377
- credits belonging to that same account, uses the soonest-expiring available credit,
378
- and writes a per-account/window idempotency key before redemption so a lost response
379
- or simultaneous fleet poll cannot spend a second reset. Set
380
- `CODEX_MULTIACC_AUTO_RESET=0` only for emergency rollback/testing.
381
- A finished-limit verdict takes precedence over a conflicting percentage. The
382
- redemption key excludes unused model windows whose reset clocks move between reads.
383
- Eligible accounts, including spent ones, check the credit endpoint every five
384
- minutes even if the usage summary still says zero credits. A newly granted reset
385
- is redeemed during that pass and clears the old limit; no-credit results are
386
- checked again next pass, without waiting for the weekly reset date.
387
- All authenticated accounts also report `usage.reset_credits_available` and
388
- `usage.reset_credits_fetched_at` (ISO 8601) in `list/status/limits --json`.
389
- The count comes from the credit endpoint, including below 95% used and with
390
- automatic redemption disabled. The scheduled usage cadence bounds these reads.
391
- Redemption shares that read, then reads back the remaining count after a POST;
392
- no subtraction or embedded usage summary substitutes for provider evidence.
393
- Zero means no available resets; missing fields mean the read was unavailable.
394
- Credit identifiers remain local and selection does not use these fields.
395
- - **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
396
- addon's no-API-keys rule.
397
-
398
- ```bash
399
- codex-accounts add # sign in a new ChatGPT account (device code —
400
- # open the URL in any browser, enter the code)
401
- codex-accounts add --browser # localhost browser-callback flow instead
402
- codex-accounts import <email> --id acct-01 && codex-accounts adopt acct-01
403
- # adopt this machine's existing ~/.codex login
404
- codex-accounts list | status | expired | relogin | verify | limits | health | sync
405
- claude-multiacc codex <cmd> # same commands via the npm wrapper
406
- ```
369
+ Both pools use the same selection policy. See [Codex account commands and behavior](docs/CODEX.md).
407
370
 
408
371
  ## Install / update / uninstall
409
372
 
410
- ### npm (recommended)
411
-
412
- ```bash
413
- npm install -g claude-multiacc # postinstall wires up the shim, CLI, and scheduled jobs
414
- # or run it once without installing globally:
415
- npx claude-multiacc
416
- ```
417
-
418
- `claude-multiacc` then wraps everything:
419
-
420
373
  ```bash
421
- claude-multiacc # install or update the addon (idempotent)
422
- claude-multiacc self-update # npm i -g @latest + re-install
423
- claude-multiacc uninstall [--purge-data]
424
- claude-multiacc list | status | add <email> | ... # passthrough to claude-accounts
374
+ npm install -g claude-multiacc@latest
375
+ codex-accounts verify
425
376
  ```
426
377
 
427
- **Automatic updates:** a globally-installed package re-runs `install.sh` from its own
428
- postinstall, so `npm i -g claude-multiacc@latest` fully updates the shim, CLI, and jobs.
429
- A daily job (launchd on macOS, cron on Linux, 04:07) runs `claude-accounts self-update`
430
- to pull the latest release on its own — `npm i -g @latest` for npm installs, `git pull +
431
- reinstall` for git checkouts; set `CLAUDE_MULTIACC_AUTOUPDATE=0` before install to
432
- opt out. `update-notifier` also prints a one-line nudge when a newer version is on npm.
433
- (Note: newer npm versions gate install scripts; if postinstall is blocked, just run
434
- `claude-multiacc install` once.)
435
-
436
- New versions publish to npm automatically from `main` via GitHub Actions
437
- (`.github/workflows/publish.yml`, OIDC Trusted Publishing — no long-lived token after the
438
- first bootstrap publish; `scripts/auto-version.mjs` bumps the patch above the last release).
439
-
440
- ### From a git checkout
441
-
442
- ```bash
443
- git clone <repo> && cd claude-multiacc && ./install.sh # macOS or Linux, idempotent
444
- ./install.sh --server root@1.2.3.4 # override sync target
445
- git pull && ./install.sh # update (data untouched)
446
- ./install.sh --uninstall # restores stock behavior, keeps account data
447
- ./install.sh --uninstall --purge-data # also deletes ~/.claude-accounts
448
- ```
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.
449
382
 
450
- `self-update` auto-detects which of these you used (npm global vs git checkout).
451
-
452
- **Servers / machines without repo access:** install via **npm** — the registry is
453
- public, so the daily self-update needs no credentials at all (no rsync, no keys).
454
- A git checkout self-updates too, but only if `git pull` can authenticate; for a
455
- private repo on a server that means a read-only deploy key (the 138 server runs
456
- this way: `core.sshCommand` pinned to its deploy key). A plain copied tree is the
457
- one layout that can NOT self-update — don't ship the addon that way.
458
-
459
- What install does (all reversible, nothing else):
460
-
461
- - **macOS:** marked PATH block at the END of `~/.zshenv`, `~/.zprofile`, `~/.zshrc`
462
- (+ bash rc files if present) — end-of-file placement matters because those files
463
- re-prepend `~/.local/bin`; launchd agents `com.claude-multiacc.limits` +
464
- `.codex-limits` (5m), `.health` + `.codex-health` (weekly Mon morning), and
465
- `.update` (daily 04:07). Notes when this Mac keeps Claude Code logins in the
466
- Keychain (the pool reads them; ssh sessions cannot — mint portable tokens for
467
- accounts that must work from everywhere).
468
- - **Linux (root):** PATH block in `~/.bashrc` + `/etc/profile.d/claude-multiacc.sh`,
469
- shim symlinks at `/usr/local/bin/claude` and `/usr/local/bin/codex` (shadow via
470
- PATH order — on the systemd default PATH too; the original binaries are untouched),
471
- cron entries for limits/health (both providers) + the daily auto-update.
472
- - 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.
473
385
 
474
386
  Account lifecycle, synchronization, fleet integration, environment switches,
475
387
  verification, troubleshooting, and security guidance continue in
@@ -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 the addon (npm i -g @latest, or git
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.sh first"; }
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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // The installer adds bin/ to PATH for git and npx installs too.
3
+ import './cli.mjs';
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 npm i -g claude-multiacc@latest, then re-install
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 to the latest npm release + re-install
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 <email> | login <acct-NN> |
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
- console.log('Updating claude-multiacc from npm…');
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 CHANGED
@@ -122,6 +122,127 @@ sel_log() {
122
122
  printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" 2>/dev/null >> "$ACC_ROOT/selection.log" || true
123
123
  }
124
124
 
125
+ # ---- one index for the shared session tree -----------------------------------------
126
+ # codex keeps a SQLite index of the rollout tree beside it (state_<schema>.sqlite) and
127
+ # refuses to start until that index has been BACKFILLED from every rollout under
128
+ # $CODEX_HOME/sessions — each file read whole. The installed layout shares one tree
129
+ # across every account (<acct>/sessions -> ~/.codex/sessions), so a private index per
130
+ # account meant one full scan of the whole tree per account: 10 GB × 12 on a busy Mac,
131
+ # minutes each, and a launch that met another process's unfinished scan died after 30 s
132
+ # with "timed out waiting for state db backfill … (status: running)" — surfaced as the
133
+ # CLI's damaged-database message. A scan cut short (a probe with a timeout, Ctrl-C) also
134
+ # left its 15-minute worker lease behind, so the account stayed unstartable long after
135
+ # its killer was gone (my-mini 2026-09-09: six fresh accounts, every launch refused).
136
+ #
137
+ # A shared tree gets ONE shared index: <acct>/state_N.sqlite is a symlink to the same
138
+ # file under ~/.codex, exactly like the tree it describes. SQLite resolves that symlink
139
+ # before naming its -wal/-shm companions, so every process shares one lock set — the
140
+ # multi-process mode a single CODEX_HOME already runs in. A private index that already
141
+ # exists and nothing holds open is retired beside the link (never deleted), or promoted
142
+ # into the home when the home has none yet; and the index the installed binary will
143
+ # create next is linked ahead of time, so a schema bump still costs one backfill, not one
144
+ # per account. Structural on purpose: a real (private) session tree keeps a private index.
145
+ # lib/common.sh carries the same function for seeding — keep the two in step.
146
+ # CORE — byte-identical in lib/common.sh; tests diff them.
147
+ state_index_names() { # $1 acct dir, $2 home dir -> the index file names either side holds
148
+ local f name seen=" "
149
+ for f in "$2"/state_[0-9]*.sqlite "$1"/state_[0-9]*.sqlite; do
150
+ [ -e "$f" ] || [ -L "$f" ] || continue # an unmatched glob is the pattern itself
151
+ name="${f##*/}"
152
+ case "$seen" in *" $name "*) continue ;; esac
153
+ seen="$seen$name "
154
+ printf '%s\n' "$name"
155
+ done
156
+ }
157
+
158
+ installed_state_index_name() { # the index file the installed binary creates; memoized per binary
159
+ local cache="$ACC_ROOT/.state-index" bin="$REAL" real id line name="" nat
160
+ # macOS `stat` reports a SYMLINK's own mtime, and codex is usually installed behind a
161
+ # stable launcher symlink — so the memo has to key on the file the link resolves to.
162
+ real="$(canon_path "$bin")"
163
+ id="$real:$(file_mtime "$real")"
164
+ if [ -f "$cache" ]; then
165
+ IFS= read -r line < "$cache" 2>/dev/null || line=""
166
+ case "$line" in "$id "*) printf '%s\n' "${line#"$id "}"; return 0 ;; esac
167
+ fi
168
+ # The npm launcher is a script; the schema name lives in the native binary vendored
169
+ # beside it. Try the launcher first (a bare binary answers directly), then the vendor.
170
+ for nat in "$bin" "$(dirname "$(canon_path "$bin")")"/../node_modules/@openai/codex-*/vendor/*/bin/codex; do
171
+ [ -f "$nat" ] || continue
172
+ name="$(LC_ALL=C grep -a -o -m1 'state_[0-9][0-9]*\.sqlite' "$nat" 2>/dev/null | head -1)"
173
+ [ -n "$name" ] && break
174
+ done
175
+ case "$name" in *[!A-Za-z0-9_.]*) name="" ;; esac
176
+ { printf '%s %s\n' "$id" "$name" > "$cache.$$" && mv -f "$cache.$$" "$cache"; } 2>/dev/null \
177
+ || rm -f "$cache.$$" 2>/dev/null
178
+ printf '%s\n' "$name"
179
+ }
180
+
181
+ # CORE — byte-identical in lib/common.sh; tests diff them.
182
+ shared_index_rejected() { # $1 acct dir, $2 index name, $3 shared target
183
+ # True when codex's own corruption recovery moved THIS account's link out of the way:
184
+ # it renames the database it judged damaged (and its -wal/-shm) into
185
+ # <CODEX_HOME>/db-backups/sqlite-<ts>-<n>/ and rebuilds, and under a link that renames
186
+ # the LINK. A link sitting in there pointing at the shared file is codex's verdict on
187
+ # that file, so this account keeps the index codex rebuilt for it and this name is
188
+ # left alone — handing the link back would hand the damage back.
189
+ local marker
190
+ for marker in "$1"/db-backups/*/"$2"; do
191
+ [ -L "$marker" ] || continue
192
+ [ "$(readlink "$marker" 2>/dev/null)" = "$3" ] && return 0
193
+ done
194
+ return 1
195
+ }
196
+
197
+ # CORE — byte-identical in lib/common.sh; tests diff them.
198
+ share_state_index_links() { # $1 acct dir, $2 home dir, $3 extra index name ('' for none)
199
+ local d="$1" home="$2" name names target link retired n
200
+ [ -L "$d" ] && return 0 # adopted: the dir IS the home
201
+ [ -L "$d/sessions" ] || return 0 # a private tree keeps its private index
202
+ [ "$(readlink "$d/sessions" 2>/dev/null)" = "$home/sessions" ] || return 0
203
+ [ -d "$home" ] || return 0
204
+ names="$(state_index_names "$d" "$home")"
205
+ case "${3:-}" in ''|*[!A-Za-z0-9_.]*) ;; *) names="$names $3" ;; esac
206
+ for name in $names; do
207
+ target="$home/$name"; link="$d/$name"
208
+ [ -L "$link" ] && continue # already shared (or pointed elsewhere on purpose)
209
+ shared_index_rejected "$d" "$name" "$target" && continue
210
+ if [ ! -e "$link" ]; then
211
+ ln -s "$target" "$link" 2>/dev/null || true # a new account: the whole point
212
+ continue
213
+ fi
214
+ # This account has an index of its own, and MOVING one that a process can still open
215
+ # is unsafe. codex holds the state database through an sqlx pool that opens its
216
+ # connections lazily and BY PATH (max_connections(5), create_if_missing(true) —
217
+ # codex-rs/state/src/sqlite.rs open_read_write_pool), so a rename under a live holder
218
+ # leaves connection 1 on the old inode while every connection the pool opens
219
+ # afterwards follows the new link: one process, two databases. A -wal or a -shm beside
220
+ # the file is that proof. A codex killed mid-write leaves them behind too, and the
221
+ # next clean session on the account removes them, so this heals itself in time.
222
+ { [ -e "$link-wal" ] || [ -e "$link-shm" ]; } && continue
223
+ if [ -e "$target" ]; then
224
+ retired="$link.private"; n=0 # never overwrite an earlier copy
225
+ while [ -e "$retired" ] && [ "$n" -lt 100 ]; do n=$((n+1)); retired="$link.private.$n"; done
226
+ [ -e "$retired" ] && continue
227
+ mv "$link" "$retired" 2>/dev/null || continue
228
+ else
229
+ # Atomic or nothing: link(2) refuses an existing target, so two shims racing to be
230
+ # the first to promote cannot rename one's fresh symlink onto the file the other
231
+ # just promoted — which is how a self-referential shared index (ELOOP, and every
232
+ # account on the Mac unable to start) could appear. A cross-device link simply
233
+ # fails and this account keeps its own index.
234
+ ln "$link" "$target" 2>/dev/null || continue
235
+ [ -L "$link" ] || rm -f "$link"
236
+ fi
237
+ ln -s "$target" "$link" 2>/dev/null || true
238
+ done
239
+ return 0
240
+ }
241
+
242
+ share_state_index() { # $1 acct dir — at launch: the home's index, plus the one this binary will create
243
+ share_state_index_links "$1" "${HOME:-/nonexistent}/.codex" "$(installed_state_index_name)"
244
+ }
245
+
125
246
  marker_active() { # true if $1/.limited is still in force; clears cleanly-expired markers
126
247
  # Parity with bin/claude's client_marker_recovered, by construction: this shim has NO
127
248
  # telemetry-based clearing path. A marker leaves here only when its OWN reset epoch has
@@ -521,6 +642,7 @@ if [ -n "${CODEX_ACCOUNT:-}" ]; then
521
642
  d="$ACC_ROOT/$CODEX_ACCOUNT"
522
643
  if [ -d "$d" ]; then
523
644
  sel_log "$CODEX_ACCOUNT pinned pwd=$PWD"
645
+ share_state_index "$d"
524
646
  export CODEX_HOME="$d"
525
647
  export CODEX_SHIM_ACTIVE=1
526
648
  exec "$REAL" "$@"
@@ -805,6 +927,7 @@ if [ "${CODEX_SHIM_RETRY:-1}" != "0" ] && [ "${#eligible[@]}" -ge 2 ]; then
805
927
  fi
806
928
  fi
807
929
 
930
+ share_state_index "$pick"
808
931
  if [ "$wants_retry" = "0" ]; then
809
932
  export CODEX_HOME="$pick"
810
933
  exec "$REAL" "$@"
@@ -906,6 +1029,7 @@ while :; do
906
1029
  if [ -n "$next" ]; then
907
1030
  sel_log "retry from=$(basename "$cur") to=$(basename "$next") rc=$rc"
908
1031
  cur="$next"
1032
+ share_state_index "$cur"
909
1033
  # The account that actually serves the work is the one the next run should rotate
910
1034
  # away from — not the one that bounced.
911
1035
  remember_pick "$cur"
@@ -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 the addon (npm i -g @latest, or git
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.sh first"; }
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
@@ -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
- ```bash
376
- tests/run-tests.sh # 645 sandboxed compatibility tests, no quota
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
+ ```