Skip to content

Stream cmuxd-remote bootstrap upload over ssh instead of scp - #6829

Closed
austinywang wants to merge 12 commits into
mainfrom
issue-6207-cmux-ssh-bootstrap-upload-method-is
Closed

austinywang wants to merge 12 commits into
mainfrom
issue-6207-cmux-ssh-bootstrap-upload-method-is

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6207.

Problem

The cmux ssh bootstrap uploads the cmuxd-remote binary with scp. But scp/sftp sessions are frequently chrooted or jailed to a subtree that differs from the interactive SSH session, so the absolute $HOME/.cmux/bin/cmuxd-remote/<version>/<os>-<arch>/cmuxd-remote destination that is valid over SSH can be unreachable — or resolve to the wrong place — under scp. Affected users (e.g. HPC logins with a jailed sftp subsystem) hit:

SSH error: Remote daemon bootstrap failed: failed to upload cmuxd-remote:
/usr/bin/scp: dest open ".../.cmux/bin/cmuxd-remote/0.64.10/linux-amd64/cmuxd-remote.tmp-...": No such file or directory

and the bootstrap never completes. This is the long-standing fragility tracked in #3689 / #3880.

Fix

Upload the binary by piping it over the SSH channel itself instead of scp, exactly as the reporter suggested:

cat <local cmuxd-remote> | ssh -T user@host 'sh -c "cat > <abs tmp path>"'

Because the upload travels over the same transport as every other bootstrap command (probe, mkdir -p, finalize chmod && mv, serve --stdio hello), the remote path semantics are guaranteed to match the interactive SSH session — no scp/sftp jail mismatch is possible. -T suppresses any forced pseudo-terminal so tty line-ending translation cannot corrupt the binary stream. The mkdir -p before and the atomic chmod 755 && mv finalize are unchanged, so the install stays atomic.

Why drag-and-drop uploads still use scp

uploadDroppedFilesLocked and DetectedSSHSession keep scp deliberately: they target /tmp (not the jailed home subtree the issue is about) and may transfer arbitrarily large user files, which scp streams from disk rather than buffering in memory.

Runner hardening

RemoteSessionProcessRunner now carries a multi-MB binary through stdin (previously stdin was only ever a few bytes), so its stdin handling is hardened:

  • write on a background thread, so the existing timeout / terminate-then-SIGKILL path stays authoritative for slow or stalled transfers;
  • suppress SIGPIPE on the fd (F_SETNOSIGPIPE) and loop over partial writes, so a remote that closes the channel early (e.g. a failed redirect target) surfaces as a nonzero exit status instead of crashing the app.

Tests

Two-commit red/green:

  1. Failing tests — the two bootstrap upload tests now assert the binary is streamed over ssh … 'cat >' (with the absolute $HOME path and the binary bytes on stdin) and that scp is never invoked. They fail against the scp-based upload.
  2. Fix — makes them pass. Also adds a RemoteSessionProcessRunner test that pipes a ~1.1 MiB stdin payload through /bin/cat to exercise the backpressured partial-write loop.

Localization

No user-facing UI strings added. The one new NSError detail (failed to read cmuxd-remote for upload) follows the existing bare-English diagnostic convention shared by every sibling daemon-bootstrap error in this file; the localized layer is the separate RemoteDaemonStrings/RemoteSessionStrings seam, which is unchanged.


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


Summary by cubic

