# Run an Agent's Shell Commands in a Remote Sandbox Over SSH

> Add a Host entry to ~/.ssh/config that tunnels to the sandbox through sbx proxy, then give your framework's SSH backend that host name. It runs each command as ssh host 'cmd'. Add ControlMaster to reuse one connection, which cut each command from about 0.53 s to 0.07 s in our 2026-11-01 test.

Source: https://sandboxer.com/guides/run-an-agents-shell-commands-in-a-remote-sandbox. Updated 2026-11-01.

**Short answer:** Add a Host entry to ~/.ssh/config that tunnels to the sandbox through sbx proxy, then give your framework's SSH backend that host name. It runs each command as ssh host 'cmd'. Add ControlMaster to reuse one connection, which cut each command from about 0.53 s to 0.07 s in our 2026-11-01 test.

Many agent frameworks let you choose where shell commands run: on your machine, in a container, or on a host over SSH. The SSH option needs only a host name your system `ssh` can reach without a prompt. This page sets that up for a Sandboxer sandbox, then tests what an agent's SSH backend actually does: one-shot commands, a working directory, environment variables, long jobs, file transfer, and a stop and resume. Everything below ran on prod on 2026-11-01 from a Linux machine with OpenSSH 10.2, against a `small` sandbox.

For the general SSH setup and editors, see [SSH into a cloud sandbox from VS Code or Cursor](/guides/ssh-into-a-cloud-sandbox-from-vs-code-or-cursor). For running a coding agent inside the VM instead, see [Run Codex CLI on a remote machine](/guides/run-codex-cli-on-a-remote-machine).

## 1. Create the sandbox and a host entry

```bash
sbx new --type small
sbx ssh-config          # prints the stanza the CLI's tunnel needs
```

`sbx ssh-config` prints a `Host *.sbx` block. Copy its lines into a named entry for your agent, so the framework only needs one word as its host. Paths must be absolute on your machine (here `/home/you` and `/usr/local/bin/sbx`):

```ini
# ~/.ssh/config
Host agent-box
  HostName sb_spayp0bfj4.sbx
  User user
  IdentityFile /home/you/.ssh/sandboxer_ed25519
  IdentitiesOnly yes
  ProxyCommand /usr/local/bin/sbx proxy %h
  UserKnownHostsFile /home/you/.config/sandboxer/known_hosts
  StrictHostKeyChecking yes
  ServerAliveInterval 30
  ServerAliveCountMax 3
  BatchMode yes
  ControlMaster auto
  ControlPath ~/.ssh/cm-%C
  ControlPersist 10m
```

What the extra lines do:

- `ProxyCommand sbx proxy %h` carries SSH over an authenticated WebSocket, so only outbound HTTPS is needed. Before each connection it authorizes the CLI key and pins the sandbox's host key ([SSH docs](/docs/ssh)).
- `BatchMode yes` makes `ssh` fail instead of waiting for a password or a host key prompt that no agent will answer.
- `ControlMaster`, `ControlPath` and `ControlPersist` keep one connection open and run later commands over it.

The `ProxyCommand` uses the CLI's saved sign-in, or `SANDBOXER_API_KEY` if it's set in the environment that starts `ssh`. Run `sbx login` once on the machine where the framework runs.

## 2. Run commands the way an SSH backend does

An SSH backend runs each tool call as a non-interactive `ssh host 'command'` and reads stdout, stderr and the exit code:

```bash
ssh agent-box 'whoami; pwd; uname -sr; echo $SHELL'
ssh agent-box 'exit 7'; echo "exit=$?"
```

```text
user
/home/user
Linux 6.18.56-sandboxer
/bin/bash
exit=7
```

The remote exit code comes back unchanged, so the framework can tell a failed test from a passing one.

## 3. Measure: with and without ControlMaster

We ran `ssh <host> true` ten times in a row, once against an entry without the `Control*` lines and once with them (milliseconds, 2026-11-01):

