claude-multiacc 2.0.31 → 2.0.33
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 -1
- package/bin/claude-accounts +24 -2
- package/docs/CODEX.md +12 -9
- package/docs/INSTALLATION.md +13 -4
- package/docs/VERIFICATION.md +13 -4
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.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/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/codex_config_edit.py +46 -25
- package/lib/codex_settings.py +3 -1
- package/lib/install_actions.sh +67 -13
- package/lib/shim_path.py +306 -0
- package/package.json +2 -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/run-tests.sh +114 -0
- package/tests/test_codex_config_edit.py +18 -2
- package/tests/test_codex_settings.py +19 -1
- package/tests/test_packaged_commands.py +39 -3
- package/tests/test_shim_path.py +285 -0
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ MAC (source of truth) SERVER (mirror)
|
|
|
59
59
|
server.token / .credentials.json
|
|
60
60
|
(or the login lives in the macOS Keychain — see below)
|
|
61
61
|
selection.log sync.log health.log /usr/local/bin/claude -> repo shim
|
|
62
|
-
repo bin/ first on PATH (rc
|
|
62
|
+
repo bin/ first on PATH (rc block + hook) /root/claude-multiacc/ (addon repo)
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
**Shim selection order** (identical file on both machines, `bin/claude`):
|
|
@@ -380,6 +380,16 @@ into its global bin directory, so account commands work in the current shell. Op
|
|
|
380
380
|
new shell for the `claude` and `codex` shims. If auto-setup was skipped, run
|
|
381
381
|
`claude-multiacc install` to initialize the pools and shell setup.
|
|
382
382
|
|
|
383
|
+
The shims shadow the real binaries by PATH order alone, so the installer's rc block
|
|
384
|
+
(`~/.zshenv`, `~/.zprofile`, `~/.zshrc`, `~/.profile` and the bash rc files when they
|
|
385
|
+
exist) ends with a prompt hook that puts the shim dir back in front before every
|
|
386
|
+
prompt: an rc file interrupted with Ctrl-C, or a tool that rewrites PATH, can no
|
|
387
|
+
longer leave `~/.local/bin/claude` first — that launch would run on the machine's own
|
|
388
|
+
`~/.claude` login and never reach the pool. `claude-accounts status` probes fresh
|
|
389
|
+
`zsh`/`bash`/`sh` login shells with your rc files and prints `BYPASSED -> <path>` for
|
|
390
|
+
any that still resolve something other than the shim; the weekly `health` check fails
|
|
391
|
+
on it.
|
|
392
|
+
|
|
383
393
|
See [installation, updates, removal, and command-not-found recovery](docs/INSTALLATION.md)
|
|
384
394
|
for npm, npx, git checkout, and server instructions.
|
|
385
395
|
|
package/bin/claude-accounts
CHANGED
|
@@ -23,7 +23,8 @@ claude-accounts — multi-account pool manager for claude-multiacc
|
|
|
23
23
|
|
|
24
24
|
USAGE
|
|
25
25
|
claude-accounts list [--json] brief account list (--json: machine-readable)
|
|
26
|
-
claude-accounts status [--json] full health: auth, per-bucket limits, markers
|
|
26
|
+
claude-accounts status [--json] full health: auth, per-bucket limits, markers,
|
|
27
|
+
and whether fresh login shells reach the shim
|
|
27
28
|
claude-accounts add [email] [--token] [--force]
|
|
28
29
|
login-FIRST: runs the full Claude Code login (`claude auth login` — the normal
|
|
29
30
|
browser sign-in, no long-lived-token step-up), then registers only after the
|
|
@@ -85,7 +86,7 @@ USAGE
|
|
|
85
86
|
persisted), so idle accounts keep fresh telemetry and stay selectable.
|
|
86
87
|
Skips accounts fetched in the last 45s and honors 429/refresh backoff;
|
|
87
88
|
--force ignores all three.
|
|
88
|
-
claude-accounts health limits + full verify; logs to health.log
|
|
89
|
+
claude-accounts health limits + full verify + shim-on-PATH probe; logs to health.log
|
|
89
90
|
claude-accounts self-update update this npm/git install; logs to update.log
|
|
90
91
|
claude-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
91
92
|
|
|
@@ -255,6 +256,18 @@ if dups:
|
|
|
255
256
|
PYEOF
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
# A fresh login shell that never reaches the shim is the one failure the pool cannot
|
|
260
|
+
# see from the inside — the launch simply happens elsewhere (my-mini 2026-09-17: an
|
|
261
|
+
# interrupted ~/.zshrc, and every `bash -l`, resolved the real binary and opened on the
|
|
262
|
+
# un-pooled ~/.claude login). lib/shim_path.py starts each login shell with this user's
|
|
263
|
+
# rc files and asks. Off with CLAUDE_MULTIACC_PATH_PROBE=0 — the sandboxed suites run
|
|
264
|
+
# under the operator's real HOME and must not depend on its rc files.
|
|
265
|
+
shim_path_report() {
|
|
266
|
+
# The module honours the kill switch itself and says so in its output, so a status
|
|
267
|
+
# or health.log read with the probe off never looks like a probe that passed.
|
|
268
|
+
"$PYBIN" "$LIB_DIR/shim_path.py" "$REPO_DIR"
|
|
269
|
+
}
|
|
270
|
+
|
|
258
271
|
cmd_status() {
|
|
259
272
|
require_manifest
|
|
260
273
|
local json=0
|
|
@@ -482,6 +495,8 @@ elif verdict == 'degraded':
|
|
|
482
495
|
print(" fix : claude-accounts limits --force # then, if it still fails:")
|
|
483
496
|
print(" claude-accounts login <acct-NN> # per account, on THIS machine")
|
|
484
497
|
PYEOF
|
|
498
|
+
echo
|
|
499
|
+
shim_path_report || true
|
|
485
500
|
}
|
|
486
501
|
|
|
487
502
|
cmd_add() {
|
|
@@ -2956,6 +2971,13 @@ cmd_health() {
|
|
|
2956
2971
|
# verify first: its real claude runs refresh any expired OAuth creds, so the
|
|
2957
2972
|
# limits pass that follows always has fresh bearers.
|
|
2958
2973
|
out="$( { echo "== verify =="; cmd_verify; echo; echo "== limits =="; cmd_limits; } 2>&1 )" || rc=1
|
|
2974
|
+
# The shim being reachable at all is part of the pool's health: a login shell that
|
|
2975
|
+
# resolves the real binary hands every `claude` typed there to ~/.claude, whatever
|
|
2976
|
+
# the accounts above say. Reported (and failed) here, where the weekly check pages.
|
|
2977
|
+
local shim_out
|
|
2978
|
+
shim_out="$( { echo; echo "== shim =="; shim_path_report; } 2>&1 )" || rc=1
|
|
2979
|
+
out="$out
|
|
2980
|
+
$shim_out"
|
|
2959
2981
|
printf '%s\n' "$out"
|
|
2960
2982
|
printf '%s health rc=%s\n%s\n' "$(ts_utc)" "$rc" "$out" >> "$ACC_ROOT/health.log"
|
|
2961
2983
|
if [ "$rc" -ne 0 ] && [ "$(machine_kind)" = "mac" ]; then
|
package/docs/CODEX.md
CHANGED
|
@@ -88,16 +88,19 @@ The addon does not choose delegation policy by default. To manage the concurrent
|
|
|
88
88
|
subagent ceiling across one Codex pool, explicitly configure it (Python 3.11+):
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
codex-accounts configure --max-subagents
|
|
92
|
-
codex-accounts configure --max-subagents
|
|
91
|
+
codex-accounts configure --max-subagents 7 --include-global --json
|
|
92
|
+
codex-accounts configure --max-subagents 7 --include-global --check --json
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
An explicitly selected modern Python is preserved. If the shell resolves an older
|
|
96
96
|
system Python, this command tries already installed Python 3.11+ executables on
|
|
97
97
|
PATH and in standard Homebrew locations, with bounded probes and no downloads.
|
|
98
98
|
|
|
99
|
-
The first command sets `
|
|
99
|
+
The first command sets `features.multi_agent_v2.max_concurrent_threads_per_session`
|
|
100
|
+
and the compatible `agents.max_concurrent_threads_per_session` in every manifest
|
|
100
101
|
account's `config.toml`, migrating its legacy `agents.max_threads` key when present.
|
|
102
|
+
An existing boolean `multi_agent_v2` becomes a table with the same `enabled` value;
|
|
103
|
+
an existing feature cap is updated along with the agents cap.
|
|
101
104
|
`--include-global` also updates the machine's `~/.codex/config.toml`; without it,
|
|
102
105
|
the global file is untouched. Unrelated models, instructions, MCP servers, project
|
|
103
106
|
trust and custom agents remain intact. Retired directories outside the manifest
|
|
@@ -112,16 +115,16 @@ fails sync instead of reporting success. Remote sync does not change the target'
|
|
|
112
115
|
global config. Without the policy file, existing settings keep their usual
|
|
113
116
|
copy-only-if-missing behavior and no new remote command runs.
|
|
114
117
|
|
|
115
|
-
`--check` never writes; it checks
|
|
116
|
-
and selected configs, returning exit 1 on drift. Without
|
|
117
|
-
command only reads the current pool policy and configuration. JSON reports contain
|
|
118
|
+
`--check` never writes; it checks both caps against the persisted policy
|
|
119
|
+
and requested value in selected configs, returning exit 1 on drift. Without
|
|
120
|
+
`--max-subagents`, the command only reads the current pool policy and configuration. JSON reports contain
|
|
118
121
|
the installed `plugin_version`, `max_subagents`, `policy_max_subagents`,
|
|
119
122
|
`accounts_total`, `accounts_configured`, `global_configured`, `mismatch_count` and
|
|
120
123
|
`verified`, without account identities, paths, credentials or arbitrary config.
|
|
121
124
|
|
|
122
|
-
Conflicting profile
|
|
125
|
+
Conflicting profile overrides are rejected.
|
|
123
126
|
Unusual inline-table layouts are also rejected before configuration changes;
|
|
124
|
-
use
|
|
125
|
-
validated before writes, each replacement is atomic, and repeating the command
|
|
127
|
+
use ordinary `[agents]` and `[features.multi_agent_v2]` tables for the managed settings.
|
|
128
|
+
All target configs are validated before writes, each replacement is atomic, and repeating the command
|
|
126
129
|
repairs an interrupted write. Existing running sessions retain the settings they
|
|
127
130
|
loaded; the policy applies to subsequent sessions.
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -78,14 +78,23 @@ one layout that can NOT self-update — don't ship the addon that way.
|
|
|
78
78
|
What install does (all reversible, nothing else):
|
|
79
79
|
|
|
80
80
|
- **macOS:** marked PATH block at the END of `~/.zshenv`, `~/.zprofile`, `~/.zshrc`
|
|
81
|
-
(+ bash rc files
|
|
82
|
-
re-prepend `~/.local/bin
|
|
81
|
+
(+ `~/.profile` and the bash rc files when they exist) — end-of-file placement
|
|
82
|
+
matters because those files re-prepend `~/.local/bin`. Every copy of the block also
|
|
83
|
+
registers a prompt hook (zsh `precmd` / bash `PROMPT_COMMAND`) that puts the shim
|
|
84
|
+
dir back in front before each prompt, so an rc file interrupted with Ctrl-C or a
|
|
85
|
+
tool that rewrites PATH (conda, nvm, a venv) cannot leave the real binary first at
|
|
86
|
+
the next prompt. `~/.profile` is what `bash -l` / `sh -l` read when there is no
|
|
87
|
+
`~/.bash_profile`, so a launcher that runs `bash -lc claude` reaches the pool too;
|
|
88
|
+
it is created when absent (a stock macOS account has none), and the installer never
|
|
89
|
+
creates `~/.bash_profile` (that would stop bash reading `~/.profile`). launchd agents `com.claude-multiacc.limits` +
|
|
83
90
|
`.codex-limits` (15m Claude / 5m Codex), `.health` + `.codex-health` (weekly Mon morning), and
|
|
84
91
|
`.update` (daily 04:07). Notes when this Mac keeps Claude Code logins in the
|
|
85
92
|
Keychain (the pool reads them; ssh sessions cannot — mint portable tokens for
|
|
86
93
|
accounts that must work from everywhere).
|
|
87
|
-
- **Linux (root):** PATH block in `~/.bashrc` + `/etc/profile.d/claude-multiacc.sh
|
|
88
|
-
|
|
94
|
+
- **Linux (root):** PATH block in `~/.bashrc` + `/etc/profile.d/claude-multiacc.sh`
|
|
95
|
+
(+ `~/.profile`, created when absent — Debian's own `~/.profile` prepends
|
|
96
|
+
`~/.local/bin` after `/etc/profile.d` has run, so the block has to end that file
|
|
97
|
+
too), shim symlinks at `/usr/local/bin/claude` and `/usr/local/bin/codex` (shadow via
|
|
89
98
|
PATH order — on the systemd default PATH too; the original binaries are untouched),
|
|
90
99
|
cron entries for limits/health (both providers) + the daily auto-update.
|
|
91
100
|
- Both: `~/.claude-accounts/` and `~/.codex-accounts/`, each with an `accounts.json` manifest.
|
package/docs/VERIFICATION.md
CHANGED
|
@@ -11,7 +11,8 @@ claude-accounts verify --quick # auth presence/expiry only, no inference
|
|
|
11
11
|
codex-accounts verify # real codex exec call per account
|
|
12
12
|
codex-accounts verify --quick # local Codex auth check, no inference
|
|
13
13
|
claude-accounts limits # live per-bucket usage incl. the Fable bucket
|
|
14
|
-
claude-accounts health # limits + full verify, logs to health.log, notifies on failure (Mac)
|
|
14
|
+
claude-accounts health # limits + full verify + shim-on-PATH probe, logs to health.log, notifies on failure (Mac)
|
|
15
|
+
python3 lib/shim_path.py "$(npm root -g)/claude-multiacc" # just the probe; exit 1 on a bypass
|
|
15
16
|
```
|
|
16
17
|
|
|
17
18
|
Verified end-to-end on both machines (2026-07-13): full matrix PASS, 20-invocation shim
|
|
@@ -22,9 +23,17 @@ repointed via `CLAUDE_BIN=/usr/local/bin/claude` and restarted healthy.
|
|
|
22
23
|
|
|
23
24
|
## Troubleshooting
|
|
24
25
|
|
|
25
|
-
- **`claude` resolves to the real binary, not the shim** —
|
|
26
|
-
|
|
27
|
-
(
|
|
26
|
+
- **`claude` resolves to the real binary, not the shim** — the launch then never
|
|
27
|
+
reaches the pool: the real client starts on the machine's own `~/.claude` login, and
|
|
28
|
+
from ssh/tmux (where that login sits in a locked Keychain) it opens on
|
|
29
|
+
"Not logged in · Please run /login" (my-mini, 2026-09-17). `claude-accounts status`
|
|
30
|
+
and the weekly `health` probe every login shell mode (`zsh -li`/`-lc`, `bash -li`/`-lc`,
|
|
31
|
+
`sh -lc`) with your own rc files and report `BYPASSED -> <path>` for any that resolve
|
|
32
|
+
something other than the shim. Fix: `claude-multiacc install` rewrites the rc blocks
|
|
33
|
+
(`~/.zshenv`, `~/.zprofile`, `~/.zshrc`, `~/.profile`, bash rc files) — each carries
|
|
34
|
+
a prompt hook that re-asserts the shim dir before every prompt — then open a new
|
|
35
|
+
shell; an already-open shell keeps the PATH it has. To check one shell by hand:
|
|
36
|
+
`zsh -lic 'whence -p claude'` / `bash -lc 'type -P claude'`.
|
|
28
37
|
- **An account never gets picked** — `claude-accounts status`: no auth on this machine,
|
|
29
38
|
a dead login (`selectable: NO`, see `claude-accounts expired`), or an active `.limited`
|
|
30
39
|
marker (shows bucket + minutes to reset).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/codex_config_edit.py
CHANGED
|
@@ -8,7 +8,6 @@ except ImportError:
|
|
|
8
8
|
raise SystemExit("Codex settings configuration requires Python 3.11 or newer") from None
|
|
9
9
|
|
|
10
10
|
KEY = "max_concurrent_threads_per_session"
|
|
11
|
-
ALIASES = (KEY, "max_threads")
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def limits(document):
|
|
@@ -33,39 +32,61 @@ def validate_overrides(document, value):
|
|
|
33
32
|
raise ValueError(f"{name}: profile overrides the requested subagent limit")
|
|
34
33
|
|
|
35
34
|
|
|
35
|
+
def dotted_pattern(parts):
|
|
36
|
+
return r'\s*\.\s*'.join(r'(?:' + re.escape(part) + r'|"' + re.escape(part)
|
|
37
|
+
+ r'"|\x27' + re.escape(part) + r'\x27)' for part in parts)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def edit_setting(source, table, key, literal, aliases=()):
|
|
41
|
+
"""Edit table/dotted assignments, or remove one when literal is None."""
|
|
42
|
+
headers = [re.compile(r'^\s*\[\s*' + dotted_pattern(table[:depth]) + r'\s*\]\s*(?:#.*)?$')
|
|
43
|
+
for depth in range(1, len(table) + 1)]
|
|
44
|
+
assignments = [re.compile(r'^\s*(?:' + '|'.join(dotted_pattern((*table[depth:], name))
|
|
45
|
+
for name in (key, *aliases)) + r')\s*=')
|
|
46
|
+
for depth in range(len(table) + 1)]
|
|
47
|
+
section, output, inserted = 0, [], False
|
|
48
|
+
for line in source.splitlines(keepends=True):
|
|
49
|
+
if line.lstrip().startswith("["):
|
|
50
|
+
section = next((depth for depth, header in enumerate(headers, 1)
|
|
51
|
+
if header.match(line.rstrip("\r\n"))), None)
|
|
52
|
+
if section == len(table) and literal is not None and not inserted:
|
|
53
|
+
output.extend([line.rstrip("\r\n") + "\n", f"{key} = {literal}\n"])
|
|
54
|
+
inserted = True
|
|
55
|
+
continue
|
|
56
|
+
if section is not None and assignments[section].match(line):
|
|
57
|
+
if literal is not None and not inserted:
|
|
58
|
+
output.append(f"{'.'.join((*table[section:], key))} = {literal}\n")
|
|
59
|
+
inserted = True
|
|
60
|
+
continue
|
|
61
|
+
output.append(line)
|
|
62
|
+
if literal is not None and not inserted:
|
|
63
|
+
output.extend([f"\n[{'.'.join(table)}]\n", f"{key} = {literal}\n"])
|
|
64
|
+
return "".join(output)
|
|
65
|
+
|
|
66
|
+
|
|
36
67
|
def updated_config(source, value):
|
|
37
|
-
"""
|
|
68
|
+
"""Set the v2 feature cap and agents fallback, preserving enablement and unrelated values."""
|
|
38
69
|
original = tomllib.loads(source)
|
|
39
|
-
validate_overrides(original, value)
|
|
70
|
+
validate_overrides({"profiles": original.get("profiles", {})}, value)
|
|
40
71
|
expected = copy.deepcopy(original)
|
|
41
72
|
agents = expected.setdefault("agents", {})
|
|
42
73
|
agents.pop("max_threads", None)
|
|
43
74
|
agents[KEY] = value
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
section = "other"
|
|
56
|
-
if (section == "agents" and assignment.match(line)) or (not section and dotted.match(line)):
|
|
57
|
-
continue
|
|
58
|
-
output.append(line)
|
|
59
|
-
if not inserted:
|
|
60
|
-
if any(dotted.match(line) for line in lines):
|
|
61
|
-
output.insert(0, f"agents.{KEY} = {value}\n")
|
|
62
|
-
else:
|
|
63
|
-
output.extend(["\n[agents]\n", f"{KEY} = {value}\n"])
|
|
64
|
-
result = "".join(output)
|
|
75
|
+
features = expected.setdefault("features", {})
|
|
76
|
+
feature = features.get("multi_agent_v2")
|
|
77
|
+
result = edit_setting(source, ("agents",), KEY, value, ("max_threads",))
|
|
78
|
+
if isinstance(feature, bool):
|
|
79
|
+
result = edit_setting(result, ("features",), "multi_agent_v2", None)
|
|
80
|
+
result = edit_setting(result, ("features", "multi_agent_v2"), "enabled", str(feature).lower())
|
|
81
|
+
features["multi_agent_v2"] = {"enabled": feature}
|
|
82
|
+
elif feature is None:
|
|
83
|
+
features["multi_agent_v2"] = {}
|
|
84
|
+
features["multi_agent_v2"][KEY] = value
|
|
85
|
+
result = edit_setting(result, ("features", "multi_agent_v2"), KEY, value)
|
|
65
86
|
try:
|
|
66
87
|
unchanged = tomllib.loads(result) == expected
|
|
67
88
|
except tomllib.TOMLDecodeError:
|
|
68
89
|
unchanged = False
|
|
69
90
|
if not unchanged:
|
|
70
|
-
raise ValueError("unsupported
|
|
91
|
+
raise ValueError("unsupported Codex TOML layout; use normal [agents] and [features.multi_agent_v2] tables")
|
|
71
92
|
return result
|
package/lib/codex_settings.py
CHANGED
|
@@ -92,7 +92,9 @@ def readback(paths, value):
|
|
|
92
92
|
result = {}
|
|
93
93
|
for path in paths:
|
|
94
94
|
document = tomllib.loads(path.read_text()) if path.exists() else {}
|
|
95
|
-
|
|
95
|
+
current = limits(document)
|
|
96
|
+
matches = (all(current[key] == value for key in ("configured", "feature_override"))
|
|
97
|
+
if value is not None else True)
|
|
96
98
|
try:
|
|
97
99
|
validate_overrides(document, value)
|
|
98
100
|
except ValueError:
|
package/lib/install_actions.sh
CHANGED
|
@@ -33,14 +33,48 @@ path_block_body() {
|
|
|
33
33
|
printf 'export PATH="%s/bin:$PATH"\n' "$REPO_DIR"
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
path_guard_body() {
|
|
37
|
+
# The two lines above win only while they are the LAST thing to touch PATH. A later
|
|
38
|
+
# rc line, a tool that rewrites PATH (conda, nvm, a venv) or an rc file that was
|
|
39
|
+
# INTERRUPTED before reaching them (a Ctrl-C at a slow `conda` hook, my-mini
|
|
40
|
+
# 2026-09-17) leaves ~/.local/bin in front again — and the next `claude` typed at
|
|
41
|
+
# that prompt runs the real binary on the machine's own ~/.claude login (or on no
|
|
42
|
+
# login at all), never reaching the pool. Nothing inside the shim can notice a launch
|
|
43
|
+
# that never reached it, so the rc block also registers a prompt hook (zsh precmd /
|
|
44
|
+
# bash PROMPT_COMMAND) that puts the shim dir back in front before EVERY prompt.
|
|
45
|
+
# Fork-free while PATH is already right, registered once, inert once the addon is
|
|
46
|
+
# gone. The array syntax is wrapped in eval so sh/dash — which read ~/.profile and
|
|
47
|
+
# /etc/profile.d as well — can parse the block.
|
|
48
|
+
printf '_claude_multiacc_path_guard() {\n'
|
|
49
|
+
printf ' [ -x "%s/bin/claude" ] || return 0\n' "$REPO_DIR"
|
|
50
|
+
printf ' case ":$PATH:" in ":%s/bin:"*) return 0 ;; esac\n' "$REPO_DIR"
|
|
51
|
+
printf ' PATH="$(printf %%s ":$PATH:" | sed '\''s|:%s/bin:|:|g; s|^:||; s|:$||'\'')"\n' "$REPO_DIR"
|
|
52
|
+
printf ' export PATH="%s/bin:$PATH"\n' "$REPO_DIR"
|
|
53
|
+
printf '}\n'
|
|
54
|
+
# Registered LAST on purpose, in both shells: a hook that runs after this one and
|
|
55
|
+
# rewrites PATH (direnv's, a venv's) would otherwise have the final say at the
|
|
56
|
+
# prompt. zsh removes any earlier registration and re-appends, so the copy of this
|
|
57
|
+
# block at the END of ~/.zshrc outranks hooks that ~/.zshrc registered before it;
|
|
58
|
+
# the filter is IFS-independent. bash appends once — its rc block is the last thing
|
|
59
|
+
# sourced, so an append lands last.
|
|
60
|
+
printf 'if [ -n "${ZSH_VERSION:-}" ]; then\n'
|
|
61
|
+
printf ' eval '\''precmd_functions=(${precmd_functions:#_claude_multiacc_path_guard} _claude_multiacc_path_guard)'\''\n'
|
|
62
|
+
printf 'elif [ -n "${BASH_VERSION:-}" ]; then\n'
|
|
63
|
+
printf ' case ";${PROMPT_COMMAND:-};" in *";_claude_multiacc_path_guard;"*) ;; *) PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND;}_claude_multiacc_path_guard" ;; esac\n'
|
|
64
|
+
printf 'fi\n'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
rc_block() { # the complete marked block, for every rc file and /etc/profile.d alike
|
|
68
|
+
printf '%s\n' "$MARK_BEGIN"
|
|
69
|
+
path_block_body
|
|
70
|
+
path_guard_body
|
|
71
|
+
printf '%s\n' "$MARK_END"
|
|
72
|
+
}
|
|
73
|
+
|
|
36
74
|
append_block() { # strip then append our PATH block to a file
|
|
37
75
|
local f="$1"
|
|
38
76
|
strip_block "$f" || return 1 # never create a second block next to a broken one
|
|
39
|
-
|
|
40
|
-
printf '%s\n' "$MARK_BEGIN"
|
|
41
|
-
path_block_body
|
|
42
|
-
printf '%s\n' "$MARK_END"
|
|
43
|
-
} >> "$f"
|
|
77
|
+
rc_block >> "$f"
|
|
44
78
|
}
|
|
45
79
|
|
|
46
80
|
do_uninstall() {
|
|
@@ -154,11 +188,33 @@ PYEOF
|
|
|
154
188
|
fi
|
|
155
189
|
}
|
|
156
190
|
|
|
191
|
+
# bash and sh read ~/.profile for a LOGIN shell when ~/.bash_profile and ~/.bash_login
|
|
192
|
+
# are absent — `bash -lc`, `sh -lc`, a tmux/ssh session whose login shell is bash. Left
|
|
193
|
+
# without a block, such a shell ends its startup with whatever /etc/profile and its own
|
|
194
|
+
# ~/.local/bin prepend put in front and never sees the shim (my-mini 2026-09-17: every
|
|
195
|
+
# `bash -l` resolved the real binary). The file is CREATED when absent — a stock macOS
|
|
196
|
+
# account has no ~/.profile at all, and without one the bash/sh modes stay bypassed and
|
|
197
|
+
# the shim probe fails `health` with a remedy that changes nothing — exactly as the
|
|
198
|
+
# installer already creates ~/.zshenv and ~/.zprofile. It never creates ~/.bash_profile:
|
|
199
|
+
# that file's mere existence stops bash reading ~/.profile.
|
|
200
|
+
install_profile_block() {
|
|
201
|
+
touch "$HOME/.profile" 2>/dev/null || {
|
|
202
|
+
echo " WARNING: cannot create $HOME/.profile — bash/sh login shells will not see the shim" >&2
|
|
203
|
+
return 0
|
|
204
|
+
}
|
|
205
|
+
if append_block "$HOME/.profile"; then
|
|
206
|
+
echo " PATH block: also at the end of ~/.profile (bash/sh login shells)"
|
|
207
|
+
else
|
|
208
|
+
echo " WARNING: ~/.profile was NOT updated (see above) — bash/sh login shells will not see the shim" >&2
|
|
209
|
+
fi
|
|
210
|
+
}
|
|
211
|
+
|
|
157
212
|
install_mac_paths() {
|
|
158
213
|
# .zshenv covers non-interactive zsh; the .zshrc block must be LAST so it wins
|
|
159
214
|
# over ~/.local/bin re-prepends done earlier in .zshrc/.zprofile.
|
|
160
215
|
# zsh reads: .zshenv always; .zprofile for login; .zshrc for interactive.
|
|
161
|
-
# The block must end each file that later re-prepends ~/.local/bin
|
|
216
|
+
# The block must end each file that later re-prepends ~/.local/bin, and every copy
|
|
217
|
+
# also carries the prompt hook (path_guard_body) for the rc-interrupted case.
|
|
162
218
|
if [ -z "$INSTANCE" ]; then
|
|
163
219
|
touch "$HOME/.zshenv"
|
|
164
220
|
append_block "$HOME/.zshenv"
|
|
@@ -168,7 +224,8 @@ install_mac_paths() {
|
|
|
168
224
|
append_block "$HOME/.zshrc"
|
|
169
225
|
[ -f "$HOME/.bash_profile" ] && append_block "$HOME/.bash_profile"
|
|
170
226
|
[ -f "$HOME/.bashrc" ] && append_block "$HOME/.bashrc"
|
|
171
|
-
echo " PATH block: end of ~/.zshenv, ~/.zprofile, ~/.zshrc (+ bash rc files if present)"
|
|
227
|
+
echo " PATH block: end of ~/.zshenv, ~/.zprofile, ~/.zshrc (+ bash rc files if present), with a prompt hook that re-asserts it"
|
|
228
|
+
install_profile_block
|
|
172
229
|
fi
|
|
173
230
|
}
|
|
174
231
|
|
|
@@ -177,13 +234,10 @@ install_linux_paths() {
|
|
|
177
234
|
touch "$HOME/.bashrc"
|
|
178
235
|
append_block "$HOME/.bashrc"
|
|
179
236
|
if [ -w /etc/profile.d ] 2>/dev/null || [ "$(id -u)" = "0" ]; then
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
path_block_body
|
|
183
|
-
printf '%s\n' "$MARK_END"
|
|
184
|
-
} > "$PROFILED"
|
|
185
|
-
echo " PATH block: ~/.bashrc + $PROFILED"
|
|
237
|
+
rc_block > "$PROFILED"
|
|
238
|
+
echo " PATH block: ~/.bashrc + $PROFILED (with a prompt hook that re-asserts it)"
|
|
186
239
|
fi
|
|
240
|
+
install_profile_block
|
|
187
241
|
fi
|
|
188
242
|
if [ -z "$INSTANCE" ] && [ "$(id -u)" = "0" ]; then
|
|
189
243
|
if [ -e /usr/local/bin/claude ] && [ ! -L /usr/local/bin/claude ]; then
|