claude-multiacc 2.0.58 → 2.0.60

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.
Files changed (151) hide show
  1. package/README.md +125 -4
  2. package/bin/claude +389 -94
  3. package/bin/claude-accounts +276 -42
  4. package/bin/codex +69 -5
  5. package/bin/codex-accounts +33 -4
  6. package/docs/ACCOUNT_OPERATIONS.md +62 -2
  7. package/docs/AUTORESUME-HANDOFF.md +28 -3
  8. package/docs/AUTORESUME.md +275 -18
  9. package/docs/CODEX.md +1 -0
  10. package/docs/INSTALLATION.md +7 -1
  11. package/docs/VERIFICATION.md +4 -0
  12. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  17. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  18. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  19. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  20. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  21. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  22. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  23. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  24. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  25. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  26. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  27. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  28. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  29. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  30. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  31. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  32. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  33. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  34. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  35. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  36. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  37. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  38. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  39. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  40. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  41. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  42. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  43. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  44. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  45. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  46. package/lib/audit.py +88 -22
  47. package/lib/autoresume.py +1197 -82
  48. package/lib/autoresume_cross.py +3 -1
  49. package/lib/autoresume_handoff.py +81 -9
  50. package/lib/autoresume_probe.py +2 -2
  51. package/lib/claude_client_error.py +9 -4
  52. package/lib/codex_trust.py +263 -0
  53. package/lib/common.sh +146 -3
  54. package/lib/credential.py +5 -2
  55. package/lib/install_actions.sh +19 -0
  56. package/lib/keychain.py +180 -36
  57. package/lib/mcp_oauth.py +1748 -0
  58. package/lib/mcp_registry.py +230 -8
  59. package/lib/model_catalog.json +404 -0
  60. package/lib/model_catalog.py +201 -0
  61. package/lib/model_match.py +104 -0
  62. package/lib/report.py +8 -5
  63. package/lib/session_settings.py +123 -0
  64. package/package.json +2 -2
  65. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  66. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  67. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  68. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  69. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  70. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  95. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  96. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  97. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  98. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  99. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  100. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  101. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  102. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  103. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  105. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  106. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  108. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  109. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  110. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  111. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  112. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  113. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  114. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  115. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  116. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  117. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  118. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  119. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  120. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  121. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  122. package/tests/fake_codex_tui.py +25 -0
  123. package/tests/fake_security.sh +24 -2
  124. package/tests/fixtures/cross_provider_cli.py +109 -0
  125. package/tests/run-tests.sh +1171 -23
  126. package/tests/test_autoresume.py +215 -8
  127. package/tests/test_autoresume_catalog.py +226 -0
  128. package/tests/test_autoresume_catalog_cli.py +185 -0
  129. package/tests/test_autoresume_codex_records.py +16 -0
  130. package/tests/test_autoresume_codex_trust.py +247 -0
  131. package/tests/test_autoresume_cross_guards.py +28 -0
  132. package/tests/test_autoresume_cross_tmux.py +27 -2
  133. package/tests/test_autoresume_cross_tokens.py +32 -2
  134. package/tests/test_autoresume_handoff_argv.py +120 -16
  135. package/tests/test_autoresume_handoff_origins.py +14 -0
  136. package/tests/test_autoresume_memory.py +1141 -0
  137. package/tests/test_autoresume_memory_tmux.py +94 -0
  138. package/tests/test_autoresume_probe.py +2 -1
  139. package/tests/test_autoresume_relaunch_settings.py +317 -0
  140. package/tests/test_autoresume_settings.py +135 -0
  141. package/tests/test_autoresume_sidechain.py +1518 -0
  142. package/tests/test_autoresume_sidechain_tmux.py +79 -0
  143. package/tests/test_autoresume_trust_tmux.py +63 -0
  144. package/tests/test_claude_client_error.py +10 -7
  145. package/tests/test_claude_org_block.py +1 -0
  146. package/tests/test_claude_pool_prepare.py +4 -1
  147. package/tests/test_claude_reset.py +3 -0
  148. package/tests/test_keychain_nologin.py +340 -0
  149. package/tests/test_mcp_oauth.py +1414 -0
  150. package/tests/test_mcp_registry.py +234 -3
  151. package/tests/test_packaged_commands.py +41 -0
