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, {