Run an Agent's Shell Commands in a Remote Sandbox Over SSH
Updated November 1, 2026 .md
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. For running a coding agent inside the VM instead, see Run Codex CLI on a remote machine.
1. Create the sandbox and a host entry#
sbx new --type small
sbx ssh-config # prints the stanza the CLI's tunnel needssbx 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):
# ~/.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 10mWhat the extra lines do:
ProxyCommand sbx proxy %hcarries 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).BatchMode yesmakessshfail instead of waiting for a password or a host key prompt that no agent will answer.ControlMaster,ControlPathandControlPersistkeep 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:
ssh agent-box 'whoami; pwd; uname -sr; echo $SHELL'
ssh agent-box 'exit 7'; echo "exit=$?"user
/home/user
Linux 6.18.56-sandboxer
/bin/bash
exit=7The 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:
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}'/home/user/proj
/home/user
FOO=bar
FOO=unsetFrameworks 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:
printf 'cd proj\nexport STEP=2\npwd\necho STEP=$STEP\n' | ssh agent-box bash -s/home/user/proj
STEP=2For 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 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:
ssh agent-box 'sleep 150; echo done-after-sleep'done-after-sleepTwo 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 localsshwithtimeout 3. It finished and wrote its file 8 seconds later. - With
-tt, killing the localsshalso 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:
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'tick 1
tick 2
tick 3The nohup line returned in 525 ms and ~/build.out held built later. tmux is installed in the sandbox. Long-running tasks 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:
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).
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-boxfailed in about 0.1 s withmachine_not_runningand 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 3is 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 thetmuxjob 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 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#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.