Run NVIDIA OpenShell on a remote VM
Agents in OpenShell sandboxes, inside a VM of their own. No API keys on it.
Shelley handles the setup. About 10 minutes.
Prefer a terminal?
curl -fsSL https://exe-staging.dev/deploy/openshell/prompt.txt | ssh exe.dev new --prompt=/dev/stdinOpenShell is NVIDIA's runtime for running agents safely. Each agent gets a sandbox with no network and limited file access. When it tries to reach something new, OpenShell blocks it and drafts a rule for you to approve.
Here it runs on an exe.dev VM: a second boundary, with its own kernel, around the sandboxes. Agents reach models through exe.dev's LLM integration, so the VM holds no API keys.
What you get#
- The OpenShell gateway running as a systemd user service, back after a reboot, with
openshell statussayingConnected - An agent image,
agents:local, with Claude Code and Codex set up for exe.dev's LLM integration, and no API key on the VM - An
exe-llmprovider that lets sandboxes reach models through a relay on the VM, and nothing else on the internet - A test sandbox in which Claude Code answered, then deleted
What's running, and where
ssh my-openshell.exe.xyz
the openshell CLI and your agents' terminals
https://my-openshell.exe.xyz:8080/
optional, a web app running in a sandbox
Deploy with Shelley#
Click Deploy and Shelley will set up OpenShell on a new VM, then check that it works. No config-file wrangling required.
Prefer a terminal?
curl -fsSL \
https://exe-staging.dev/deploy/openshell/prompt.txt \
| ssh exe.dev new --prompt=/dev/stdinWhat Shelley is told
Set up OpenShell on this VM by following the exe.dev recipe at https://exe-staging.dev/deploy/openshell.md from top to bottom.
The recipe's example VM name is my-openshell. This VM's name is the output of `hostname`: use it wherever the recipe writes my-openshell. Skip the step that creates the VM; you are on it.
You are done when all of these are true. Check each one yourself:
- The OpenShell gateway running as a systemd user service, back after a reboot, with `openshell status` saying `Connected`
- An agent image, `agents:local`, with Claude Code and Codex set up for exe.dev's LLM integration, and no API key on the VM
- An `exe-llm` provider that lets sandboxes reach models through a relay on the VM, and nothing else on the internet
- A test sandbox in which Claude Code answered, then deleted
Run "Set up by hand" from "On the VM", skipping VM creation and SSH,
including "Check it". Don't start interactive agents or keep any
sandbox. Finish by telling the owner to SSH in and run the command in
"Start an agent".
Finish with the URLs I should open.
Also at /deploy/openshell/prompt.txt. The recipe it follows is /deploy/openshell.md.
Start an agent#
On the VM (ssh my-openshell.exe.xyz):
openshell sandbox create --name claude --from agents:local --provider exe-llm -- claude
Detach with Ctrl-P Ctrl-Q; Claude keeps running. Reattach with
openshell sandbox connect claude. Use -- codex for Codex, or no
command for a shell. Your work is in /sandbox.
Other commands: openshell sandbox list, openshell sandbox delete <name>,
and openshell term (a dashboard).
Approve network access#
A new sandbox reaches the models and nothing else. When something is
blocked, say a git clone, OpenShell drafts a rule:
openshell rule get claude --status pending
openshell rule approve claude --chunk-id <chunk-id>
The retry works without a restart. Rules apply to one program: allowing
github.com for git doesn't allow it for curl. openshell rule approve-all claude approves everything pending, and openshell logs claude
shows each connection. See OpenShell's
policy docs.
Set it up#
Set up by hand
On your computer (any free VM name):
ssh exe.dev new --name=my-openshell
ssh my-openshell.exe.xyz
On the VM, start Docker at boot and add the LLM relay:
sudo systemctl enable --now docker.service
sudo curl -fsSL -o /etc/nginx/conf.d/exe-llm-relay.conf \
https://exe-staging.dev/deploy/openshell/exe-llm-relay.conf
sudo systemctl enable nginx && sudo systemctl restart nginx
curl -s localhost:11435/v1/models | head -c 80; echo
Expect JSON listing models. Install OpenShell (deb skips a snap check
that hangs for two minutes):
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh \
| OPENSHELL_INSTALL_METHOD=deb sh
openshell status
Expect Status: Connected. Add the exe-llm provider and build the agent
image (about two minutes):
mkdir -p ~/openshell && cd ~/openshell
for f in exe-llm.yaml Dockerfile; do
curl -fsSLO https://exe-staging.dev/deploy/openshell/$f
done
openshell profile import --file exe-llm.yaml
openshell provider create --name exe-llm --type exe-llm
docker build -t agents:local .
Check it#
openshell sandbox create --name check --no-keep --from agents:local --provider exe-llm \
-- claude -p "Reply with exactly: claude ok"
Expect claude ok, then Deleted sandbox check.
How it works#
- The gateway (
openshell-gateway, a systemd user service) stores sandboxes, policies, and providers in~/.local/state/openshell. - A supervisor beside each sandbox routes all of its traffic and checks every connection against the policy.
- The sandbox is a Docker container with no network interface, plus Landlock and seccomp.
Sandboxes can't reach llm.int.exe.xyz directly, because OpenShell blocks
link-local addresses. A keyless nginx relay
(exe-llm-relay.conf)
listens on the VM's loopback, and
exe-llm.yaml lets the
agents reach it at host.openshell.internal:11435. The
Dockerfile installs Claude
Code and Codex and points both at the relay.
Open a web app in a sandbox#
Expose a port, here 3000 in the sandbox claude:
openshell service expose claude 3000
That URL works only on the VM. To serve it at
https://my-openshell.exe.xyz:8080/, private to you through the
HTTPS proxy:
sudo tee /etc/nginx/conf.d/openshell-web.conf > /dev/null <<'EOF'
server {
listen 8080;
location / {
proxy_pass http://127.0.0.1:17670;
proxy_set_header Host default--claude.openshell.localhost:17670;
proxy_set_header Origin "";
proxy_set_header Referer "";
proxy_set_header Sec-Fetch-Site "";
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}
}
EOF
sudo nginx -t && sudo systemctl reload nginx
Clearing the origin headers is needed because OpenShell rejects cross-origin requests.
Run sandboxes as microVMs#
Switch to microVMs
Each sandbox can run in its own small VM (libkrun on KVM) instead of a
container. Limits: 2 vCPUs, 2 GB of memory, and 4 GB of disk each; about a
minute to start the first one. After a reboot, sandboxes show Error
until you run openshell sandbox stop <name> and then start (files
are kept). Delete Docker sandboxes before you switch, because the gateway
forgets them.
sudo usermod -aG kvm exedev
mkdir -p ~/.config/openshell
cat > ~/.config/openshell/gateway.toml <<'EOF'
[openshell]
version = 2
[openshell.gateway]
compute_driver = "vm"
EOF
openshell-gateway config preflight --path ~/.config/openshell/gateway.toml
sudo systemctl restart user@$(id -u).service
Then run "Check it" again. To switch back, delete gateway.toml and
restart the same way.
Back up and update#
Back up, update, and uninstall
Copy a sandbox's files to the VM (or push them from the sandbox). Gateway
state is in ~/.local/state/openshell.
openshell sandbox download claude /sandbox ~/backup/claude
Update OpenShell by running the installer again (read the release notes first; running sessions are interrupted):
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh \
| OPENSHELL_INSTALL_METHOD=deb sh
Update the agents with docker build --no-cache -t agents:local ~/openshell; only new sandboxes use the new image. To uninstall:
systemctl --user disable --now openshell-gateway
sudo apt remove openshell
rm -rf ~/.local/state/openshell
Troubleshooting#
openshell statuscan't connect:journalctl --user -u openshell-gateway -n 50.- Agent can't reach its API: run
curl -s localhost:11435/v1/modelsto check the relay. Checkopenshell policy get <name> --fullfor_provider_exe_llm; if it's missing,openshell sandbox provider attach <name> exe-llm. Ifopenshell logs <name>showsDENIED <program>, add that program tobinariesinexe-llm.yaml. - Installs fail inside a sandbox: that's the policy. Approve the pending rule (above).
Frequently asked questions#
Why put OpenShell in a VM when it's already a sandbox?
Defense in depth. OpenShell's sandboxes share a kernel with the gateway. On exe.dev that kernel belongs to a VM holding none of your keys or files, so an escape gets nothing, and deleting the VM removes it.
Where are the API keys?
Not on the VM. exe.dev's LLM integration adds them outside it and bills your account. Keys you give other OpenShell providers stay with the gateway; sandboxes see placeholders.
Which agents does it run?
Whatever is in the image: here, Claude Code and Codex. Build your own and pass it to --from. Programs that call models need their path under binaries in exe-llm.yaml.
Do sandboxes survive a reboot?
Yes, with their /sandbox files and approved rules, Ready again in about a minute. The agent restarts with a new session. Deleting a sandbox deletes its files.
Is OpenShell stable?
It's pre-1.0 with weekly releases, and 0.1.0 broke compatibility with earlier versions. We tested 0.1.6. Read the release notes before updating.
Updated Oct 31, 2026 · tested end to end Oct 31, 2026 · Markdown · manifest.json · Something wrong? Tell us