Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to cmux are documented here.

## [Unreleased]

### Changed

- `cmux ssh` now forwards `PATH`, `SHELL`, and `SSH_AUTH_SOCK` from the invoking shell by default, so ProxyCommand helpers and other tools on your shell PATH are reachable from the SSH session. Add `--inherit-env` to forward your full environment (stale cmux socket and workspace variables are scrubbed automatically).
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## [0.64.13] - 2026-06-04

### Added
Expand Down
67 changes: 63 additions & 4 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7420,6 +7420,7 @@ struct CMUXCLI {
let workspaceName: String?
let windowRaw: String?
let noFocus: Bool
let inheritEnvironment: Bool
let sshOptions: [String]
let extraArguments: [String]
let agentSocketPath: String?
Expand All @@ -7437,6 +7438,7 @@ struct CMUXCLI {
workspaceName: String?,
windowRaw: String? = nil,
noFocus: Bool,
inheritEnvironment: Bool = false,
sshOptions: [String],
extraArguments: [String],
agentSocketPath: String? = nil,
Expand All @@ -7451,6 +7453,7 @@ struct CMUXCLI {
self.workspaceName = workspaceName
self.windowRaw = windowRaw
self.noFocus = noFocus
self.inheritEnvironment = inheritEnvironment
self.sshOptions = sshOptions
self.extraArguments = extraArguments
self.agentSocketPath = agentSocketPath
Expand Down Expand Up @@ -7711,10 +7714,9 @@ struct CMUXCLI {
var workspaceCreateParams: [String: Any] = [
"initial_command": initialSSHStartupCommand,
]
if let agentSocketPath = sshOptions.agentSocketPath {
workspaceCreateParams["initial_env"] = [
"SSH_AUTH_SOCK": agentSocketPath,
]
let startupEnvironment = sshStartupEnvironment(for: sshOptions)
if !startupEnvironment.isEmpty {
workspaceCreateParams["initial_env"] = startupEnvironment
}
Comment on lines +7717 to 7720

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist forwarded env for reusable SSH terminals

When the user creates another terminal/split in this SSH workspace, the app runs the saved terminal_startup_command from workspace.remote.configure, not the workspace.create initial_env. The new PATH/SHELL/--inherit-env values are only attached to the first terminal here; later remote terminals go through Workspace.terminalStartupEnvironment, which currently pulls only remoteConfiguration.sshTerminalStartupEnvironment (SSH_AUTH_SOCK only), so ProxyCommand helpers on the caller's PATH still disappear for subsequent SSH sessions.

Useful? React with 👍 / 👎.

try applyWindowOrCallerContext(to: &workspaceCreateParams, client: client, windowRaw: sshOptions.windowRaw)

Expand Down Expand Up @@ -7890,6 +7892,7 @@ struct CMUXCLI {
var workspaceName: String?
var windowRaw: String?
var noFocus = false
var inheritEnvironment = false
var sshOptions: [String] = []
var extraArguments: [String] = []
var forwardAgentOverride: Bool?
Expand Down Expand Up @@ -7938,6 +7941,9 @@ struct CMUXCLI {
case "--no-focus":
noFocus = true
index += 1
case "--inherit-env":
inheritEnvironment = true
index += 1
case "-A", "--forward-agent":
forwardAgentOverride = true
index += 1
Expand Down Expand Up @@ -7985,6 +7991,7 @@ struct CMUXCLI {
workspaceName: workspaceName,
windowRaw: windowRaw ?? windowOverride,
noFocus: noFocus,
inheritEnvironment: inheritEnvironment,
sshOptions: agentForwarding.sshOptions,
extraArguments: extraArguments,
agentSocketPath: agentForwarding.agentSocketPath,
Expand All @@ -7993,6 +8000,57 @@ struct CMUXCLI {
)
}

private static let safeSSHStartupEnvironmentKeys: [String] = [
"PATH",
"SHELL",
"SSH_AUTH_SOCK",
]
Comment on lines +8003 to +8007

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate SSH_AUTH_SOCK before forwarding it

With a stale SSH_AUTH_SOCK in the invoking shell, this default safe-env path forwards the raw value even when resolvedSSHAgentForwarding rejects it because the socket does not exist. In that scenario the later agentSocketPath override is nil, so the newly created terminal still receives the dead socket and OpenSSH can fail agent/config cases (e.g. ForwardAgent yes/ask) that previously fell back to the app environment instead of injecting a stale caller socket.

Useful? React with 👍 / 👎.


private static let sshInheritedEnvironmentScrubbedKeys: Set<String> = [
"CMUX_BUNDLED_CLI_PATH",
"CMUX_SOCKET",
"CMUX_SOCKET_PATH",
"CMUX_SOCKET_PASSWORD",
"CMUX_WORKSPACE_ID",
"CMUX_SURFACE_ID",
"CMUX_PANEL_ID",
"CMUX_TAB_ID",
"CMUX_PANE_ID",
"CMUXD_UNIX_PATH",
Comment on lines +8009 to +8023

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 security Relay credentials and pane ID missing from scrub list

CMUX_RELAY_TOKEN and CMUX_RELAY_ID are relay authentication credentials read from the process environment at runtime (used at line ~1926). If a user invokes cmux ssh --inherit-env from inside a relay-enabled workspace where these variables are set, they will be forwarded verbatim to the remote SSH host, allowing processes there to authenticate against the relay API with the user's credentials. CMUX_PANE_ID is also absent — it is a workspace-scoped surface identifier used alongside CMUX_SURFACE_ID (which is scrubbed) and should be treated the same way. CMUX_SOCKET_PASSWORD is correctly included, so the omission of the relay tokens appears to be an oversight rather than a deliberate choice.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
"CMUX_DEBUG_LOG",
"CMUX_RELAY_ID",
"CMUX_RELAY_TOKEN",
]

private func sshStartupEnvironment(for options: SSHCommandOptions) -> [String: String] {
let environment = ProcessInfo.processInfo.environment
var startupEnvironment = options.inheritEnvironment
? scrubbedSSHInheritedEnvironment(environment)
: safeSSHStartupEnvironment(environment)
if let agentSocketPath = options.agentSocketPath {
startupEnvironment["SSH_AUTH_SOCK"] = agentSocketPath
}
return startupEnvironment
}

private func safeSSHStartupEnvironment(_ environment: [String: String]) -> [String: String] {
var result: [String: String] = [:]
for key in Self.safeSSHStartupEnvironmentKeys {
if let value = Self.normalizedEnvValue(environment[key]) {
result[key] = value
}
}
return result
}

private func scrubbedSSHInheritedEnvironment(_ environment: [String: String]) -> [String: String] {
var result = environment.compactMapValues { Self.normalizedEnvValue($0) }
for key in Self.sshInheritedEnvironmentScrubbedKeys {
Comment on lines +8046 to +8052

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 --inherit-env silently drops empty-valued variables

scrubbedSSHInheritedEnvironment applies compactMapValues { Self.normalizedEnvValue($0) }, which strips any variable whose value is empty or whitespace-only. A user who relies on SOME_FLAG="" to disable a downstream tool will find the variable absent in the SSH session even though --inherit-env is documented as forwarding the full environment. This is an undocumented limitation; a comment or doc update noting that zero-length values are not forwarded would prevent confusion.

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!

result.removeValue(forKey: key)
}
return result
}

private func resolvedSSHAgentForwarding(
sshOptions: [String],
override: Bool?
Expand Down Expand Up @@ -13522,6 +13580,7 @@ struct CMUXCLI {
-A, --forward-agent Forward the caller's SSH agent; also honors ForwardAgent yes from ssh_config
-a, --no-forward-agent Disable SSH agent forwarding for this workspace
--ssh-option <opt> Extra SSH -o option (repeatable)
--inherit-env Forward the caller environment after scrubbing stale cmux context
--window <id|ref|index> Target window for the managed workspace
--no-focus Create workspace without switching to it

Expand Down
Loading