From fc3b849a08310c9985cd67f919273c3f03df65e9 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Tue, 7 Jul 2026 21:03:35 +0200 Subject: [PATCH] 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. --- src/session/session.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/session/session.ts b/src/session/session.ts index 416b508..228ed71 100644 --- a/src/session/session.ts +++ b/src/session/session.ts @@ -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_ if it exists (restart survival) or creates it. + // attaches to web_ 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, {