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 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-file block) /root/claude-multiacc/ (addon repo)
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
 
@@ -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 8 --include-global --json
92
- codex-accounts configure --max-subagents 8 --include-global --check --json
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 `agents.max_concurrent_threads_per_session` in every manifest
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 the requested value against the persisted policy
116
- and selected configs, returning exit 1 on drift. Without `--max-subagents`, the
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 or `features.multi_agent_v2` object overrides are rejected.
125
+ Conflicting profile overrides are rejected.
123
126
  Unusual inline-table layouts are also rejected before configuration changes;
124
- use an ordinary `[agents]` table for the managed setting. All target configs are
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.
@@ -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 if present) — end-of-file placement matters because those files
82
- re-prepend `~/.local/bin`; launchd agents `com.claude-multiacc.limits` +
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
- shim symlinks at `/usr/local/bin/claude` and `/usr/local/bin/codex` (shadow via
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.
@@ -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** — open a new shell, or check
26
- that the marked block is the LAST PATH manipulation in your rc file
27
- (`grep -A2 'claude-multiacc >>>' ~/.zshrc`).
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).
@@ -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
- """Edit ordinary [agents] or dotted keys, then prove no unrelated TOML value changed."""
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
- lines, section, output, inserted = source.splitlines(keepends=True), "", [], False
45
- header = re.compile(r'^\s*\[\s*(?:agents|"agents"|\x27agents\x27)\s*\]\s*(?:#.*)?$')
46
- assignment = re.compile(r'^\s*(?:"|\x27)?(?:' + '|'.join(ALIASES) + r')(?:"|\x27)?\s*=')
47
- dotted = re.compile(r'^\s*agents\s*\.\s*(?:' + '|'.join(ALIASES) + r')\s*=')
48
- for line in lines:
49
- if header.match(line.rstrip("\r\n")):
50
- section = "agents"
51
- output.extend([line.rstrip("\r\n") + "\n", f"{KEY} = {value}\n"])
52
- inserted = True
53
- continue
54
- if line.lstrip().startswith("["):
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 agents TOML layout; use a normal [agents] table before configuring")
91
+ raise ValueError("unsupported Codex TOML layout; use normal [agents] and [features.multi_agent_v2] tables")
71
92
  return result
@@ -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
- matches = limits(document)["effective"] == value if value is not None else True
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:
@@ -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
- printf '%s\n' "$MARK_BEGIN"
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