fix(remote-ssh): route claude/opencode/codex/gemini/antigravity through login shell

remain-on-exit (previous commit) preserved dead remote panes instead of
destroying them, which revealed the real failure: `exec claude`/`exec
opencode` ran under ssh's non-interactive, non-login remote-command
shell, which only sees sshd's minimal default PATH — not the ~/.zshrc
PATH entries where these CLIs actually live (e.g. ~/.local/bin,
~/.opencode/bin). Wrap them in `$SHELL -i -l -c '<cmd>'`, mirroring the
fix shell mode already had.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
timkjr
2026-08-04 16:41:27 -05:00
co-authored by Claude Sonnet 5
parent 474efd9023
commit e803186dfe
5 changed files with 33 additions and 12 deletions
+3 -1
View File
@@ -118,6 +118,8 @@ describe('Antigravity mode gates', () => {
it('has docker/remote default commands', () => {
expect(defaultDockerCommandForMode('antigravity')).toBe('exec agy');
expect(defaultRemoteCommandForMode('antigravity')).toBe('exec agy');
// Routed through an interactive login shell so per-user PATH entries resolve —
// same fix as the other remote agent CLIs (see defaultRemoteCommandForMode).
expect(defaultRemoteCommandForMode('antigravity')).toBe("exec $SHELL -i -l -c 'agy'");
});
});
+5 -2
View File
@@ -56,9 +56,12 @@ describe('remote-hosts domain', () => {
it('returns safe mode defaults and remote display values', () => {
expect(defaultRemoteCommandForMode('shell')).toBe('exec $SHELL -i -l');
expect(defaultRemoteCommandForMode('codex')).toBe('exec codex');
// Routed through an interactive login shell so per-user PATH entries (e.g.
// ~/.local/bin, ~/.opencode/bin) resolve — a bare `exec codex` sees only
// sshd's minimal default PATH and fails with "command not found".
expect(defaultRemoteCommandForMode('codex')).toBe("exec $SHELL -i -l -c 'codex'");
// Mirrors the local claude default so the remote agent runs non-interactively.
expect(defaultRemoteCommandForMode('claude')).toBe('exec claude --dangerously-skip-permissions');
expect(defaultRemoteCommandForMode('claude')).toBe("exec $SHELL -i -l -c 'claude --dangerously-skip-permissions'");
expect(remoteSshTarget({ id: 'h1', label: 'H1', host: 'box.local', username: 'aamer' })).toBe('aamer@box.local');
expect(remoteDisplayPath({ username: 'aamer', host: 'box.local', path: '/opt/work' })).toBe(
'aamer@box.local:/opt/work'
+7 -1
View File
@@ -147,7 +147,13 @@ describe('TmuxManager (unit)', () => {
remote: { hostId: 'gpu-box', label: 'GPU Box', host: '10.0.0.42', username: 'ubuntu', remotePath: '/w' },
sessionId: 'abc123def456',
});
expect(command).toContain('exec claude --dangerously-skip-permissions');
// Routed through an interactive login shell so ~/.local/bin (where `claude`
// typically lives) is on PATH — ssh's remote-command execution is neither
// interactive nor login, so a bare `exec claude` fails with "command not found".
// The inner quoting is escaped twice over (once per shellescape() layer), so
// assert on the unescaped substrings rather than the literal quoted form.
expect(command).toContain('exec $SHELL -i -l -c');
expect(command).toContain('claude --dangerously-skip-permissions');
});
});