* test(ssh): cover false remote death recovery * ci: exercise SSH regression baseline * fix(ssh): preserve remote PTY recovery * fix(ssh): harden recovery diagnostics * fix(remote): keep log routing portable * test: follow renamed resume command field * test: cover recovery preservation edge cases * fix: preserve recovery state through inconclusive probes * test: exercise terminal teardown through app fixture * test(remote): cover closed daemon output descriptors * fix: harden recovery review edge cases * test: substitute pinned SSH in teardown fixture * test: cover final recovery review edge cases * fix: preserve recovery through final reconciliation edges * test: distinguish automatic and manual PTY recovery * fix: report PTY recovery state accurately * test: make reconciliation parameter type explicit * test: require reconciliation acknowledgement contracts * Fix SSH startup fixtures after main merge * Test confirmed PTY end cleanup failure * Finalize cleanup for confirmed PTY exit * Keep SSH auth fixtures off live network * Fix SSH tests after reconnect merge * Test terminal reset before no-progress reattach * Reset terminal modes before no-progress reattach * Run no-progress reset regression in package tests * Fix SSH test fixtures after reconnect merge
cmuxd-remote (Go)
Go remote daemon for cmux ssh bootstrap, capability negotiation, and remote proxy RPC. It is not in the terminal keystroke hot path.
Commands
cmuxd-remote versioncmuxd-remote serve --stdiocmuxd-remote serve --stdio --persistent --slot <slot> [--persistent-lease-port <port>]cmuxd-remote serve --persistent-stop --slot <slot>— internal authenticated slot teardowncmuxd-remote serve --ws --auth-lease-file <path> [--rpc-auth-lease-file <path>] [--listen 127.0.0.1:7777]cmuxd-remote cli <command> [args...]— relay cmux commands to the local app over the reverse SSH forward
serve --ws is explicit opt-in for cloud VM images only. The normal cmux ssh
code path uses serve --stdio --persistent --slot <slot> over an SSH exec
channel. That stdio process is only a proxy to an authenticated per-slot daemon
with credentials and logs under ~/.cmux/daemon/<version>/<slot>/, so remote PTY sessions
can survive local surface close, local reconnect, and app relaunch. The persistent
server never opens a public listener; it accepts only a per-user Unix socket under
/tmp/cmuxd-remote-<uid>/ and the slot token.
When invoked as cmux (via wrapper/symlink installed during bootstrap), the binary auto-dispatches to the cli subcommand. This is busybox-style argv[0] detection.
RPC methods (newline-delimited JSON over stdio)
hellopingproxy.openproxy.closeproxy.writeproxy.stream.subscribe- async
proxy.stream.data/proxy.stream.eof/proxy.stream.errorevents session.opensession.closesession.attachsession.resizesession.detachsession.statuspty.attachpty.writepty.resizepty.detachpty.closepty.list
Current integration in cmux:
workspace.remote.configurenow bootstraps this binary over SSH when missing.- Client sends
hellobefore enabling remote proxy transport. - Local workspace proxy broker serves SOCKS5 + HTTP CONNECT and tunnels stream traffic through
proxy.*RPC overserve --stdio, using daemon-pushed stream events instead of polling reads. - Daemon status/capabilities are exposed in
workspace.remote.status -> remote.daemon(includingsession.resize.min). - Persistent SSH terminals require the
pty.session.persistent_daemoncapability before cmux will restore a saved remote PTY session ID after relaunch.
Persistent SSH PTY daemon
cmux ssh uses one persistent daemon slot per CLI-launched SSH workspace. The
slot name is generated locally, validated as [A-Za-z0-9._-]{1,128}, and sent
to the remote daemon bootstrap as --slot.
Remote slot files:
/tmp/cmuxd-remote-<uid>/cmuxd-<slot-hash>.sockauthenticated Unix socket for stdio proxies.~/.cmux/daemon/<version>/<slot>/auth.tokenrandom 32-byte hex token, mode0600.~/.cmux/daemon/<version>/<slot>/daemon.locksingle-owner lock.~/.cmux/daemon/<version>/<slot>/daemon.loglifecycle and crash diagnostics.
PTY lifecycle:
- A local attach creates or reuses a named
pty.*session in the persistent daemon. - If the local surface closes, the stdio proxy disconnects and its attachment detaches, but the PTY process and bounded scrollback remain in the daemon.
cmux ssh-session-listcallspty.list;cmux ssh-session-attachcreates a new local terminal whose startup script callsssh-pty-attach --require-existing.cmux ssh-session-cleanupcallspty.closeto terminate a persisted PTY session explicitly.- Sessions with no attachments keep their last-known size and are reaped by the daemon idle TTL.
- Closing the owning workspace sends an authenticated slot-shutdown request, waits a bounded interval for the daemon lock to be released, and removes the relay's shell-state directory. As defense in depth, a daemon launched with
--persistent-lease-portobserves that exact~/.cmux/relay/<port>.slotlease, but retires passively only after the observed lease disappears and both stdio connections and live PTY sessions are empty. A detached live PTY survives lease loss until it exits or is closed explicitly. Older callers that omit the flag retain the prior behavior without unsafe broad lease scanning.
Persistent daemon diagnostics
Persistent-daemon logging is always enabled. The current log is
~/.cmux/daemon/<version>/<slot>/daemon.log, mode 0600. It records daemon
start/readiness/stop, authenticated connection lifecycle, PTY attach/detach/
close/exit, channel or PTY-operation faults, and process-level stdout/stderr
occurrence and rate-limited aggregate byte counts. Arbitrary process output is
discarded rather than persisted. Faults are recorded as bounded codes or
categories rather than raw error text. Tokens, commands, terminal input, RPC
request identifiers, and raw process output are never logged.
The log rotates at 2 MiB. daemon.log is the newest file, with at most two
older generations in daemon.log.1 and daemon.log.2, so one slot uses at
most approximately 6 MiB of diagnostics.
Cloud WebSocket PTY transport
The WebSocket PTY transport is locked until the backend writes a short-lived lease file. The baked image contains only the daemon binary and service command, not user secrets or provider API keys.
Lease file shape:
{
"version": 1,
"token_sha256": "<sha256 hex of client attach token>",
"expires_at_unix": 1770000000,
"session_id": "optional-session-binding",
"single_use": true
}
Client flow:
- Connect to
/terminal. - Send a text JSON auth frame first:
{"type":"auth","token":"...","session_id":"...","cols":80,"rows":24}. - After
{"type":"ready"}, binary WebSocket frames are terminal input/output. - Text frames after auth are control frames such as
{"type":"resize","cols":120,"rows":40}.
Security invariants:
serve --wsfails to start without--auth-lease-file.- Missing, expired, wrong-token, or wrong-session leases close with WebSocket policy violation before a PTY is started.
- Successful single-use leases are consumed before the shell is spawned, so a
replay gets
no active lease. - Provider traffic auth remains separate. E2B images should be created with
network.allowPublicTraffic: false, so E2B requirese2b-traffic-access-tokenbefore the daemon sees the request.
workspace.remote.configure contract notes:
port/local_proxy_portaccept integer values and numeric strings; explicitnullclears each field.- Out-of-range values and invalid types return
invalid_params. local_proxy_portis an internal deterministic test hook used by bind-conflict regressions.- SSH option precedence checks are case-insensitive; user overrides for
StrictHostKeyCheckingand control-socket keys prevent default injection.
Distribution
Release and nightly builds publish prebuilt cmuxd-remote binaries on GitHub Releases for:
darwin/arm64darwin/amd64linux/arm64linux/amd64
The app embeds a compact manifest in Info.plist with:
- exact release asset URLs
- pinned SHA-256 digests
- release tag and checksums asset URL
Release and nightly apps download and cache the matching binary locally, verify its SHA-256, then upload it to the remote host if needed. Dev builds can opt into a local go build fallback with CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1.
To inspect what a given app build trusts, run:
cmux remote-daemon-statuscmux remote-daemon-status --os linux --arch amd64
The command prints the exact release asset URL, expected SHA-256, local cache status, and a copy-pasteable gh attestation verify command for the selected platform.
CLI relay
The cli subcommand (or cmux wrapper/symlink) connects to the local cmux app through an SSH reverse forward and relays commands using the v2 JSON-RPC protocol.
Cloud VM images install /usr/local/bin/cmux as a symlink to cmuxd-remote,
so cmux --help works before a user-specific SSH bootstrap has written
~/.cmux/bin/cmux.
Socket discovery order:
--socket <path>flagCMUX_SOCKET_PATHenvironment variable~/.cmux/socket_addrfile (written by the app after the reverse relay establishes)
For TCP addresses, the CLI dials once and only refreshes ~/.cmux/socket_addr a single time if the first address was stale. Relay metadata is published only after the reverse forward is ready, so steady-state use does not rely on polling.
Authenticated relay details:
- Each SSH workspace gets its own relay ID and relay token.
- The app runs a local loopback relay server that requires an HMAC-SHA256 challenge-response before forwarding a command to the real local Unix socket.
- The remote shell never gets direct access to the local app socket. It only gets the reverse-forwarded relay port plus
~/.cmux/relay/<port>.auth, which is written with0600permissions and removed when the relay stops.
Integration additions for the relay path:
- Bootstrap installs
~/.cmux/bin/cmuxwrapper and keeps a default daemon target (~/.cmux/bin/cmuxd-remote-current). - A background
ssh -N -Rprocess reverse-forwards a TCP port to the authenticated local relay server. The relay address is written to~/.cmux/socket_addron the remote. - Relay startup writes
~/.cmux/relay/<port>.daemon_pathso the wrapper can route each shell to the correct daemon binary when multiple local cmux instances or versions coexist. - Relay startup writes
~/.cmux/relay/<port>.authwith the relay ID and token needed for HMAC authentication.
Protocol and flags
All relay commands use v2 JSON-RPC. Flags map to JSON params via flagToParamKey (e.g. --workspace → workspace_id). Boolean flags (--focus) accept true/false/1/0/yes/no and are sent as JSON booleans.
Environment fallbacks:
CMUX_WORKSPACE_ID— used asworkspace_idwhen--workspaceis not providedCMUX_SURFACE_ID— used assurface_idwhen--surfaceis not provided
Migration notes
new-workspace: The flag --working-directory was removed. It was accepted by the old relay but sent the wrong param name (working_directory instead of cwd), so the server silently ignored it. Use --cwd for the working directory. The flag --command is now supported: it sends the command text to the new workspace's default surface after creation.
send / send-key: The --text and --key flags were removed. Both commands now take their argument positionally, matching the Mac CLI convention: cmux send "hello world" and cmux send-key ctrl+c.
Window commands: Prior to this release, list-windows, current-window, new-window, focus-window, and close-window used a v1 text protocol and returned plain-text responses (e.g. window:abc123 per line). They now use v2 JSON-RPC and return JSON. Scripts parsing that output will need updating.
Browser relay behavior:
cmux browser ...inside an SSH session controls the local cmux browser through the authenticated relay, not a browser process inside the VM.- The remote CLI supports the common automation commands:
open,navigate,back,forward,reload,get-url,snapshot,eval,wait,click,dblclick,hover,focus,check,uncheck,fill,type,press,select, andscreenshot. - Commands that target an existing browser surface default to
CMUX_SURFACE_ID;opendefaults toCMUX_WORKSPACE_IDso agents can create a browser pane next to the active SSH terminal.
Workspace group relay behavior:
cmux workspace group <sub>(and thecmux workspace-group <sub>alias) maps to theworkspace.group.*v2 methods, with the same subcommands and flags as the macOS CLI:list,create,ungroup,delete,rename,collapse,expand,pin,unpin,add,remove,set-anchor,new-workspace,set-color,set-icon,move, andfocus.- The group id comes from
--group <id>or the first positional argument and accepts UUIDs or refs such asworkspace_group:1. Like the macOS CLI,addandset-anchorrequire explicit--group <id> --workspace <id>.