| Entry | Run 1 | Runs 2–10 |
| - | - | - |
| No ControlMaster | 528 | 525–537 |
| ControlMaster auto | 527 | 66–68 |

Without it, every command pays for a new tunnel, key exchange and authorization, about half a second. With it, only the first command does. An agent that runs 200 shell commands in a session saves well over a minute of waiting. For comparison, `sbx exec <id> true` took about 107 ms per call and `sbx ssh <id> true` about 535 ms.

The sandbox's SSH server allows 10 sessions per connection (`MaxSessions 10`). When we started 12 commands at once over one master, 10 used it. The other 2 printed `Session open refused by peer`, fell back to their own connections, and still succeeded. If your framework runs many commands in parallel, expect that warning on stderr.

## 4. Working directory and environment variables

Each `ssh host 'cmd'` starts a new shell. Nothing you set in one command is there in the next:

```bash
ssh agent-box 'mkdir -p proj && cd proj && pwd'
ssh agent-box 'pwd'
ssh agent-box 'export FOO=bar; echo FOO=$FOO'
ssh agent-box 'echo FOO=${FOO:-unset}'
```

```text
/home/user/proj
/home/user
FOO=bar
FOO=unset
```

Frameworks handle this in one of two ways. Some track the directory themselves and prefix every command with `cd <dir> &&`. Others keep one remote shell open and write commands to its stdin, so state carries over:

```bash
printf 'cd proj\nexport STEP=2\npwd\necho STEP=$STEP\n' | ssh agent-box bash -s
```

```text
/home/user/proj
STEP=2
```

For variables every command needs, these worked:

| Method | Result in our test |
| - | - |
| `export` line at the very top of `~/.bashrc` | Seen by every `ssh host 'cmd'` |
| `export` lines in a file, run as `. ~/.agent_env && cmd` | Works; explicit per command |
| `ssh -o SetEnv=NAME=value` | Not passed: the server only accepts `LANG` and `LC_*` |

The `~/.bashrc` line must go above the `case $- in` check, because the default file returns early for non-interactive shells. Keep secrets out of these files, since the agent can read them. [Keep API keys out of an agent sandbox](/guides/keep-api-keys-out-of-an-agent-sandbox) covers that.

## 5. Long-running commands

A plain `ssh host 'cmd'` has no time limit of its own. A command that printed nothing for 150 seconds still returned normally:

```bash
ssh agent-box 'sleep 150; echo done-after-sleep'
```

```text
done-after-sleep
```

Two details matter when a framework enforces its own timeout by killing `ssh`:

- **Without a terminal** (the default for `ssh host 'cmd'`), the remote command kept running after we killed the local `ssh` with `timeout 3`. It finished and wrote its file 8 seconds later.
- **With `-tt`**, killing the local `ssh` also ended the remote command.

To stop a remote command on time, put the limit on the remote side: `ssh agent-box 'timeout 300 npm test'`.

For jobs that should outlive the connection, detach them. Redirect stdin, stdout and stderr so `ssh` can return right away:

```bash
ssh agent-box 'nohup bash -c "sleep 20; echo built > ~/build.out" > ~/build.log 2>&1 < /dev/null &'
ssh agent-box 'tmux new -d -s job "for i in 1 2 3; do echo tick \$i; sleep 1; done; sleep 600"'
ssh agent-box 'tmux capture-pane -p -t job | head -3'
```

```text
tick 1
tick 2
tick 3
```

The `nohup` line returned in 525 ms and `~/build.out` held `built` later. `tmux` is installed in the sandbox. [Long-running tasks](/docs/long-running) covers the CLI's own detached commands.

## 6. Move files with rsync and scp

`rsync` is installed in the sandbox, and both tools read the same host entry:

```bash
rsync -a --stats ./app/ agent-box:app/
scp agent-box:app/blob.bin ./
```

We synced a folder of 201 files (20 MB) and then changed one file (2026-11-01):

| Transfer | Time |
| - | - |
| First `rsync` upload, 201 files, 20 MB | 0.79 s |
| Second `rsync` after one small change | 0.28 s, 1 file sent |
| `scp` upload, 20 MB file | 1.3 s |