Stream the cmuxd-remote bootstrap upload over SSH stdin instead of scp to avoid jailed scp/sftp path mismatches. Fixes failed installs on chrooted sftp hosts and aligns paths with the interactive SSH session (fixes #6207).

  • Bug Fixes

    • Upload via ssh -T … 'sh -c "cat > <tmp>"', then mkdir -p and atomic chmod && mv (120s timeout).
    • Disable PTY with -T to prevent binary stream mangling; scp is never used for daemon upload.
    • Propagate local read failures from Data(contentsOf:) as Cocoa file errors (no custom wrapping).
    • Tests: assert cat > SSH pipe with -T, exact daemon bytes are sent; reinstall flow also streams over SSH; deterministic reinstall via CMUX_REMOTE_DAEMON_BINARY; large-stdin path covered; original test name kept. Drag‑and‑drop to /tmp still uses scp.
  • Refactors

    • Hardened RemoteSessionProcessRunner stdin: background writer with F_SETNOSIGPIPE, partial‑write loop, and dup()-failure synchronous fallback so input is never dropped. Writer moved to a file‑scope helper.

Written for commit cdef145. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Remote startup now uploads the daemon binary over SSH by streaming the file contents directly, improving compatibility with remote environments.
  • Bug Fixes

    • Large input can now be sent to remote processes without truncation.
    • Remote bootstrap and reinstall flows now handle binary uploads more reliably and use the expected absolute install path.

cmux and others added 2 commits June 26, 2026 00:42
Regression coverage for #6207. The cmuxd-remote bootstrap upload uses scp,
whose path semantics frequently differ from the interactive SSH session
(chroot/jailed sftp subtrees), so a destination valid over SSH can be
unreachable or resolve differently under scp.

These two tests now assert the binary is streamed over the SSH channel
itself (`ssh -T … 'cat > tmp'`) and that scp is never invoked. They fail
against the current scp-based upload; the following commit makes them pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes #6207.

The cmuxd-remote bootstrap uploaded the daemon binary with scp. scp/sftp
sessions are frequently chrooted or jailed to a subtree that differs from the
interactive SSH session, so the absolute `$HOME/.cmux/...` destination that is
valid over SSH can be unreachable — or resolve to the wrong place — under scp.
Users on such hosts hit "dest open ... No such file or directory" and the
bootstrap never completes.

Upload the binary by piping it over the SSH channel itself
(`ssh -T … 'cat > tmp'`) instead. Because it travels over the same transport
as every other bootstrap command, the remote path semantics are guaranteed to
match. `-T` suppresses any forced pseudo-terminal so tty line-ending
translation cannot corrupt the binary stream. The mkdir-before and atomic
chmod+mv finalize steps are unchanged.

The drag-and-drop file uploads (`uploadDroppedFilesLocked`, `DetectedSSHSession`)
keep scp: they target `/tmp` (not the jailed home subtree) and may transfer
arbitrarily large files, which scp streams from disk rather than buffering in
memory.

Also harden RemoteSessionProcessRunner's stdin handling, now that it carries a
multi-MB binary: write on a background thread (so the timeout/termination path
stays authoritative), suppress SIGPIPE on the fd, and loop over partial writes
so a remote that closes the channel early surfaces as a nonzero exit status
instead of crashing the app. Previously stdin was only ever a few tiny bytes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 26, 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 4, 2026 11:25pm
cmux-staging Building Building Preview, Comment Jul 4, 2026 11:25pm

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR streams remote daemon uploads over SSH instead of scp, changes process stdin handling to asynchronous writes with EINTR retry, and adds regression tests for large stdin and SSH-pipe bootstrap uploads.

Changes

Remote SSH stdin streaming and upload

Layer / File(s) Summary
Async stdin writes
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Process/RemoteSessionProcessRunner.swift, Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionProcessRunnerTests.swift
RemoteSessionProcessRunner.run now writes stdin on a background task with duplicated-fd, partial-write, and EINTR handling, and the new test sends a large /bin/cat payload through that path.
SSH bootstrap upload
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+Bootstrap.swift
The bootstrap comment and upload path now read the daemon binary into Data, send it through sshExec with -T and cat > <tmp>, and finalize the temp file with chmod and mv.
Bootstrap upload tests
cmuxTests/WorkspaceRemoteConnectionTests.swift
The daemon bootstrap regression test now captures SSH cat > uploads, rejects scp, and verifies the transferred bytes and remote target path.
Persistent PTY reinstall tests
cmuxTests/WorkspaceRemoteConnectionTests.swift
The reinstall regression test now pins CMUX_REMOTE_DAEMON_BINARY, distinguishes initial and reinstall uploads, rejects scp, and checks the reinstall target path under the remote home directory.

Sequence Diagram(s)

sequenceDiagram
  participant uploadRemoteDaemonBinaryLocked as "RemoteSessionCoordinator.uploadRemoteDaemonBinaryLocked"
  participant sshExec as "sshExec"
  participant remoteCat as "sh -c 'cat > <remoteTempPath>'"
  participant finalize as "chmod and mv"
  uploadRemoteDaemonBinaryLocked->>sshExec: stdin = Data(contentsOf: local daemon binary)
  sshExec->>remoteCat: ssh -T ... shell command
  remoteCat-->>sshExec: writes the uploaded bytes
  uploadRemoteDaemonBinaryLocked->>finalize: chmod then mv temp file into place
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#6136: Extends the same subprocess test harness in RemoteSessionProcessRunnerTests.swift, which is directly adjacent to the new large-stdin coverage here.

Poem

I twitch my nose and hop away,
through SSH streams where binary bytes play.
No scp snags my moonlit ride,
just cat > flowing side by side.
🐇✨ The tunnel hums; my whiskers glow.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error RemoteSessionProcessRunner adds a new DispatchQueue.global().async stdin writer in cmux-owned runtime code; the rule flags this legacy async pattern. Replace the fire-and-forget queue with Swift concurrency or a caller-owned async write path; keep background hopping only for required boundaries.
Cmux Full Internationalization ❌ Error Bootstrap adds a new user-facing error via raw NSLocalizedDescriptionKey, but no matching key exists in Resources/Localizable.xcstrings for the 20 supported locales. Use String(localized:defaultValue:) for the read-failure message and add the key to Resources/Localizable.xcstrings with translations for every existing locale.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (22 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: streaming the bootstrap upload over SSH instead of scp.
Linked Issues check ✅ Passed The code matches #6207: the bootstrap upload now streams over SSH with -T and preserves the mkdir/chmod/mv flow.
Out of Scope Changes check ✅ Passed The extra stdin hardening and test updates are directly tied to the new SSH-pipe upload and aren't unrelated scope.
Cmux Swift Actor Isolation ✅ Passed Prod changes stay queue-confined; no new MainActor, service-protocol, or shared mutable Sendable isolation debt is introduced.
Cmux Swift Blocking Runtime ✅ Passed No new production waits/sleeps/locks were added; stdin now writes on a background queue, and remaining semaphores/locks are existing or test-only.
Cmux Browser Automation Off-Main ✅ Passed Diff only changes remote-session upload/stdin files; the rule targets browser commands in TerminalController and ControlCommandExecutionPolicy, which this PR doesn't touch.
Cmux Expensive Synchronous Load ✅ Passed Production edits only change SSH upload and background stdin streaming; no agent-history/session/transcript loads, main-actor moves, or JSON/JSONL scans were added.
Cmux Cache Substitution Correctness ✅ Passed Touched code only changes SSH upload/stdin streaming; it does not replace any fresh persistence/history/snapshot read with a cache.
Cmux No Hacky Sleeps ✅ Passed PASS: The actual diff only changes Zig/C header files in ghostty; no TS/JS/shell/build/runtime script changes or sleep/delay hacks are present.
Cmux Algorithmic Complexity ✅ Passed No scalable-collection regressions: the new code is linear byte streaming and one-pass capability checks; no nested scans or repeated sorts/filtering in hot paths.
Cmux Swift @Concurrent ✅ Passed No changed @concurrent/nonisolated async APIs; the touched helpers stay synchronous, and the coordinator is queue-confined rather than @MainActor, so the rule isn’t violated.
Cmux Swift File And Package Boundaries ✅ Passed Changes stay within the CmuxRemoteSession package; the 685-line bootstrap file got a small focused upload fix, and the rest is test-only.
Cmux Swiftpm Lockfiles ✅ Passed Diff vs origin/main changes only Swift sources/tests and a budget TSV; no Package.swift, Package.resolved, .gitignore, or Xcode package-reference files changed.
Cmux Swift Logging ✅ Passed Touched Swift code adds no runtime print/NSLog/debugPrint or ad hoc file/stdout logging; remaining logs are #if DEBUG cmux events, which are allowed.
Cmux User-Facing Error Privacy ✅ Passed The new user-facing errors are generic cmux/SSH upload messages and don’t expose vendor names, secrets, or internal provider details.
Cmux Swiftui State Layout ✅ Passed No SwiftUI view/state code was changed; the touched files are process/bootstrap/tests and contain no new state/layout patterns.
Cmux Architecture Rethink ✅ Passed PASS: This is a local correctness fix with clear ownership; the new async stdin writer is a required Process/Pipe bridge, and the added locks/semaphores are test-only.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only touches bootstrap/process/test code; no user-visible window APIs or cmuxAuxiliaryWindowIdentifiers changes were introduced.
Cmux Source Artifacts ✅ Passed Changed paths are source/test files and submodule refs; no logs, temp dirs, caches, build output, or other artifacts are introduced.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The touched Sources files add ssh upload/streaming logic only; no new test/debug-only accessor or seam was introduced in production code.
Cmux No Ambient Global State ✅ Passed The only new file-scope function is private (writeStdinAndClose), and the diff adds no global mutable state, singleton, or namespace-only type.
Description check ✅ Passed The description clearly covers the problem, fix, testing, and rationale; only optional template sections like demo video and checklist are omitted.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6207-cmux-ssh-bootstrap-upload-method-is

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 Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the scp-based cmuxd-remote binary upload with an SSH-channel pipe (ssh -T … 'sh -c "cat > tmp"'), eliminating the chroot/jail path mismatch that caused the bootstrap to fail on HPC hosts with a jailed sftp subsystem. It also hardens RemoteSessionProcessRunner to safely stream multi-MB payloads over stdin using a background writer with dup/F_SETNOSIGPIPE/partial-write loop.

  • Bootstrap upload (RemoteSessionCoordinator+Bootstrap.swift): scp call replaced with Data(contentsOf:) + sshExec(stdin:) using the same SSH transport as every other bootstrap command, with a 120 s timeout and proper atomic chmod && mv finalize unchanged.
  • Process runner hardening (RemoteSessionProcessRunner.swift): stdin is now written on a background DispatchQueue.global thread; the write descriptor is dup'd so the timeout/kill path stays authoritative, SIGPIPE is suppressed via F_SETNOSIGPIPE, and partial writes loop until complete or EPIPE, with a synchronous fallback if dup fails.
  • Tests: Regression tests now assert the binary bytes arrive over ssh -T … cat > (never scp), and a new large-stdin test exercises the backpressured write loop with a ~1.1 MiB payload.

Confidence Score: 5/5

The change is safe to merge: it narrows a well-understood path mismatch by routing the binary through the same SSH transport used for every other bootstrap command, and the runner hardening is carefully scoped with correct fd lifetime management.

The core upload change is straightforward: reads the binary once, pipes it over an SSH channel with -T, and falls back to the same atomic chmod && mv finalize. The runner hardening (dup/SIGPIPE/partial-write loop) covers the only new failure mode — a multi-MB write through a pipe — and the synchronous fallback on dup failure ensures stdin is never silently dropped. The two previously-flagged issues (0-byte daemon install on dup failure, and duplicate NSError code 31) were addressed in the commits cited in the thread replies before this review ran. Both bootstrap tests are now deterministic, and the large-stdin test directly exercises the backpressured writer path. No new blocking primitives, ambient globals, or test seams appear in production sources.

No files require special attention.

Important Files Changed

Filename Overview
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Process/RemoteSessionProcessRunner.swift Adds background dup/write/SIGPIPE-suppressed stdin writer; private file-scope helpers are correctly scoped, partial-write loop and fallback are sound
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+Bootstrap.swift scp replaced with SSH-channel pipe; shell quoting double-wrap is consistent with existing pattern; local read failure correctly propagates as Cocoa error; atomic finalize unchanged
Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionProcessRunnerTests.swift Adds large-stdin regression test (1.1 MiB via /bin/cat) that exercises the backpressured partial-write loop; test-only scaffolding, no production-source seams
cmuxTests/WorkspaceRemoteConnectionTests.swift Both bootstrap tests updated to assert ssh pipe with -T, scp never invoked, and exact daemon bytes on stdin; reinstall test made deterministic via CMUX_REMOTE_DAEMON_BINARY override; uploadCount race is correctly lock-guarded
.github/swift-file-length-budget.tsv Budget entries updated to reflect new line counts; no files exceed the 800-line hard limit

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant B as bootstrapDaemonLocked
    participant U as uploadRemoteDaemonBinaryLocked
    participant R as RemoteSessionProcessRunner.run
    participant BG as Background Writer (DispatchQueue.utility)
    participant SSH as ssh process
    participant Remote as Remote Shell (sh -c cat > tmp)

    B->>U: localBinary URL
    U->>U: Data(contentsOf: localBinary)
    U->>R: "run(stdin=binaryData, timeout=120)"
    R->>SSH: Process.run()
    R->>BG: DispatchQueue.global.async writeStdinAndClose(binaryData, fd)
    BG->>SSH: Darwin.write(fd) loop partial writes EINTR retry
    SSH-->>Remote: binary bytes over SSH channel
    Remote-->>SSH: exit 0
    BG->>BG: Darwin.close(fd) on EPIPE or completion
    SSH-->>R: process exits exitSemaphore.signal()
    R-->>U: "RemoteCommandResult(status=0)"
    U->>U: sshExec chmod 755 tmp and mv tmp dest
    U-->>B: success
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 B as bootstrapDaemonLocked
    participant U as uploadRemoteDaemonBinaryLocked
    participant R as RemoteSessionProcessRunner.run
    participant BG as Background Writer (DispatchQueue.utility)
    participant SSH as ssh process
    participant Remote as Remote Shell (sh -c cat > tmp)

    B->>U: localBinary URL
    U->>U: Data(contentsOf: localBinary)
    U->>R: "run(stdin=binaryData, timeout=120)"
    R->>SSH: Process.run()
    R->>BG: DispatchQueue.global.async writeStdinAndClose(binaryData, fd)
    BG->>SSH: Darwin.write(fd) loop partial writes EINTR retry
    SSH-->>Remote: binary bytes over SSH channel
    Remote-->>SSH: exit 0
    BG->>BG: Darwin.close(fd) on EPIPE or completion
    SSH-->>R: process exits exitSemaphore.signal()
    R-->>U: "RemoteCommandResult(status=0)"
    U->>U: sshExec chmod 755 tmp and mv tmp dest
    U-->>B: success
Loading

Reviews (8): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment on lines +194 to +201
let writeDescriptor = Darwin.dup(pipe.fileHandleForWriting.fileDescriptor)
try? pipe.fileHandleForWriting.close()
if writeDescriptor >= 0 {
_ = Darwin.fcntl(writeDescriptor, F_SETNOSIGPIPE, 1)
DispatchQueue.global(qos: .utility).async {
Self.writeStdinAndClose(stdin, to: writeDescriptor)
}
}

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.

P1 Silent empty-binary upload when dup fails

Darwin.dup can fail (returns -1) if the process is out of file descriptors. When it does, the write end of the stdin pipe is already closed (line 195 ran first), but the if writeDescriptor >= 0 guard simply skips the background writer without throwing. The spawned ssh process immediately reads EOF, cat > remoteTempPath completes successfully with 0 bytes, uploadResult.status == 0 passes, and the chmod && mv finalize runs — atomically installing an empty cmuxd-remote on the remote host. The failure only surfaces later as a cryptic "failed to start remote daemon" from the hello handshake, with no trace of the root cause.

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.

Fixed in 263ba76. The async path now only closes the original write handle once the dup succeeds; if dup fails it falls back to a synchronous write on the original handle (writeStdinSynchronouslyAndClose), so stdin is never silently emptied and a 0-byte daemon can't be installed.

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.

Verified on current head cdef145: dup failure no longer drops stdin. RemoteSessionProcessRunner falls back to writeStdinSynchronouslyAndClose on the original pipe handle, so the SSH cat upload cannot succeed with an implicit empty stream from a skipped writer.

— Claude Code

Comment on lines 334 to 341
let binaryData: Data
do {
binaryData = try Data(contentsOf: localBinary)
} catch {
throw NSError(domain: "cmux.remote.daemon", code: 31, userInfo: [
NSLocalizedDescriptionKey: "failed to read cmuxd-remote for upload: \(error.localizedDescription)",
])
}

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.

P1 Duplicate NSError code 31 for two distinct failure modes

Both the new "failed to read cmuxd-remote for upload" branch and the pre-existing "failed to upload cmuxd-remote" branch throw NSError(domain: "cmux.remote.daemon", code: 31, ...). Any caller that routes or counts on the error code (retry logic, telemetry, tests) cannot distinguish a local file-read failure from an SSH upload failure. Code 33 is free (30 = mkdir, 31 = upload SSH exit, 32 = finalize).

Suggested change
let binaryData: Data
do {
binaryData = try Data(contentsOf: localBinary)
} catch {
throw NSError(domain: "cmux.remote.daemon", code: 31, userInfo: [
NSLocalizedDescriptionKey: "failed to read cmuxd-remote for upload: \(error.localizedDescription)",
])
}
let binaryData: Data
do {
binaryData = try Data(contentsOf: localBinary)
} catch {
throw NSError(domain: "cmux.remote.daemon", code: 33, userInfo: [
NSLocalizedDescriptionKey: "failed to read cmuxd-remote for upload: \(error.localizedDescription)",
])
}

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.

Fixed in ab6f5ee. The local Data(contentsOf:) failure now propagates its system-localized Cocoa error instead of being re-wrapped in a second code: 31 NSError, so the read failure and the remote upload failure are no longer conflated under one code.

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.

Verified on current head cdef145: this is no longer using a duplicate NSError code for local read failure. The bootstrap path now lets Data(contentsOf:) throw the local Cocoa file error and keeps code 31 for SSH upload failure only.

— Claude Code

cmux and others added 3 commits June 26, 2026 01:38
Mechanical `python3 scripts/swift_file_length_budget.py --write-budget` after
merging origin/main. Accounts for the +44 lines in WorkspaceRemoteConnectionTests.swift
(rewritten bootstrap-upload tests) and +8 lines in RemoteSessionCoordinator+Bootstrap.swift
(ssh-pipe upload). Other entries are the script tightening budgets for files that
shrank on main since the last refresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure helpers belong at file scope, not as a private static func attached to a
type (avoids the static-as-namespace pattern). Behavior unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

cmux and others added 2 commits June 26, 2026 03:31
testPersistentPTYBootstrapReinstallsOldDaemonMissingPTYCapability previously
let buildLocalDaemonBinary fall through to the dev-only `go build` path, which
needs a Go toolchain that the app-host CI runner does not have — so the upload
was never reached and the test timed out at ~2s (it failed identically on main,
masked by CI's retry/expected-failure handling).

Supply the daemon binary via the explicit-override env var instead, so the
binary is a real on-disk file. The bootstrap still reaches the hello handshake,
sees the missing persistent-PTY capability, and reinstalls; the test captures
that second (reinstall) upload and asserts it streams over ssh, not scp (#6207).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mechanical scripts/swift_file_length_budget.py --write-budget after the
deterministic reinstall test rewrite (+20 lines in WorkspaceRemoteConnectionTests.swift).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/WorkspaceRemoteConnectionTests.swift`:
- Around line 2053-2058: The SSH upload test setup in
WorkspaceRemoteConnectionTests should also assert that the upload command
includes the -T flag, not just cat > and scp rejection. Update the existing
upload command capture/assertion around the remoteProcessScript and
uploadCommand checks so the reinstall test and the other SSH upload regression
tests explicitly verify -T is present in the arguments for the upload path,
using the same assertion pattern already used elsewhere in this file.

In
`@Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Process/RemoteSessionProcessRunner.swift`:
- Around line 194-201: The stdin setup path in RemoteSessionProcessRunner should
not silently continue when dup() or F_SETNOSIGPIPE fails. Update the
writeDescriptor setup around writeStdinAndClose/stdin to follow the same failure
handling used for launch and cancellation errors: if dup() returns an invalid
descriptor or fcntl(F_SETNOSIGPIPE) fails, terminate the running process, call
finishCaptureAndCloseReadHandles(), and throw an appropriate POSIXError instead
of proceeding with an EOF or SIGPIPE-prone session.

In
`@Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator`+Bootstrap.swift:
- Around line 338-340: The upload-read failure text in
RemoteSessionCoordinator+Bootstrap is user-facing but is being inserted directly
into NSLocalizedDescriptionKey, so it must be localized. Update the NSError
construction in the bootstrap path to use String(localized:defaultValue:) for
the message, and add the corresponding key/value entries to
Resources/Localizable.xcstrings for the supported locales.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2346113-1cc4-4fba-834e-302d89930cb5

📥 Commits

Reviewing files that changed from the base of the PR and between 6d6c701 and 17c739d.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (4)
  • Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Process/RemoteSessionProcessRunner.swift
  • Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+Bootstrap.swift
  • Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionProcessRunnerTests.swift
  • cmuxTests/WorkspaceRemoteConnectionTests.swift

Comment thread cmuxTests/WorkspaceRemoteConnectionTests.swift
cmux and others added 3 commits June 26, 2026 04:55
Reverts the testDaemonBootstrapUpload... method rename. The cmuxTests shards
are a sha256-ordered bin-pack over test identifiers
(scripts/ci/cmux_unit_test_shard.py); renaming one method reshuffles unrelated
GUI suites across shards and relocated BrowserPanelWebViewLifecycleTests into a
shard ordering where its pre-existing flaky WebKit SIGSEGV
(testBackgroundPreloadIsConsumedByInitialNavigation) fails xcodebuild's
crash-recovery. That same crash occurs on main too, but main's shard layout
recovers it ("0 unexpected"); the reshuffled layout did not.

Keeping the original identifier makes the cmuxTests shard distribution
byte-identical to main, so this PR no longer surfaces that unrelated flake. The
test body is unchanged and still verifies the ssh-pipe upload from #6207.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex review caught a real failure mode in the async stdin path: if dup() of
the pipe write end fails (e.g. fd exhaustion), the code closed the original
handle and let the child read an empty stream. For the cmuxd-remote upload that
means `cat > tmp` succeeds with 0 bytes and chmod/mv installs an empty daemon,
masking the real local resource error.

Only close the original handle once the duplicate is secured; if dup fails,
fall back to a synchronous write on the original handle so required stdin is
never silently lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Let a local Data(contentsOf:) failure propagate its system-localized Cocoa
  error instead of wrapping it in a second NSError reusing daemon code 31
  (Greptile: duplicate code; CodeRabbit: localize new message). No custom code,
  no new English diagnostic.
- Both bootstrap-upload regression tests now assert the ssh invocation includes
  -T (disables pseudo-terminal allocation so the binary stream is not mangled),
  per CodeRabbit.

The dup-failure silent-empty-stdin concern (Greptile P1 / CodeRabbit) was
already fixed by the synchronous fallback in the previous commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ootstrap-upload-method-is

# Conflicts:
#	.github/swift-file-length-budget.tsv
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

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

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cmux ssh bootstrap upload method is fragile

3 participants