package/bin/codex CHANGED
@@ -119,11 +119,60 @@ mcp_ensure() { # $1 acct dir — silent, never changes the exit status
119
119
  # snapshot taken just before). The real CLI's own parser and writer do the work; the shim
120
120
  # only copies the outcome. Exits with the real binary's status; a mirror failure is one
121
121
  # hint line. Returns 1 when this run is not such a command.
122
+ # The index of the first positional argument as the real CLI's parser (clap) finds it:
123
+ # global options may come first, in any number. The operator's `alias codex='codex
124
+ # --dangerously-bypass-approvals-and-sandbox'` puts one in front of every `codex mcp add`
125
+ # typed at a prompt, and the mirror once looked at $1 alone (bin/claude met the same on
126
+ # 2026-10-22). A value option skips its value (attached `-mX`/`--model=X` is one word); a
127
+ # variadic one every word up to the next option. `--`, or no positional: status 1.
128
+ # Options of codex-cli 0.163 (`codex --help`); an unknown one counts as a flag.
129
+ CLI_VALUE_OPTS=" -c --config --enable --disable --remote --remote-auth-token-env -m --model \
130
+ --local-provider -p --profile -s --sandbox -C --cd --add-dir -a --ask-for-approval "
131
+ CLI_VARIADIC_OPTS=" -i --image "
132
+ cli_positional_at() { # argv -> prints the index of its first positional argument
133
+ local i=1 a nx
134
+ while [ "$i" -le "$#" ]; do
135
+ a="${!i}"
136
+ case "$a" in
137
+ --) return 1 ;;
138
+ --*=*) i=$((i + 1)); continue ;;
139
+ -?*) ;;
140
+ *) printf '%s\n' "$i"; return 0 ;;
141
+ esac
142
+ i=$((i + 1))
143
+ case "$CLI_VALUE_OPTS" in *" $a "*) i=$((i + 1)); continue ;; esac
144
+ case "$CLI_VARIADIC_OPTS" in
145
+ *" $a "*)
146
+ while [ "$i" -le "$#" ]; do
147
+ nx="${!i}"; case "$nx" in -*) break ;; esac; i=$((i + 1))
148
+ done ;;
149
+ esac
150
+ done
151
+ return 1
152
+ }
153
+
154
+ # Whether argv is `codex [global options] mcp add|remove …`.
155
+ mcp_mutation_argv() {
156
+ local i j
157
+ i="$(cli_positional_at "$@")" || return 1
158
+ [ "${!i}" = "mcp" ] || return 1
159
+ j=$((i + 1))
160
+ # `mcp` takes options of its own before its subcommand: -c/--config, --enable, --disable
161
+ while :; do
162
+ case "${!j:-}" in
163
+ -c|--config|--enable|--disable) j=$((j + 2)) ;;
164
+ --*=*|-c?*) j=$((j + 1)) ;;
165
+ *) break ;;
166
+ esac
167
+ done
168
+ case "${!j:-}" in add|remove) return 0 ;; esac
169
+ return 1
170
+ }
171
+
122
172
  mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
123
173
  local d="$1" snap="" rc
124
174
  shift
