Skip to content

Fix cmux ssh against hosts configured with RemoteCommand/RequestTTY - #7359

Merged
austinywang merged 3 commits into
mainfrom
issue-7246-ssh-remotecommand-requesttty
Jul 5, 2026
Merged

austinywang merged 3 commits into
mainfrom
issue-7246-ssh-remotecommand-requesttty

Conversation

@austinywang

@austinywang austinywang commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Problem

cmux ssh dev-host fails and loops the reconnect banner when the host alias is configured for interactive logins:

Host dev-host
  HostName example.internal
  User root
  RequestTTY yes
  RemoteCommand sudo su -
Cannot execute command-line and remote command.

[cmux] ssh exited with status 255; reconnecting (attempt 1/20).

OpenSSH refuses a command-line remote command while the host config sets RemoteCommand (reproducible outside cmux: ssh dev-host true → same fatal, exit 255). cmux-controlled invocations pass their own remote commands — the default cmux ssh flow dies on its very first hop, the foreground-auth ssh … <dest> true.

Fix

New shared CmuxFoundation.SSHHostConfiguredRemoteCommand (-o RemoteCommand=none, OpenSSH ≥ 7.6 — macOS has shipped newer clients since 10.13.2), applied inside every builder that appends a cmux-supplied remote command:

  • CLI cmux ssh: foreground-auth hop, bootstrap installer hop, and the cmux ssh <dest> -- <command> passthrough branch. Inserted right after ssh, so under OpenSSH's first-value-per-option rule it also wins over caller-supplied options. The session hop keeps carrying cmux's own -o RemoteCommand=<bootstrap> (which already overrides the config), and bare interactive invocations (VM attach) are untouched, so hosts without a configured RemoteCommand behave identically.
  • App restore/reattach: SSHPTYAttachStartupCommandBuilder foreground auth.
  • Coordinator batch plumbing (bootstrap probes/install, bootstrap-TTY read, port scans, upload cleanup, relay metadata, stale-listener cleanup) via sshCommonArguments(batchMode:), which now also pins -o RequestTTY=no so a host RequestTTY force cannot CRLF-corrupt parsed pipes.
  • cmuxd stdio transport: WorkspaceRemoteConfiguration.daemonTransportArguments (this is what the persistent remote PTY rides, so PTY attach works on these hosts).
  • ssh-tmux stack via RemoteTmuxHost.sshControlArguments (interactive auth, tmux -CC control mode — which keeps its forced -tt — and one-shot discovery/mutation commands).
  • File explorer listing, remote git status, drag-drop upload cleanup argv builders.

Invocations with no remote command (-N forwards, -O control ops, -G config dumps, plain interactive shells) are unchanged. scp/sftp already pass -oRemoteCommand=none themselves since OpenSSH 7.6.

Two-commit structure (regression test policy)

  1. 58843b1 — regression tests only, expected red:
    • cmuxTests/SSHConfiguredRemoteCommandHostTests: end-to-end cmux ssh startup scripts (both the persistent-PTY foreground-auth flow and the bootstrap-install flow) executed against a fake ssh that mirrors OpenSSH's real rule (positional command + no RemoteCommand override → the exact fatal + exit 255; first -o RemoteCommand wins; none clears), plus the app-side foreground-auth argv.
    • cmuxTests/RemoteTmuxHostRemoteCommandOverrideTests, CmuxCoreTests (daemon transport), CmuxRemoteSessionTests (coordinator batch argv + option ordering).
  2. second commit — the fix, plus updates to argv-pinning tests (WorkspaceRemoteConfigurationSSHBatchCommandsTests exact arrays; the CLIRemoteShellStartupPerformanceTests fake ssh now models first-wins/none-clears), and a GitStatusProvider.fetchStatusSSH argv test.

Verification

  • swift test for CmuxCore (8/8) and CmuxRemoteSession (42/42) — red on commit 1 for the new suites, green with the fix.
  • The end-to-end scenario was reproduced with the release CLI before writing the Swift tests: the generated default-flow startup script fails with the exact reported output against a RemoteCommand-simulating fake ssh, and completes exit 0 (auth → ssh-pty-attach bridge handoff) once the override is present.
  • OpenSSH semantics (fatal, none clearing, first-value-wins, -N exemption) verified empirically against OpenSSH 10.2 with a synthetic ssh_config.
  • No user-facing strings added or changed (argv/wire behavior only), so no localization updates are required.

