---
title: "Run NVIDIA OpenShell on a remote VM"
description: "Run NVIDIA OpenShell on an exe.dev VM: Claude Code and Codex in policy-locked sandboxes, models through exe.dev with no API key, and a VM around it all."
url: https://exe-staging.dev/deploy/openshell
category: "AI agents"
updated: 2026-10-31
---

# Run NVIDIA OpenShell on a remote VM

Agents in OpenShell sandboxes, inside a VM of their own. No API keys on it.

- **Software:** OpenShell (https://docs.nvidia.com/openshell/latest/index.html)
- **Source:** https://github.com/NVIDIA/OpenShell
- **License:** Apache-2.0
- **Time:** 10 minutes
- **Example VM name:** `my-openshell` (yours will differ; on the VM, `hostname` prints it)
- **Machine-readable:** https://exe-staging.dev/deploy/openshell/manifest.json
- **File:** https://exe-staging.dev/deploy/openshell/Dockerfile
- **File:** https://exe-staging.dev/deploy/openshell/exe-llm-relay.conf
- **File:** https://exe-staging.dev/deploy/openshell/exe-llm.yaml

## What you get

- 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

Running on the VM:

- **OpenShell gateway**: openshell-gateway, a systemd user service; the control plane for sandboxes, policies, and providers, port 17670, data in `~/.local/state/openshell`
- **Sandboxes**: one Docker container each (or a microVM), from an image with Claude Code and Codex; no network of their own, data in `/sandbox in each sandbox`
- **LLM relay**: nginx on 127.0.0.1:11435, passing sandbox model calls to exe.dev's LLM integration

How it is reached:

- ssh my-openshell.exe.xyz (private): the `openshell` CLI and your agents' terminals
- https://my-openshell.exe.xyz:8080/ (private): optional, a web app running in a sandbox

## Deploy with Shelley

Open https://exe-staging.dev/new?prompt=Set%20up%20OpenShell%20on%20this%20VM%20by%20following%20the%20exe.dev%20recipe%20at%20https%3A%2F%2Fexe-staging.dev%2Fdeploy%2Fopenshell.md%20from%20top%20to%20bottom.%0A%0AThe%20recipe%27s%20example%20VM%20name%20is%20my-openshell.%20This%20VM%27s%20name%20is%20the%20output%20of%20%60hostname%60%3A%20use%20it%20wherever%20the%20recipe%20writes%20my-openshell.%20Skip%20the%20step%20that%20creates%20the%20VM%3B%20you%20are%20on%20it.%0A%0AYou%20are%20done%20when%20all%20of%20these%20are%20true.%20Check%20each%20one%20yourself%3A%0A-%20The%20OpenShell%20gateway%20running%20as%20a%20systemd%20user%20service%2C%20back%20after%20a%20reboot%2C%20with%20%60openshell%20status%60%20saying%20%60Connected%60%0A-%20An%20agent%20image%2C%20%60agents%3Alocal%60%2C%20with%20Claude%20Code%20and%20Codex%20set%20up%20for%20exe.dev%27s%20LLM%20integration%2C%20and%20no%20API%20key%20on%20the%20VM%0A-%20An%20%60exe-llm%60%20provider%20that%20lets%20sandboxes%20reach%20models%20through%20a%20relay%20on%20the%20VM%2C%20and%20nothing%20else%20on%20the%20internet%0A-%20A%20test%20sandbox%20in%20which%20Claude%20Code%20answered%2C%20then%20deleted%0A%0ARun%20%22Set%20up%20by%20hand%22%20from%20%22On%20the%20VM%22%2C%20skipping%20VM%20creation%20and%20SSH%2C%0Aincluding%20%22Check%20it%22.%20Don%27t%20start%20interactive%20agents%20or%20keep%20any%0Asandbox.%20Finish%20by%20telling%20the%20owner%20to%20SSH%20in%20and%20run%20the%20command%20in%0A%22Start%20an%20agent%22.%0A%0AFinish%20with%20the%20URLs%20I%20should%20open.%0A to create a VM whose agent, Shelley, follows this recipe. From a terminal:

```sh
curl -fsSL https://exe-staging.dev/deploy/openshell/prompt.txt | ssh exe.dev new --prompt=/dev/stdin
```

Or follow the steps below by hand.

[OpenShell](https://github.com/NVIDIA/OpenShell) 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](/docs/integrations-llm), so the VM holds no API keys.

## Start an agent

On the VM (`ssh my-openshell.exe.xyz`):

```bash
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:

```bash
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](https://docs.nvidia.com/openshell/latest/how-it-works/policies/overview).

## Set it up

<details id="create-the-vm" data-manual-setup class="dp-details">
<summary>Set up by hand</summary>

On your computer (any free VM name):

```bash
ssh exe.dev new --name=my-openshell
ssh my-openshell.exe.xyz
```

On the VM, start Docker at boot and add the LLM relay:

```bash
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):

```bash
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):

```bash
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

```bash
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`.

</details>

## 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](https://exe-staging.dev/deploy/openshell/exe-llm-relay.conf))
listens on the VM's loopback, and
[exe-llm.yaml](https://exe-staging.dev/deploy/openshell/exe-llm.yaml) lets the
agents reach it at `host.openshell.internal:11435`. The
[Dockerfile](https://exe-staging.dev/deploy/openshell/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`:

```bash
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](/docs/proxy):

```bash
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

<details class="dp-details">
<summary>Switch to microVMs</summary>

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.

```bash
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.

</details>

## Back up and update

<details class="dp-details">
<summary>Back up, update, and uninstall</summary>

Copy a sandbox's files to the VM (or push them from the sandbox). Gateway
state is in `~/.local/state/openshell`.

```bash
openshell sandbox download claude /sandbox ~/backup/claude
```

Update OpenShell by running the installer again (read the
[release notes](https://github.com/NVIDIA/OpenShell/releases) first;
running sessions are interrupted):

```bash
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:

```bash
systemctl --user disable --now openshell-gateway
sudo apt remove openshell
rm -rf ~/.local/state/openshell
```

</details>

## Troubleshooting

- **`openshell status` can't connect:**
  `journalctl --user -u openshell-gateway -n 50`.
- **Agent can't reach its API:** run `curl -s localhost:11435/v1/models`
  to check the relay. Check `openshell policy get <name> --full` for
  `_provider_exe_llm`; if it's missing,
  `openshell sandbox provider attach <name> exe-llm`. If `openshell logs
  <name>` shows `DENIED <program>`, add that program to `binaries` in
  `exe-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](/docs/integrations-llm) 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](https://github.com/NVIDIA/OpenShell/releases) before updating.


## Related

- [Give agents API access without giving them the keys](https://exe-staging.dev/deploy/agent-secrets.md): Your agents call your APIs. The keys stay with exe.dev, outside the VM.
- [Run AI-generated code safely](https://exe-staging.dev/deploy/code-interpreter.md): The model writes Python. A VM of its own runs it, keeps its variables, draws the charts.
- [Run Claude Code on a remote server](https://exe-staging.dev/deploy/claude-code.md): A computer for Claude. A breather for your laptop.
- [Run Codex on a remote server](https://exe-staging.dev/deploy/codex.md): A computer for Codex. An early night for your laptop.
- [Use exe.dev as a remote Docker host](https://exe-staging.dev/deploy/docker-host.md): Keep the docker CLI. Send the builds, the containers, and the fan noise to a VM.
- [Host OpenClaw](https://exe-staging.dev/deploy/openclaw.md): An AI sidekick with its own computer. Your laptop can take a nap.