125
- [ "${1:-}" = "mcp" ] || return 1
126
- case "${2:-}" in add|remove) ;; *) return 1 ;; esac
175
+ mcp_mutation_argv "$@" || return 1
127
176
  [ "${CODEX_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
128
177
  mcp_ensure "$d"
129
178
  snap="$(mktemp "${TMPDIR:-/tmp}/codex-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
@@ -156,8 +205,8 @@ mcp_nested_mirror() {
156
205
  [ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] && return 1
157
206
  [ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 1
158
207
  [ -n "${CODEX_HOME:-}" ] || return 1
159
- local verb="${1:-}" cfg="${CODEX_HOME%/}" d=""
160
- [ "$verb" = "mcp" ] || return 1
208
+ local cfg="${CODEX_HOME%/}" d=""
209
+ mcp_mutation_argv "$@" || return 1
161
210
  # A DIRECT child of the pool root, by its given spelling or its physical one (an
162
211
  # adopted account is a symlink whose target is the operator's ~/.claude).
163
212
  case "$cfg" in
@@ -766,6 +815,7 @@ AR_AVOID_CLAUDE="" AR_AVOID_CODEX="" AR_POOL_CLAUDE="" AR_POOL_CODEX=""
766
815
  AR_ARGV=()
767
816
  AR_ORIG_ARGV=()
768
817
  AR_NO_DAEMON=0
818
+ AR_TRUST=0 # only a consumed automatic relaunch trusts its cwd in the new account
769
819
  AR_ROLLOUT_CURSOR=""
770
820
  AR_TIER="" # which cut produced the pick: eligible | soft | hard (probe answer)
771
821
  AR_R_SID="" # the session a relaunch file named, for the dead-token hint
@@ -987,11 +1037,14 @@ ar_relaunch() { # $1 token
987
1037
  # Field 2 is the word `autoresume`, never an account id: lib/report.py and the
988
1038
  # *-accounts tools read an acct-NN in field 2 as a pick.
989
1039
  sel_log "autoresume relaunch chain=${AR_CHAIN:--} from=${acct:--} class=${cls:--} depth=$AR_DEPTH"
1040
+ AR_TRUST=1
990
1041
  return 0
991
1042
  }
992
1043
 
993
1044
  # The argv shapes a relaunch can rebuild as `resume <kept opts> <sid> <prompt>` — anything
994
1045
  # else (exec/e, -p profile, -c overrides, any subcommand, an unknown flag) gets no watcher.
1046
+ # The one -c a relaunch carries itself is let through: `-c model_reasoning_effort=<level>`,
1047
+ # beside `-m`, pins the session's own effort (lib/autoresume.py codex_argv_parse mirrors it).
995
1048
  # A positional must contain a space to count as a prompt: a single word may be a
996
1049
  # subcommand.
997
1050
  ar_argv_ok() { # the argv codex is about to get
@@ -1002,6 +1055,8 @@ ar_argv_ok() { # the argv codex is about to get
1002
1055
  sandbox:read-only) sandbox=1 ;;
1003
1056
  approval:on-request) approval=1 ;;
1004
1057
  model:*) case "$a" in ''|-*) return 1 ;; esac ;;
1058
+ effort:model_reasoning_effort=?*)
1059
+ case "${a#model_reasoning_effort=}" in *[!$AR_LOWER]*) return 1 ;; esac ;;
1005
1060
  *) return 1 ;;
1006
1061
  esac
1007
1062
  want=""
@@ -1013,6 +1068,7 @@ ar_argv_ok() { # the argv codex is about to get
1013
1068
  --sandbox) [ "$sandbox" = 0 ] || return 1; want=sandbox ;;
1014
1069
  --ask-for-approval) [ "$approval" = 0 ] || return 1; want=approval ;;
1015
1070
  -m|--model) want=model ;;
1071
+ -c) want=effort ;;
1016
1072
  --model=?*) ;;
1017
1073
  --last)
1018
1074
  [ "$resume" = 1 ] && [ "$sid" = 0 ] || return 1
@@ -1519,7 +1575,7 @@ export CODEX_SHIM_ACTIVE=1
1519
1575
 
1520
1576
  # A `codex mcp add|remove` is run as a child under the picked account and mirrored to
1521
1577
  # every other one (exits inside); anything else falls through to exec.
1522
- if [ "${1:-}" = "mcp" ]; then
1578
+ if mcp_mutation_argv "$@"; then
1523
1579
  share_state_index "$pick"
1524
1580
  export CODEX_HOME="$pick"
1525
1581
  mcp_mirror_run "$pick" "$@" || true
@@ -1554,6 +1610,14 @@ fi
1554
1610
  share_state_index "$pick"
1555
1611
  if [ "$wants_retry" = "0" ]; then
1556
1612
  export CODEX_HOME="$pick"
1613
+ # Folder access is account-local even when the conversation and cwd are unchanged.
1614
+ # Persist the relaunch's trust before Codex starts, without sending keys into its TUI.
1615
+ if [ "$AR_TRUST" = "1" ]; then
1616
+ if ! ar_find_python || ! "$AR_PY" -I "$SELF_DIR/../lib/codex_trust.py" \
1617
+ --acct-dir "$pick" --cwd "$PWD" </dev/null >/dev/null 2>&1; then
1618
+ sel_log "autoresume trust-failed chain=${AR_CHAIN:--} acct=${pick##*/}"
1619
+ fi
1620
+ fi
1557
1621
  # Auto-resume: a gated, detached watcher beside this exec — the exec itself (same pid,
