Summary
The openshell CLI opens its ssh sessions (sandbox connect, sandbox upload,
sandbox download, forward start) by running ssh from PATH with a
ProxyCommand that names the sandbox and a session token, and the literal host
name sandbox for every sandbox. The user's ~/.ssh/config and
/etc/ssh/ssh_config still apply, and the CLI does not turn off OpenSSH
connection sharing. With a common multiplexing setup such as
Host *
ControlMaster auto
ControlPersist 10m
ControlPath ~/.ssh/cm-%C
every sandbox resolves to the same control socket (%C hashes the local host,
the host name sandbox, the port and the user, which are the same for every
sandbox). The first session becomes the master and stays up for
ControlPersist; every later ssh the CLI starts, whatever sandbox it names,
connects to that master instead of running its own ProxyCommand, and so
reaches the first sandbox.
Observed on one Mac with two sandboxes, A and B, on one gateway:
openshell sandbox upload A … followed, within 10 minutes, by
openshell sandbox upload B … put B's files into A. The gateway logged a
CreateSshSession for the second upload but no ForwardTcp: the session
token for B was created and never used.
openshell forward start --background <port> B failed with
ssh exited before local forward listener opened.
By the same mechanism openshell sandbox connect B can attach the terminal to
A (not measured separately), which is the worst case: the user types into a
sandbox other than the one they named.
The effect is silent: no error, and the data lands in the wrong sandbox.
Environment
- OpenShell CLI 0.1.1, local gateway, two sandboxes on the MicroVM (
vm)
driver; the CLI code path is driver-independent, so the docker driver and
Linux are affected the same way.
- macOS 27.0 on Apple silicon,
/usr/bin/ssh (OpenSSH_10.3p1, LibreSSL 3.3.6).
~/.ssh/config with Host *, ControlMaster auto, ControlPersist 10m,
ControlPath ~/.ssh/cm-%C.
Steps to reproduce
-
Put the Host * block above in ~/.ssh/config.
-
Create two sandboxes, a and b, on the same gateway.
-
Confirm they share a control path:
$ ssh -G sandbox | grep -i ^control
controlmaster auto
controlpath /Users/me/.ssh/cm-<hash>
controlpersist 600
-
Upload to each within ten minutes:
$ mkdir -p /tmp/up-a /tmp/up-b && touch /tmp/up-a/from-a /tmp/up-b/from-b
$ openshell sandbox upload a /tmp/up-a /sandbox
$ openshell sandbox upload b /tmp/up-b /sandbox
$ openshell sandbox exec -n a --no-tty -- ls /sandbox/up-a /sandbox/up-b
$ openshell sandbox exec -n b --no-tty -- ls /sandbox/up-b
Expected: up-b in b only. Observed: up-b is in a, and not in b.
-
With the master from step 4 still up, openshell forward start --background 18080 b fails with ssh exited before local forward listener opened.
A PATH-first ssh wrapper that runs /usr/bin/ssh -o ControlMaster=no -o ControlPath=none "$@" makes both steps work (each upload lands in the
sandbox it names, and the forward starts), which isolates the cause to ssh
connection sharing.
Expected
Every openshell ssh session reaches the sandbox it names, whatever the user's
ssh configuration says about connection sharing.
Cause
The CLI builds its ssh command lines with -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -o LogLevel=ERROR (plus -o ConnectTimeout=15 -N -f -L … for forwards, and
-tt -o SetEnv=TERM=… for connect), a ProxyCommand of
openshell ssh-proxy --gateway <url> --sandbox <name> --token <token>, and the
host sandbox, but no ControlMaster, ControlPath or ControlPersist
options, so ssh takes them from the user's configuration. Because the host name
is the same for every sandbox, so is the expanded ControlPath, and ssh reuses
a master whose ProxyCommand named another sandbox. (A master's session token
is also only good for the sandbox it was issued for, so sharing is never
correct across sandboxes.)
Suggested fix
Pass connection-sharing options on every ssh command line the CLI builds; on
the command line they take precedence over every ssh_config file:
-o ControlMaster=no -o ControlPath=none -o ControlPersist=no
If the CLI wants multiplexing for speed, it should own it: a ControlPath in
the CLI's own state directory (owner-only) whose name includes the gateway,
workspace and sandbox (or a per-sandbox HostKeyAlias / host name such as
openshell-<gateway>-<workspace>-<sandbox>, so %C differs per sandbox), and
never the user's. The per-session token argues for simply turning sharing off.
The same applies to the ssh config that sandbox connect --editor installs,
if its host names are not already unique per sandbox.
Workaround used downstream
DefenseClaw runs every openshell command with a private directory (0700) first
on the child's PATH holding an ssh shim:
#!/bin/sh
exec /usr/bin/ssh -o ControlMaster=no -o ControlPath=none -o ControlPersist=no "$@"
(the real ssh resolved once from the original PATH), without changing the
user's ssh configuration. Its doctor checks ssh -G sandbox through the shim
and warns users whose own configuration shares connections for sandbox,
since openshell commands they run themselves are still affected. Users can
also add, above Host *:
Host sandbox
ControlMaster no
ControlPath none
🤖 Generated with Claude Code
Summary
The
openshellCLI opens its ssh sessions (sandbox connect,sandbox upload,sandbox download,forward start) by runningsshfromPATHwith aProxyCommandthat names the sandbox and a session token, and the literal hostname
sandboxfor every sandbox. The user's~/.ssh/configand/etc/ssh/ssh_configstill apply, and the CLI does not turn off OpenSSHconnection sharing. With a common multiplexing setup such as
every sandbox resolves to the same control socket (
%Chashes the local host,the host name
sandbox, the port and the user, which are the same for everysandbox). The first session becomes the master and stays up for
ControlPersist; every later ssh the CLI starts, whatever sandbox it names,connects to that master instead of running its own
ProxyCommand, and soreaches the first sandbox.
Observed on one Mac with two sandboxes, A and B, on one gateway:
openshell sandbox upload A …followed, within 10 minutes, byopenshell sandbox upload B …put B's files into A. The gateway logged aCreateSshSessionfor the second upload but noForwardTcp: the sessiontoken for B was created and never used.
openshell forward start --background <port> Bfailed withssh exited before local forward listener opened.By the same mechanism
openshell sandbox connect Bcan attach the terminal toA (not measured separately), which is the worst case: the user types into a
sandbox other than the one they named.
The effect is silent: no error, and the data lands in the wrong sandbox.
Environment
vm)driver; the CLI code path is driver-independent, so the docker driver and
Linux are affected the same way.
/usr/bin/ssh(OpenSSH_10.3p1, LibreSSL 3.3.6).~/.ssh/configwithHost *,ControlMaster auto,ControlPersist 10m,ControlPath ~/.ssh/cm-%C.Steps to reproduce
Put the
Host *block above in~/.ssh/config.Create two sandboxes,
aandb, on the same gateway.Confirm they share a control path:
Upload to each within ten minutes:
Expected:
up-binbonly. Observed:up-bis ina, and not inb.With the master from step 4 still up,
openshell forward start --background 18080 bfails withssh exited before local forward listener opened.A PATH-first
sshwrapper that runs/usr/bin/ssh -o ControlMaster=no -o ControlPath=none "$@"makes both steps work (each upload lands in thesandbox it names, and the forward starts), which isolates the cause to ssh
connection sharing.
Expected
Every
openshellssh session reaches the sandbox it names, whatever the user'sssh configuration says about connection sharing.
Cause
The CLI builds its ssh command lines with
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -o LogLevel=ERROR(plus-o ConnectTimeout=15 -N -f -L …for forwards, and-tt -o SetEnv=TERM=…for connect), aProxyCommandofopenshell ssh-proxy --gateway <url> --sandbox <name> --token <token>, and thehost
sandbox, but noControlMaster,ControlPathorControlPersistoptions, so ssh takes them from the user's configuration. Because the host name
is the same for every sandbox, so is the expanded
ControlPath, and ssh reusesa master whose
ProxyCommandnamed another sandbox. (A master's session tokenis also only good for the sandbox it was issued for, so sharing is never
correct across sandboxes.)
Suggested fix
Pass connection-sharing options on every ssh command line the CLI builds; on
the command line they take precedence over every ssh_config file:
If the CLI wants multiplexing for speed, it should own it: a
ControlPathinthe CLI's own state directory (owner-only) whose name includes the gateway,
workspace and sandbox (or a per-sandbox
HostKeyAlias/ host name such asopenshell-<gateway>-<workspace>-<sandbox>, so%Cdiffers per sandbox), andnever the user's. The per-session token argues for simply turning sharing off.
The same applies to the ssh config that
sandbox connect --editorinstalls,if its host names are not already unique per sandbox.
Workaround used downstream
DefenseClaw runs every
openshellcommand with a private directory (0700) firston the child's
PATHholding ansshshim:(the real ssh resolved once from the original
PATH), without changing theuser's ssh configuration. Its doctor checks
ssh -G sandboxthrough the shimand warns users whose own configuration shares connections for
sandbox,since
openshellcommands they run themselves are still affected. Users canalso add, above
Host *:🤖 Generated with Claude Code