diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 690020cd..1587b5ca 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -2121,13 +2121,19 @@ struct CMUXCLI { } if options.extraArguments.isEmpty { - // No explicit remote command provided: launch an interactive shell while prepending - // ~/.cmux/bin so `cmux` works in this SSH session without touching remote dotfiles. + // No explicit remote command provided: keep destination-only argv so Ghostty's + // ssh-terminfo bootstrap can safely append its own remote install command. + // Use RemoteCommand for session-local PATH bootstrap to make `cmux` available. if !hasSSHOptionKey(options.sshOptions, key: "RequestTTY") { parts.append("-tt") } + if !hasSSHOptionKey(options.sshOptions, key: "RemoteCommand") { + parts += [ + "-o", + "RemoteCommand=export PATH=\"$HOME/.cmux/bin:$PATH\"; exec \"${SHELL:-/bin/zsh}\" -l", + ] + } parts.append(options.destination) - parts.append("export PATH=\"$HOME/.cmux/bin:$PATH\"; exec \"${SHELL:-/bin/zsh}\" -l") } else { parts.append(options.destination) parts.append(contentsOf: options.extraArguments) diff --git a/tests_v2/test_ssh_remote_cli_metadata.py b/tests_v2/test_ssh_remote_cli_metadata.py index c540ff62..5ff706de 100644 --- a/tests_v2/test_ssh_remote_cli_metadata.py +++ b/tests_v2/test_ssh_remote_cli_metadata.py @@ -116,6 +116,10 @@ def main() -> int: _must("-o ControlMaster=auto" in ssh_command, f"ssh command should opt into connection reuse: {ssh_command!r}") _must("-o ControlPersist=600" in ssh_command, f"ssh command should keep master alive for reuse: {ssh_command!r}") _must("ControlPath=/tmp/cmux-ssh-" in ssh_command, f"ssh command should use shared control path template: {ssh_command!r}") + _must( + "RemoteCommand=export PATH=\"$HOME/.cmux/bin:$PATH\"; exec \"${SHELL:-/bin/zsh}\" -l" in ssh_command, + f"cmux ssh should use -o RemoteCommand for PATH bootstrap (not positional command): {ssh_command!r}", + ) listed_row = None deadline = time.time() + 8.0 diff --git a/tests_v2/test_ssh_remote_shell_integration.py b/tests_v2/test_ssh_remote_shell_integration.py index 38dd1710..325ece2f 100755 --- a/tests_v2/test_ssh_remote_shell_integration.py +++ b/tests_v2/test_ssh_remote_shell_integration.py @@ -246,6 +246,11 @@ def main() -> int: surfaces = client.list_surfaces(workspace_id) _must(bool(surfaces), f"workspace should have at least one surface: {workspace_id}") surface_id = surfaces[0][1] + terminal_text = client.read_terminal_text(surface_id) + _must( + "Reconstructed via infocmp" not in terminal_text, + "ssh-terminfo bootstrap should not leak raw infocmp output into the interactive shell", + ) term_value = _read_probe_payload(client, surface_id, "printf '%s' \"$TERM\"") terminfo_state = _read_probe_value(client, surface_id, "infocmp xterm-ghostty >/dev/null 2>&1")