1558
1622
  # same environment minus the auto-resume variables). Supported clients own their
1559
1623
  # task through --no-daemon, including the continuation passed on resume.
@@ -2,7 +2,7 @@
2
2
  # codex-accounts — manage the codex-multiacc account pool (OpenAI Codex CLI).
3
3
  # Subcommands: list status add import adopt dedupe remove login expired relogin
4
4
  # sync verify limits post-sync health self-update
5
- # export-credential import-credential
5
+ # export-credential import-credential model-catalog
6
6
  set -u
7
7
  # lib/codex_audit.py is imported by several subcommands; keep the install tree free
8
8
  # of __pycache__ (it may be root-owned, read-only, or an npm global prefix).
@@ -98,7 +98,14 @@ USAGE
98
98
  stay selectable. Skips accounts fetched in the last 45s and honors
99
99
  429/refresh backoff; --force ignores all three.
100
100
  codex-accounts health limits + full verify; logs to health.log
101
- codex-accounts self-update update this npm/git install; logs to update.log
101
+ codex-accounts self-update update this npm/git install (never to an older version); logs to update.log
102
+ codex-accounts model-catalog [refresh|show] [--if-stale] [--quiet]
103
+ the model/effort catalog auto-resume relaunches a session with. refresh (the default)
104
+ merges the catalog this release ships with the model caches the CLIs keep in their
105
+ account dirs (no request, no credential) into model-catalog.json in BOTH pool roots;
106
+ every install and self-update runs it, and each limits pass refreshes its own pool's
107
+ copy once it is an hour old. --if-stale leaves a younger copy alone; --quiet prints
108
+ nothing. show prints the merged catalog.
102
109
  codex-accounts post-sync (server side) seed dirs, fix perms, quick verify
103
110
  codex-accounts mcp add <name> [--provider claude|codex|both] [--scope user]
104
111
  [-e KEY=VAL]... [-H 'Header: v']... [--transport stdio|http]
@@ -139,6 +146,8 @@ ENV
139
146
  CODEX_MULTIACC_DISABLE=1 bypass the shim entirely
140
147
  CODEX_MULTIACC_AUTO_RESET=0 disable automatic usage-reset redemption
141
148
  CODEX_MULTIACC_MCP=0 disable the MCP registry (no reconcile at seed/launch, no mirror)
149
+ CODEX_MULTIACC_MODEL_CATALOG=0 never refresh the codex pool's model-catalog.json
150
+ (MULTIACC_MODEL_CATALOG=0: neither pool's)
142
151
  EOF
143
152
  }
144
153
 
@@ -1837,6 +1846,10 @@ PYEOF
1837
1846
  # lock while a consumer reads its output.
1838
1847
  codex_limits_lock_release
1839
1848
  trap - EXIT
1849
+ # Every account's MCP servers, adopting what one holds that the registry does not know.
1850
+ mcp_registry_pass
1851
+ # This pool's model catalog, once its copy is an hour old (lib/common.sh).
1852
+ model_catalog_backstop
1840
1853
  # The refresher fails open per account; a NON-zero status means the pass itself
1841
1854
  # broke (unreadable manifest, dead python). Report the state anyway — stale data
1842
1855
  # beats silence — but hand the caller the failure, exactly as before --json existed.
@@ -2251,6 +2264,10 @@ cmd_mcp() {
2251
2264
  local args=()
2252
2265
  case "$sub" in
2253
2266
  add|add-json|remove|list|apply|import-local) shift ;;
2267
+ # Sharing MCP OAuth logins across accounts is a claude-pool feature (lib/mcp_oauth.py):
2268
+ # Codex keeps an MCP server's OAuth state in its own per-account store.
2269
+ auth-status|auth-sync|auth-forget)
2270
+ die "mcp $sub is not supported for the codex pool — MCP OAuth logins are shared across the claude pool only (claude-accounts mcp $sub)" ;;
2254
2271
  "") usage; return 1 ;;
2255
2272
  *) die "unknown mcp subcommand: $sub (add, add-json, remove, list, apply, import-local)" ;;
2256
2273
  esac
