fix(macos): scrub parent-session env, survive node upgrades and reboots, store lanes by real path (#22)
What broke (code audit, untested on a Mac): - Without systemd the server is always spawned from inside a Claude Code session, so it inherited CLAUDECODE, CLAUDE_CODE_SESSION_ID, CLAUDE_PLUGIN_ROOT, ... and tmux handed them to every lane pane (a codex pane even resolved as `claude` in `agenthub`'s harness check). - The ~/.local/bin/agenthub launcher pinned a versioned node path that `brew upgrade`/nvm deletes, leaving the CLI dead until the next bootstrap. - A discovery entry left over from before a reboot could name a recycled pid, so the bootstrap's fast path believed a dashboard was live and stopDashboard could signal an unrelated process. - Lanes were stored under the typed cwd; macOS reports /private/tmp and the on-disk case, so sessions never bound to a /tmp or differently-cased lane. What changed: - server/index.js deletes a DENYLIST of parent-session variables at startup; user config (CLAUDE_CONFIG_DIR, CLAUDE_CODE_USE_BEDROCK, ANTHROPIC_*) stays. - linkCli's launcher falls back to `node` on PATH when the pinned one is gone. - liveServers ignores entries whose startedAt predates this boot (60 s margin). - createLane and POST /api/lanes/ensure store/look up fs.realpathSync.native of an existing cwd; the liveness reaper also tries the realpath. Existing rows are untouched. - session-liveness gains an exec seam so the darwin ps+lsof branch is tested. - plugin-doctor names ~/.zshrc on darwin; INSTALL.md gains a macOS section with the Mac verification checklist, mirrored in both READMEs and PLUGINS.md. No launchd, no macOptionIsMeta, engines stays >=20.
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ agenthub lanes add --cwd /path/to/repo --title "My Feature"
|
||||
|
||||
The lane is now ready to track work. When you run a Claude Code session in that directory (or a subdirectory), the dashboard will bind the session to the lane automatically by matching the cwd.
|
||||
|
||||
**Note:** The directory must exist and be an absolute path. A lane with a cwd of `/tmp/wt` will NOT capture `/tmp/wt-sibling` (no partial prefix match) but WILL capture sessions in `/tmp/wt/subdir/` (subdirectories are owned by the parent lane unless a deeper lane exists).
|
||||
**Note:** The directory must exist and be an absolute path. A lane with a cwd of `/tmp/wt` will NOT capture `/tmp/wt-sibling` (no partial prefix match) but WILL capture sessions in `/tmp/wt/subdir/` (subdirectories are owned by the parent lane unless a deeper lane exists). A `cwd` that exists is stored by its real path (`fs.realpathSync.native`), so `/tmp/wt` on macOS is stored as `/private/tmp/wt` and a differently-cased path on a case-insensitive volume as the spelling on disk — the path a session reports. A path that does not exist yet keeps the string it was given; rows created before this rule keep theirs.
|
||||
|
||||
To provision a dashboard-managed git worktree instead of adopting an existing directory, use a source repository:
|
||||
|
||||
|
||||
+9
-3
@@ -128,13 +128,13 @@ check.
|
||||
milliseconds — the real work happens in a detached worker, so a session never
|
||||
waits on an install:
|
||||
|
||||
1. **Fast path** — recorded state matches this plugin build and a server is live → exit.
|
||||
1. **Fast path** — recorded state matches this plugin build and a server is live → exit. "Live" means a discovery entry whose pid answers AND whose `startedAt` is after this boot (60 s margin): an entry left over from before a reboot names a pid the OS may have handed to an unrelated process, and without systemd (macOS) nothing else clears it.
|
||||
2. **Superseded check** — if Claude Code records a DIFFERENT build of this plugin as installed, this one logs a line and does nothing else (see [A retired build never acts](#a-retired-build-never-acts)).
|
||||
3. **Node gate** — refuse below 22.5 with one line (see Prerequisites).
|
||||
4. **Lock** — atomic `mkdir` lock holding the PID; reclaimed when the owner is dead or the lock is older than 10 minutes, so two sessions cannot race the install or spawn two servers.
|
||||
5. **Install** — `npm install --omit=dev --ignore-scripts` into the runtime dir (never into the plugin cache, which is garbage-collected and replaced on every update). `--ignore-scripts` keeps the root `postinstall` from pulling the whole Vite client toolchain.
|
||||
6. **Legacy hook cleanup** — see above.
|
||||
7. **CLI** — write the `~/.local/bin/agenthub` launcher, never clobbering a `agenthub` the bootstrap did not write.
|
||||
7. **CLI** — write the `~/.local/bin/agenthub` launcher, never clobbering a `agenthub` the bootstrap did not write. It pins the node that ran the bootstrap and falls back to `node` on `PATH` when that binary is gone (a Homebrew Cellar or nvm path deleted by an upgrade).
|
||||
8. **UI build** — `client/` is copied into the runtime dir and built there (`npm ci --include=dev`, falling back to `npm install --include=dev`, then `npm run build`), landing in `runtime/client-dist` — the same directory the server serves from. `--include=dev` is required, not tidy: the entire build toolchain (`vite`, `typescript`, `@vitejs/plugin-react`, `tailwindcss`, `@types/*`) is a devDependency, and npm reads an inherited `NODE_ENV=production` as `--omit=dev`. Every bootstrap descended from the running server carries that env — the self-update chain, and any `SessionStart` fired by a `claude` inside a tmux pane the server started — so without the flag the install drops `vite` and the build dies on missing types. Skipped when a bundle for the current version already exists; a failure here does not fail the bootstrap (the API still works, and `/agenthub-open` can retry) but does mean `/run` 404s until it's fixed — and when a bundle from the PREVIOUS version is still on disk the failure is quieter still: the dashboard keeps serving the old UI against the new API, visible only as the Settings page's `UI build vX (rebuild client to match)` line.
|
||||
9. **Server** — on Linux with `systemd --user` available, install/refresh a
|
||||
`agenthub.service` unit (see [Auto-start on boot](#auto-start-on-boot)) instead
|
||||
@@ -235,7 +235,13 @@ server falls back to the old per-session detached spawn instead.
|
||||
|
||||
On macOS, Windows, or a Linux box without systemd, nothing changes: the
|
||||
server is a detached spawn started fresh on the next `SessionStart` after a
|
||||
reboot, same as always.
|
||||
reboot, same as always. That spawn inherits the Claude Code session's env, so
|
||||
`server/index.js` deletes the parent-session variables (`CLAUDECODE`,
|
||||
`CLAUDE_CODE_SESSION_ID`, `CLAUDE_CODE_MESSAGING_*`, `CLAUDE_PLUGIN_ROOT`, …)
|
||||
before anything else runs — otherwise tmux hands them to every lane pane. It is
|
||||
a denylist: user configuration such as `CLAUDE_CONFIG_DIR`,
|
||||
`CLAUDE_CODE_USE_BEDROCK` and `ANTHROPIC_*` passes through. There is no
|
||||
launchd agent; see [INSTALL.md § macOS](../INSTALL.md#macos).
|
||||
|
||||
### Who may stop the server
|
||||
|
||||
|
||||
Reference in New Issue
Block a user