Fixes #7246

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Fixes cmux ssh failing on hosts that set RemoteCommand/RequestTTY by clearing the host RemoteCommand when cmux sends its own command and disabling TTY for batch ops to prevent reconnect loops and CRLF issues. Applies across default, bootstrap, tmux, daemon, file explorer, and git status flows.

  • Bug Fixes

    • Add -o RemoteCommand=none via CmuxFoundation SSHHostConfiguredRemoteCommand to all cmux-controlled ssh calls that pass a command (foreground auth, bootstrap installer, passthrough, daemon transport, coordinator batch, ssh-tmux, file explorer, remote git status); place it before the destination and ahead of user -o options.
    • Keep cmux’s own -o RemoteCommand= on the session hop; interactive shells and -N/-O/-G remain unchanged.
    • Pin -o RequestTTY=no for batch/parseable commands to avoid PTY CRLF corruption and ensure first-wins over caller-configured options.
    • Add regression tests for default and bootstrap flows, ssh-tmux, daemon transport, coordinator batch argv/order, file explorer git status, and updated fake-ssh semantics (first-wins, none clears).
  • Refactors

    • Make SSHHostConfiguredRemoteCommand an instantiable struct (per package conventions) and update call sites; no behavior change.

Written for commit 759c48c. Summary will update on new commits.

Review in cubic

…and/RequestTTY