@@ -2369,7 +2386,17 @@ cmd_self_update() {
2369
2386
  ulog "self-update: already latest ($cur)"
2370
2387
  return 0
2371
2388
  fi
2372
- ulog "self-update: npm $cur -> ${lat:-latest} (prefix $prefix, npm $npm_bin)"
2389
+ # Only ever UP, to a version known to be newer (claude-accounts says why): the same
2390
+ # package, so either CLI could otherwise install `latest` over a newer hand install.
2391
+ case "$lat" in [0-9]*.*[0-9]) ;; *)
2392
+ ulog "self-update: could not read the registry's version (npm view); not updating"
2393
+ return 1 ;;
2394
+ esac
2395
+ if ver_newer "$cur" "$lat"; then
2396
+ ulog "self-update: this install ($cur) is newer than the registry's $lat; not downgrading"
2397
+ return 0
2398
+ fi
2399
+ ulog "self-update: npm $cur -> $lat (prefix $prefix, npm $npm_bin)"
2373
2400
  if PATH="$npm_path" "$npm_bin" install -g --prefix "$prefix" claude-multiacc@latest \
2374
2401
  >>"$ACC_ROOT/update.log" 2>&1; then
2375
2402
  after="$(pkg_version_at "$REPO_DIR")"
@@ -2378,6 +2405,7 @@ cmd_self_update() {
2378
2405
  return 1
2379
2406
  fi
2380
2407
  ulog "self-update: npm update ok ($after)"
2408
+ model_catalog_after_update "$quiet"
2381
2409
  else
2382
2410
  ulog "self-update: npm update FAILED (see update.log)"
2383
2411
  return 1
@@ -2441,7 +2469,7 @@ PYEOF
2441
2469
  # distribution for as long as the arg loops rejected the flag. An UNKNOWN verb must still
2442
2470
  # FAIL, or the probe stops meaning what it says — so this list must hold exactly the verbs
2443
2471
  # the dispatcher below implements, and a test pins that both ways.
2444
- _KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp"
2472
+ _KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp model-catalog"
2445
2473
  case " $_KNOWN_VERBS " in
2446
2474
  *" ${1:-help} "*)
2447
2475
  # ...but never past a literal `--`: from there on the words belong to an MCP
@@ -2473,6 +2501,7 @@ case "${1:-help}" in
2473
2501
  self-update) shift; cmd_self_update "$@" ;;
2474
2502
  init-pool) shift; cmd_init_pool "$@" ;;
2475
2503
  mcp) shift; cmd_mcp "$@" ;;
2504
+ model-catalog) shift; cmd_model_catalog "$@" ;;
2476
2505
  help|--help|-h) usage ;;
2477
2506
  *) usage; exit 1 ;;
2478
2507
  esac
@@ -59,7 +59,9 @@ claude-accounts adopt acct-01
59
59
  `adopt` makes the account dir a **symlink to `~/.claude`** — the machine's existing
60
60
  login joins the pool with a single credential file (no copy of the OAuth grant exists
61
61
  anywhere else, so refreshes can never race). This is how the two current accounts run:
62
- acct-01 = the Mac's login, acct-02 = the server's login (adopted server-side).
62
+ acct-01 = the Mac's login, acct-02 = the server's login (adopted server-side). It refuses
63
+ an account dir that already holds a credential of its own — any `.credentials.json`, even
64
+ one with only MCP server sign-ins in it, or a `server.token` — rather than delete it.
63
65
 
64
66
  ### Add a fresh account (~1 min, one browser sign-in)
65
67
 
@@ -212,6 +214,62 @@ is applied on the Mac and never synced. `--help` on `mcp` lists every option;
212
214
  `CLAUDE_MULTIACC_MCP=0` / `CODEX_MULTIACC_MCP=0` stop the reconcile at seed and launch and the
213
215
  shim's mirror (the CLI, sync and fan-out keep working).
214
216
 
