diff --git a/src/remote-hosts.ts b/src/remote-hosts.ts index 54722c7b..04adff16 100644 --- a/src/remote-hosts.ts +++ b/src/remote-hosts.ts @@ -60,7 +60,11 @@ export async function writeRemoteCases(configDir: string, cases: RemoteCase[]): export function defaultRemoteCommandForMode(mode: SessionMode): string { const commands: Record = { - shell: 'exec bash -l', + // $SHELL, not a hardcoded bash: sshd sets it from the remote user's + // /etc/passwd entry, so this launches their actual login shell (zsh, + // fish, etc.). -i -l so it sources rc files (~/.zshrc etc.), matching + // the local shell-mode launch. + shell: 'exec $SHELL -i -l', // Mirror the LOCAL claude default so the remote agent runs non-interactively // (no trust-folder/permission prompt that nothing on the remote answers). The // per-host `commands.claude` override stays the escape hatch. diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index acfecb3a..e1697525 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -873,6 +873,14 @@ export function buildRemoteLaunchCommand(options: { // shared remote tmux server's other sessions keep their own prefix/mouse. const tmuxInvocation = [ `tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${shellescape(paneCommand)}`, + // Without this, tmux's default behavior destroys the pane -> window -> + // session (and, being the only session, the whole remote server) the + // instant paneCommand exits for ANY reason -- even something transient. + // That tears down the local ssh -t attach along with it (dead pane, + // status whatever ssh reported), and reconnect's `-A` then creates a + // fresh session with no trace of what actually happened. Set first, so + // it applies as early as possible after the pane starts. + `set -t ${remoteName} remain-on-exit on`, `set -t ${remoteName} status off`, `set -t ${remoteName} mouse off`, `set -t ${remoteName} prefix C-q`, diff --git a/test/remote-hosts.test.ts b/test/remote-hosts.test.ts index 863bcff2..7986d11e 100644 --- a/test/remote-hosts.test.ts +++ b/test/remote-hosts.test.ts @@ -55,7 +55,7 @@ describe('remote-hosts domain', () => { }); it('returns safe mode defaults and remote display values', () => { - expect(defaultRemoteCommandForMode('shell')).toBe('exec bash -l'); + expect(defaultRemoteCommandForMode('shell')).toBe('exec $SHELL -i -l'); expect(defaultRemoteCommandForMode('codex')).toBe('exec codex'); // Mirrors the local claude default so the remote agent runs non-interactively. expect(defaultRemoteCommandForMode('claude')).toBe('exec claude --dangerously-skip-permissions'); diff --git a/test/remote-ssh-options.test.ts b/test/remote-ssh-options.test.ts index 81dbeb44..978b1233 100644 --- a/test/remote-ssh-options.test.ts +++ b/test/remote-ssh-options.test.ts @@ -150,9 +150,10 @@ describe('COD-107 buildRemoteLaunchCommand — threads connection args', () => { const sh = (s: string) => "'" + s.replace(/'/g, "'\\''") + "'"; const remoteName = `codeman-ssh-${SESSION_ID.slice(0, 8)}`; const path = sh('/home/ubuntu/work'); - const paneCommand = `cd ${path} && exec bash -l`; + const paneCommand = `cd ${path} && exec $SHELL -i -l`; const tmuxInvocation = [ `tmux -L codeman-remote new-session -A -s ${remoteName} -c ${path} ${sh(paneCommand)}`, + `set -t ${remoteName} remain-on-exit on`, `set -t ${remoteName} status off`, `set -t ${remoteName} mouse off`, `set -t ${remoteName} prefix C-q`, diff --git a/test/tmux-manager.test.ts b/test/tmux-manager.test.ts index 72545fe4..16675b7d 100644 --- a/test/tmux-manager.test.ts +++ b/test/tmux-manager.test.ts @@ -137,7 +137,8 @@ describe('TmuxManager (unit)', () => { sessionId: 'abc123def456', }); - expect(command).toContain('exec bash -l'); + expect(command).toContain('exec $SHELL -i -l'); + expect(command).toContain('remain-on-exit on'); }); it('defaults claude to a non-interactive launch (--dangerously-skip-permissions)', () => {