cmux ssh against a host alias whose ssh_config sets `RequestTTY yes` and
`RemoteCommand sudo su -` exits 255 with OpenSSH's "Cannot execute
command-line and remote command." and loops the reconnect banner
(issue #7246): every cmux-controlled invocation that supplies its own
remote command (foreground auth `true`, bootstrap installer hop, daemon
stdio transport, coordinator batch plumbing, ssh-tmux control commands)
inherits the host RemoteCommand instead of overriding it.

Covers, all red without the fix:
- cmuxTests/SSHConfiguredRemoteCommandHostTests: end-to-end `cmux ssh`
  startup scripts (persistent-PTY foreground-auth flow and bootstrap
  install flow) against a fake ssh that mirrors OpenSSH's rule, plus the
  app-side SSHPTYAttachStartupCommandBuilder foreground auth argv.
- cmuxTests/RemoteTmuxHostRemoteCommandOverrideTests: shared ssh-tmux
  control args, interactive auth, and tmux -CC control-mode argv.
- CmuxCoreTests: daemonTransportArguments (cmuxd stdio transport).
- CmuxRemoteSessionTests: coordinator batch exec argv (port scan) and
  override/RequestTTY ordering ahead of caller-configured options.

Part 1 of 2 (test-only, expected red); the fix lands separately so CI
proves these tests catch the bug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 5, 2026 8:18am
cmux-staging Building Building Preview, Comment Jul 5, 2026 8:18am

@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@austinywang, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e30ec84c-8e75-49e6-a9b4-f5ef49b1eca0

📥 Commits

Reviewing files that changed from the base of the PR and between 9c91710 and 759c48c.

📒 Files selected for processing (17)
  • CLI/CMUXCLI+SSHCommandSupport.swift
  • CLI/cmux.swift
  • Packages/macOS/CmuxCore/Sources/CmuxCore/Remote/WorkspaceRemoteConfiguration+SSHBatchCommands.swift
  • Packages/macOS/CmuxCore/Tests/CmuxCoreTests/WorkspaceRemoteConfigurationSSHBatchCommandsTests.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHHostConfiguredRemoteCommand.swift
  • Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+SSHArguments.swift
  • Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionSSHRemoteCommandOverrideTests.swift
  • Sources/FileExplorerStore.swift
  • Sources/GitStatusProvider.swift
  • Sources/RemoteTmuxHost.swift
  • Sources/SSHPTYAttachStartupCommandBuilder.swift
  • Sources/TerminalSSHSessionDetector.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CLIRemoteShellStartupPerformanceTests.swift
  • cmuxTests/FileExplorerGitStatusProviderTests.swift
  • cmuxTests/RemoteTmuxHostRemoteCommandOverrideTests.swift
  • cmuxTests/SSHConfiguredRemoteCommandHostTests.swift
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7246-ssh-remotecommand-requesttty

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a crash loop where cmux ssh <host> fails with "Cannot execute command-line and remote command." (exit 255) when the SSH host alias is configured with RemoteCommand and/or RequestTTY in ~/.ssh/config. The fix introduces SSHHostConfiguredRemoteCommand in CmuxFoundation and inserts -o RemoteCommand=none into every cmux-controlled ssh invocation that appends its own positional command, relying on OpenSSH's first-value-per-option rule.

  • New shared type SSHHostConfiguredRemoteCommand in CmuxFoundation exposes the -o RemoteCommand=none argv fragment; consumed across seven distinct SSH argv builders (CLI auth/bootstrap/passthrough, daemon transport, coordinator batch plumbing, ssh-tmux control, file explorer, git status, upload cleanup).
  • RequestTTY=no also pinned for batch/pipe-parsed commands in sshCommonArguments(batchMode:) to prevent CRLF corruption from a host RequestTTY force config.
  • Comprehensive regression tests added for all patched flows, using a fake ssh that models OpenSSH's first-value-wins and none-clears semantics.

Confidence Score: 5/5

Safe to merge — the fix is well-scoped, thoroughly tested with fake-ssh harnesses that model real OpenSSH semantics, and does not touch interactive or -N/-O/-G invocations.

The change is purely additive argv surgery on cold SSH process-launch paths. Every patched builder is covered by new regression tests using a fake ssh that enforces first-value-wins and none-clears. Interactive and non-command invocations are explicitly excluded and untouched. The one inconsistency (override placement after user options in SSHPTYAttachStartupCommandBuilder) only matters for a user who has RemoteCommand in their cmux SSH settings rather than in ~/.ssh/config, and does not regress the reported bug scenario.

Sources/SSHPTYAttachStartupCommandBuilder.swift — override is appended after the user-options loop rather than before it, unlike every other patched site.

Important Files Changed

Filename Overview
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHHostConfiguredRemoteCommand.swift New shared value type providing the -o RemoteCommand=none argv fragment; well-documented, Sendable, no stored state.
CLI/CMUXCLI+SSHCommandSupport.swift Adds sshArgumentsOverridingHostRemoteCommand helper that inserts the override right after ssh, winning over all subsequent options; used correctly in the three CLI flows.
Sources/SSHPTYAttachStartupCommandBuilder.swift Override added after the user-options loop, unlike every other patched site; fixes the primary ssh_config case but placement is inconsistent with the stated first-wins goal.
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+SSHArguments.swift Override and RequestTTY=no correctly inserted in the batchMode block before user SSH options; order guarantees both overrides win over caller configuration.
Packages/macOS/CmuxCore/Sources/CmuxCore/Remote/WorkspaceRemoteConfiguration+SSHBatchCommands.swift Override correctly prepended before batchSSHArguments() in daemonTransportArguments, fixing the persistent PTY path.
Sources/RemoteTmuxHost.swift Override placed first in sshControlArguments, before all other options, correctly guarding tmux mirror and discovery commands.
Sources/GitStatusProvider.swift Override prepended before user-supplied port/identity/options, correctly guarding the remote git status command.
Sources/FileExplorerStore.swift Override prepended before connection.sshOptions, correctly first in the argv fragment for file explorer SSH commands.
Sources/TerminalSSHSessionDetector.swift Override placed right after -T, before all user SSH options, correctly guarding upload-cleanup and related commands.
cmuxTests/SSHConfiguredRemoteCommandHostTests.swift End-to-end tests using a fake ssh that mirrors OpenSSH first-value-wins and none-clears semantics; covers both default PTY-attach flow and bootstrap-install flow.
Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionSSHRemoteCommandOverrideTests.swift Coordinator batch argv tests verify override and RequestTTY=no appear before the destination across port-scan, relay-metadata, and upload-cleanup commands.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant cmux
    participant SSH as ssh binary
    participant Config as ~/.ssh/config
    participant Host as Remote Host

    Note over User,Host: Before fix – host has RemoteCommand in ssh_config
    User->>cmux: cmux ssh dev-host
    cmux->>SSH: ssh [options] dev-host true
    SSH->>Config: "read RemoteCommand=sudo su -"
    SSH-->>cmux: fatal: Cannot execute command-line and remote command. (exit 255)
    cmux-->>User: reconnect loop (1/20)

    Note over User,Host: After fix – -o RemoteCommand=none inserted first
    User->>cmux: cmux ssh dev-host
    cmux->>SSH: "ssh -o RemoteCommand=none [options] dev-host true"
    SSH->>Config: "read RemoteCommand=sudo su - (overridden by first -o)"
    SSH->>Host: exec true (exit 0 – auth hop succeeds)
    cmux->>SSH: "ssh [session -o RemoteCommand=bootstrap] dev-host"
    SSH->>Host: exec bootstrap (persistent PTY attached)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant cmux
    participant SSH as ssh binary
    participant Config as ~/.ssh/config
    participant Host as Remote Host

    Note over User,Host: Before fix – host has RemoteCommand in ssh_config
    User->>cmux: cmux ssh dev-host
    cmux->>SSH: ssh [options] dev-host true
    SSH->>Config: "read RemoteCommand=sudo su -"
    SSH-->>cmux: fatal: Cannot execute command-line and remote command. (exit 255)
    cmux-->>User: reconnect loop (1/20)

    Note over User,Host: After fix – -o RemoteCommand=none inserted first
    User->>cmux: cmux ssh dev-host
    cmux->>SSH: "ssh -o RemoteCommand=none [options] dev-host true"
    SSH->>Config: "read RemoteCommand=sudo su - (overridden by first -o)"
    SSH->>Host: exec true (exit 0 – auth hop succeeds)
    cmux->>SSH: "ssh [session -o RemoteCommand=bootstrap] dev-host"
    SSH->>Host: exec bootstrap (persistent PTY attached)
Loading

Reviews (3): Last reviewed commit: "Make SSHHostConfiguredRemoteCommand an i..." | Re-trigger Greptile

Comment on lines +259 to +264
XCTAssertFalse(startupResult.timedOut, startupResult.stderr)
XCTAssertFalse(
startupResult.stderr.contains("Cannot execute command-line and remote command."),
"The bootstrap installer hop must override a host-configured RemoteCommand; stderr: \(startupResult.stderr)"
)
XCTAssertEqual(startupResult.status, 0, startupResult.stderr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The bootstrap-install test checks for the "Cannot execute command-line and remote command." string but omits the reconnect-banner assertion ([cmux] ssh exited with status) that the default-flow test above uses to confirm no retry loop was entered. A fatal exit 255 on the installer hop would still produce the banner, and the missing check would leave that failure mode silent.

Suggested change
XCTAssertFalse(startupResult.timedOut, startupResult.stderr)
XCTAssertFalse(
startupResult.stderr.contains("Cannot execute command-line and remote command."),
"The bootstrap installer hop must override a host-configured RemoteCommand; stderr: \(startupResult.stderr)"
)
XCTAssertEqual(startupResult.status, 0, startupResult.stderr)
XCTAssertFalse(startupResult.timedOut, startupResult.stderr)
XCTAssertFalse(
startupResult.stderr.contains("Cannot execute command-line and remote command."),
"The bootstrap installer hop must override a host-configured RemoteCommand; stderr: \(startupResult.stderr)"
)
XCTAssertFalse(
startupResult.stderr.contains("[cmux] ssh exited with status"),
startupResult.stderr
)
XCTAssertEqual(startupResult.status, 0, startupResult.stderr)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — applied. The bootstrap-install test now also asserts the reconnect banner ([cmux] ssh exited with status) is absent, matching the default-flow test; it lands with the fix commit so the red commit stays as-pushed while CI records the failure.

austinywang and others added 2 commits July 4, 2026 16:54
…ions

Fixes `cmux ssh` (and every other cmux-built ssh exec) against host
aliases whose ssh_config sets `RemoteCommand` (typically with
`RequestTTY yes`): OpenSSH refuses a command-line remote command while a
configured RemoteCommand is in effect ("Cannot execute command-line and
remote command.", exit 255), so the foreground auth hop died before the
session ever started and the pane looped reconnect attempts
(issue #7246).

New shared CmuxFoundation constant `SSHHostConfiguredRemoteCommand`
(`-o RemoteCommand=none`, OpenSSH >= 7.6 — macOS has shipped newer
clients since 10.13.2) applied at every builder that appends its own
remote command:

- CLI `cmux ssh`: foreground-auth hop, bootstrap installer hop, and the
  `cmux ssh <dest> -- <command>` passthrough branch (inserted right
  after `ssh`, so it also wins over caller-supplied options under
  OpenSSH's first-value-per-option rule). The interactive session hop
  keeps carrying cmux's own `-o RemoteCommand=<bootstrap>`, which
  already overrides the host config; bare interactive invocations (VM
  attach) are untouched.
- App restore/reattach: SSHPTYAttachStartupCommandBuilder foreground
  auth.
- Coordinator batch plumbing (bootstrap probes/install, BootstrapTTY,
  port scans, upload cleanup, relay metadata, stale-listener cleanup):
  sshCommonArguments(batchMode:) now also pins `-o RequestTTY=no` so a
  host `RequestTTY force` cannot CRLF-corrupt parsed pipes.
- CmuxCore daemonTransportArguments (cmuxd stdio transport).
- ssh-tmux stack via RemoteTmuxHost.sshControlArguments (interactive
  auth, `tmux -CC` control mode — which keeps its forced `-tt` — and
  one-shot discovery/mutation commands).
- File explorer listing, remote git status, and drag-drop upload
  cleanup argv builders.

Invocations with no remote command (`-N` forwards, `-O` control ops,
`-G` config dumps, plain interactive shells) are unchanged, and hosts
without a configured RemoteCommand see identical behavior — the
override is inert there.

The CLIRemoteShellStartupPerformanceTests fake ssh now mirrors
OpenSSH's real RemoteCommand semantics (first value wins, `none`
clears) so the installer hop's new override falls through to the
positional command exactly like real ssh.

Fixes #7246

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e conventions

The package-conventions lint forbids all-static public namespace types in
packages; follow the SSHAgentSocketResolver pattern (public struct with a
public initializer) and access the override via an instance at every call
site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@austinywang
austinywang merged commit b3e791c into main Jul 5, 2026
33 of 35 checks passed
azooz2003-bit added a commit that referenced this pull request Jul 6, 2026
… ssh fixes)

Notable: #7393 moves macOS-15 CI jobs off the dead Blacksmith pool (cures the
tests-build-and-lag runner failure), remote workspace package test
stabilization, #7359 ssh RemoteCommand/RequestTTY fix, #7255 client config
API, #7174 NIGHTLY updater fix, and the sidebar inline-rename feature.

Conflicts resolved keeping HEAD's refactored structure:
- RemoteTmuxHost: union imports (main's CmuxFoundation + HEAD's CmuxRemoteSession).
- TerminalSSHSessionDetector: took main's scpArguments addition (#7359).
- FileExplorerStore: HEAD tombstone kept — the CmuxFoundation package copy of
  SSHFileExplorerProvider already carries main's stateLock/State shape.
- ContentView (2 regions): kept HEAD's extracted SidebarWorkspaceRowContent row.
  main's inline-rename edits target the inline row body the refactor extracted;
  the feature's six implementation files + tests auto-merged in and the row-
  architecture port follows as a bounded task (rename-port) before merge.
- budget.tsv regenerated; pbxproj union-dedup + normalize (SidebarScrim.swift
  ref pruned: whole-file-lifted into CmuxSidebarUI earlier, unreferenced on
  main too). Test-wiring/budget/conventions lints green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 759c48c8 Deployed Jul 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cmux ssh fails when SSH Host config sets RemoteCommand/RequestTTY

1 participant