fix(session): spawn PTY as login shell so hooks find nvm node

Spawn the shell with -l (POSIX only) so it loads the full profile chain
and rebuilds PATH. Without it, a shell spawned by a GUI-launched app or a
long-lived tmux keepalive server inherits a minimal PATH, so nvm-managed
node (only on the login PATH) is missing and Claude Code hooks fail with
'node: command not found'. PowerShell (Windows) has no -l; skipped there.

Applies to both the tmux new-session command and the direct-spawn path.
This commit is contained in:
Yaojia Wang
2026-07-07 21:03:35 +02:00
parent a24465623e
commit fc3b849a08

View File

@@ -81,10 +81,23 @@ export function createSession(
// event belongs to ($WEBTERM_SESSION) and where to POST ($WEBTERM_HOOK_URL, H2).
const tName = cfg.useTmux ? tmuxName(id) : null;
// Spawn the shell as a LOGIN shell (POSIX only) so it loads the full profile
// chain (/etc/zprofile, ~/.zprofile, ~/.zshrc, …) and rebuilds PATH — exactly
// like Terminal.app / iTerm. Without `-l`, a shell spawned by a GUI-launched
// app (Finder / login-item) or a long-lived tmux keepalive server inherits a
// minimal PATH, so nvm-managed `node` (only on the login PATH) is missing and
// Claude Code hooks fail with "node: command not found". PowerShell (Windows)
// has no `-l`; skip it there (Windows also forces tmux off — server-config.ts).
const loginArgs = process.platform === 'win32' ? [] : ['-l'];
// H1: under tmux, the node-pty process is a tmux CLIENT; `new-session -A`
// attaches to web_<id> if it exists (restart survival) or creates it.
// attaches to web_<id> if it exists (restart survival) or creates it. The
// shell-command (shellPath + login flag) is the tmux session's root process.
const file = tName !== null ? 'tmux' : cfg.shellPath;
const args = tName !== null ? ['new-session', '-A', '-s', tName, cfg.shellPath] : [];
const args =
tName !== null
? ['new-session', '-A', '-s', tName, cfg.shellPath, ...loginArgs]
: loginArgs;
// M4: let a spawn failure (e.g. missing shell / tmux) propagate synchronously.
const pty: IPty = spawn(file, args, {