For a one-off copy without a host entry, `sbx scp` does the same job ([SSH docs](/docs/ssh)).

## 7. What happens after stop and resume

We stopped the sandbox with `sbx stop --wait` while the ControlMaster connection was open:

- **While stopped**, a new `ssh agent-box` failed in about 0.1 s with `machine_not_running` and exit code 255. But a command sent over the still-open master **hung**: we cut it off after 20 seconds.
- **After a short stop** (under a minute), the old master carried commands again after `sbx resume`, at the same 67 ms.
- **After a 130-second stop**, the master had already closed itself (`ServerAliveInterval 30` × `ServerAliveCountMax 3` is about 90 seconds). The next command opened a new one in 664 ms.
- **Host key:** the same `SHA256:u+K3hN…` fingerprint before and after both resumes, and memory was restored (`memoryRestored: true`), so the `tmux` job from step 5 was still listed.

So an agent should not send commands to a stopped sandbox. Have the orchestrator resume it first (`sbx resume <id>`), or remove the master with `ssh -O exit agent-box` before a stop. A [fork](/guides/pause-and-resume-a-sandbox-with-memory) is a different sandbox with new SSH host keys, so give it its own host entry.

## Checklist for any framework's SSH backend

| Setting | Value for a sandbox |
| - | - |
| Host | A named entry like `agent-box` in `~/.ssh/config`, with `HostName <id>.sbx` |
| User | `user` (home `/home/user`, `sudo` available) |
| Key | `~/.ssh/sandboxer_ed25519`, created and authorized by the CLI |
| Port | None with the `ProxyCommand` tunnel; for the direct route, the port from `sbx info`'s `SSH endpoint` |
| Proxy | `ProxyCommand /path/to/sbx proxy %h`, with `sbx` signed in on that machine |
| Prompts | `BatchMode yes`; host key pinned by `sbx proxy` |
| Keepalive | `ServerAliveInterval 30`, `ServerAliveCountMax 3` |
| Speed | `ControlMaster auto`, `ControlPath ~/.ssh/cm-%C`, `ControlPersist 10m` |
| Timeouts | Wrap commands in remote `timeout`; killing local `ssh` doesn't stop them |

If the framework asks for host, port, user and key path instead of a host alias, use the IPv4 `sshEndpoint` from `sbx info` with `user` and the key path. That port belongs to the running sandbox, so check it again after a resume. In our test it stayed the same.

## FAQ

### Do I need to open a port or copy a key into the sandbox?

No. The `sbx proxy` tunnel uses outbound HTTPS. Each connection authorizes the CLI's key and pins the host key, so `StrictHostKeyChecking yes` holds without a prompt.

### Why is ssh slower than sbx exec?

Each new SSH connection sets up a tunnel, a key exchange and authorization, about 0.53 s in our test. `sbx exec` is a single API call, about 0.11 s. With ControlMaster, SSH commands after the first took about 0.07 s.

### Does the agent's working directory carry over between commands?

Not with one `ssh host 'cmd'` per command: each starts in `/home/user`. Prefix commands with `cd <dir> &&`, or keep one `ssh host bash -s` session open and feed it commands.

### Will a stopped sandbox wake up when the agent connects?

No. A new connection fails at once with `machine_not_running`, and a command over an existing ControlMaster hangs. Resume the sandbox first with `sbx resume <id>`.

### Can I run several agents against one sandbox?

Yes, but one ControlMaster connection carries at most 10 sessions. Extra commands open their own connections and print a warning. For separate workspaces, give each agent its own sandbox and host entry.

## Related

- [SSH and file transfer](/docs/ssh)
- [SSH into a cloud sandbox from VS Code or Cursor](/guides/ssh-into-a-cloud-sandbox-from-vs-code-or-cursor)
- [Run Codex CLI on a remote machine](/guides/run-codex-cli-on-a-remote-machine)
- [Long-running tasks](/docs/long-running)