217
+ ### MCP OAuth logins (per machine, shared by every account)
218
+
219
+ The registry carries a server's CONFIG; an OAuth server's LOGIN lives in each account's
220
+ credential store (`mcpOAuth` in `.credentials.json`, or the account's Keychain item). The
221
+ claude pool shares those logins across every account on the machine through
222
+ `<pool>/mcp-oauth.json` (lib/mcp_oauth.py): authenticate a server once in any account
223
+ (`/mcp` in a session) and every account has it from its next launch. It goes into the store
224
+ each account already has — its `.credentials.json`, or its Keychain item — and an account that
225
+ runs on its setup token with no file gets one created, holding the MCP logins alone (0600):
226
+ in an ssh/tmux session that file is all its client reads them from, and it is no Claude login
227
+ to the shim, the limits pass, the audit or verify (`list` keeps showing `auth=token`). No file
228
+ is created beside a Keychain login, in a parked account (`.expired`) or an adopted dir.
229
+
230
+ ```bash
231
+ claude-accounts mcp auth-status # server, expiry, source account, refresh state, per-account stores
232
+ claude-accounts mcp auth-sync # what the limits pass does every 15 minutes, now
233
+ claude-accounts mcp auth-sync --force # ...retrying a refresh that is backing off
234
+ claude-accounts mcp auth-forget higgsfield # deliberate logout, every account on this machine
235
+ ```
236
+
237
+ What to expect, and how to read `auth-status`:
238
+
239
+ - **fresh / stale / own / stub / missing / absent** per account file or Keychain item. `stale`
240
+ (a provably older copy), `stub` and `missing` turn `fresh` at that account's next launch (the
241
+ shim merges before exec) or at the next limits pass; `own` is a login of another grant whose
242
+ age the expiries cannot settle — left where it is; `locked` means the item exists but this
243
+ session (ssh, tmux) cannot open it — a Terminal on the Mac, or the scheduled pass, reaches it.
244
+ An `absent` file turns `fresh` only for a setup-token account (the file is created for it);
245
+ an absent Keychain item is never created.
246
+ - **NOT shared: …** / **sharing paused: …**: one login in many accounts needs ONE refresher —
247
+ the pool, ahead of every client — so nothing is shared with
248
+ `CLAUDE_MULTIACC_MCP_OAUTH_REFRESH=0`, by a launch when no refreshing limits pass ran in the
249
+ last hour (`mcp auth-sync` runs one now), or for a login inside the clients' own 5-minute
250
+ window, refused by its server, or whose failing refresh would next try too late. Each
251
+ account keeps what it holds meanwhile.
252
+ - **NEEDS RE-AUTH**: the authorization server refused the refresh token. Sign in again in any
253
+ one account (`/mcp`); the new login replaces the old one everywhere. A backoff (`N
254
+ failure(s), last http 503`) retries by itself, 15 minutes doubling to 6 hours; `refresh: due
255
+ … — deferred: a copy sits in acct-NN keychain` waits for a pass that can open that item,
256
+ until 20 minutes before expiry.
257
+ - **Never synced**: each Mac keeps its own grants. A runner Mac, a replica and the source
258
+ each sign in once; `sync` and the limits fan-out push explicit file lists without it.
259
+ - **Logging out**: a `/mcp` "Clear authentication", `claude mcp login <server>` or a
260
+ re-authentication in ANY one account revokes the shared login at the server, so the pool
261
+ retires it in every account (`login: none — retired … (cleared in acct-NN file)`); one
262
+ sign-in in any account shares the new one. `mcp auth-forget NAME` is the same logout from
263
+ the operator's side. `codex-accounts mcp auth-*` is not supported (Codex keeps MCP OAuth in
264
+ its own store).
265
+ - **Rolling it out**: the first share runs at install time (the limits agent starts on
266
+ install), and a build older than this one reads a shared MCP-only file as a live login:
267
+ every setup-token account would then say "Not logged in". Install it only where no older
268
+ build can come back: from the registry once it is published, or by hand at or above the
269
+ registry's version (npm `self-update` never installs an older one). Until then, hold the
270
+ sharing with `CLAUDE_MULTIACC_MCP_OAUTH=0` (`launchctl setenv` before the install for the
271
+ scheduled agents).
272
+
215
273
  ### Pointing sync somewhere else (or nowhere)
216
274
 
217
275
  The target is no longer baked in. In precedence order:
@@ -332,7 +390,7 @@ emitted for diagnostics.
332
390
  | Credential | Where it lives | Class | Can it be copied to another machine? |
333
391
  | --- | --- | --- | --- |
334
392
  | claude setup-token | `<acct>/server.token` | **portable** | **Yes**; the fleet-distributed credential |
335
- | claude OAuth login | `<acct>/.credentials.json`, or a macOS Keychain item | machine-local | **No**; refresh tokens rotate |
393
+ | claude OAuth login | `<acct>/.credentials.json`, or a macOS Keychain item, carrying `claudeAiOauth` (MCP OAuth state alone is no login) | machine-local | **No**; refresh tokens rotate |
336
394
  | codex ChatGPT login | `<acct>/auth.json` | machine-local | **No**; Codex has no portable credential |
337
395
 
338
396
  Non-portable accounts are not a dead end: they show up as `credential_class:
@@ -422,6 +480,8 @@ derived from the pool root. Uninstalling an instance removes only that instance'
422
480
  | `CLAUDE_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY=<s>` | five-hour recovery grace; default 300s (weekly never clears early) |
423
481
  | `CLAUDE_SHIM_RETRY=0` | disable the `-p` auto-retry |
424
482
  | `CLAUDE_MULTIACC_AUTORESUME=0` | no [auto-resume](AUTORESUME.md) watcher for launches from this environment. `touch <pool>/autoresume.off` also stops running watchers |
483
+ | `CLAUDE_MULTIACC_MCP_OAUTH=0` | stop sharing MCP OAuth logins across accounts (no merge before exec, no sync in the limits pass); `auth-status` / `auth-forget` still run |
484
+ | `CLAUDE_MULTIACC_MCP_OAUTH_REFRESH=0` | never refresh MCP OAuth logins — and so share none: a login in many accounts needs ONE refresher, or their clients refresh one rotating grant at once (each account keeps what it holds) |
425
485
  | `CLAUDE_ACCOUNTS_ROOT=...` | relocate the pool; legacy spelling is `CLAUDE_ACCOUNTS_DIR` |
426
486
  | `CLAUDE_MULTIACC_SYNC_TARGET=...` | sync target, overriding the manifest; `none` = local-only |
427
487
  | `CLAUDE_MULTIACC_SYNC_ROOT=... / _SYNC_REPO=...` | remote pool root / addon repo that goes with it |
@@ -42,9 +42,34 @@ Account avoidance is separate for each provider, so Claude's `acct-01` and Codex
42
42
 
43
43
  ## Permissions and arguments
44
44
 
45
- The working directory is preserved. Source model names, effort settings, session
46
- selectors and the original launch prompt are not replayed as destination CLI arguments.
47
- The destination uses its own configured model.
45
+ The working directory is preserved. Session selectors, the original launch prompt and the
46
+ source's own model and effort flags are not replayed as destination CLI arguments. The
47
+ destination starts on the model and effort of the **same level** instead. Both are read from
48
+ the validated source history, up to its snapshot boundary, and matched through the
49
+ [model catalog](../README.md#auto-resume):
50
+
51
+ - **Models match by level** (operator's map, 2026-10-14):
52
+
53
+ | Codex source | Claude destination | | Claude source | Codex destination |
54
+ |---|---|---|---|---|
55
+ | astra | opus | | fable, mythos | astra |
56
+ | sol | sonnet | | opus | astra |
57
+ | terra | haiku | | sonnet | sol |
58
+ | luna | haiku | | haiku | terra |
59
+
60
+ The catalog encodes it as levels: Claude haiku 2, sonnet 3, opus 4, fable 5 (mythos counts
61
+ as fable); Codex luna 1, terra 2, sol 3, astra 4. Equal levels match; a level with no
62
+ family on the other side (luna, fable) takes the nearest one, the higher on a tie. The
63
+ match is that family's newest listed model, whatever its version (launches are staggered,
64
+ so a family a release behind is still its own level). A retiring model counts as the model
65
+ the catalog upgrades it to.
66
+ - **Efforts match by name.** low, medium, high, xhigh and max carry over unchanged. Codex's
67
+ `ultra` becomes `max`, and `minimal` or `none` becomes `low`. The result is clamped to the
68
+ destination model: the nearest supported level below, else its lowest.
69
+ - **Unknown means the destination's default.** A model or effort the history does not
70
+ record, or the catalog cannot place, is left out, and the destination CLI applies its own
71
+ default. Claude takes either flag alone. Codex takes both or neither, with the matched
72
+ model's default effort when only the model is known.
48
73
 
49
74
  An explicit permission bypass maps to the equivalent destination bypass. Merely allowing
50
75
  a bypass flag is not treated as enabling it. Ordinary launches use explicit conservative