---
title: "Run AI-generated code safely"
description: "A private code interpreter on exe.dev: a model writes Python, and each conversation's own sandbox VM runs it in a Jupyter kernel and returns charts."
url: https://exe.dev/deploy/code-interpreter
category: "AI agents"
updated: 2026-10-28
---

# Run AI-generated code safely

The model writes Python. A VM of its own runs it, keeps its variables, draws the charts.

- **Software:** Code interpreter (https://ipython.readthedocs.io)
- **Source:** https://github.com/boldsoftware/exe-sdk-python
- **License:** MIT (exedev SDK), BSD-3-Clause (IPython)
- **Time:** 10 minutes
- **Example VM name:** `my-code-interpreter` (yours will differ; on the VM, `hostname` prints it)
- **Machine-readable:** https://exe.dev/deploy/code-interpreter/manifest.json
- **File:** https://exe.dev/deploy/code-interpreter/app.py
- **File:** https://exe.dev/deploy/code-interpreter/ask.py
- **File:** https://exe.dev/deploy/code-interpreter/index.html
- **File:** https://exe.dev/deploy/code-interpreter/sandbox.py

## What you get

- The code interpreter at https://my-code-interpreter.exe.xyz/, private behind exe.dev login, as code-interpreter.service, restarted on failure and after a reboot
- The exe.dev API key in /etc/code-interpreter.env, mode 600, and on no other VM
- `curl -s localhost:8000/api/health` on the VM answers `"exe":"ok"` and `"llm":"ok"`
- A test question answered by code run in a sandbox VM, and that test conversation and its sandbox deleted

Running on the VM:

- **Code interpreter**: app.py (FastAPI), run by uv as code-interpreter.service; the model through exe.dev's LLM gateway, port 8000, data in `~/code-interpreter/data`
- **Sandbox VMs**: one per conversation, copied from a base VM, each running an IPython kernel, data in `~/work on each sandbox`

How it is reached:

- https://my-code-interpreter.exe.xyz/ (private): the chat, for you and people you share the VM with

## Deploy with Shelley

Open https://exe.dev/new?prompt=EXE_TOKEN%3D%3Cfill-this-in%3E%0A%0ASet%20up%20Code%20interpreter%20on%20this%20VM%20by%20following%20the%20exe.dev%20recipe%20at%20https%3A%2F%2Fexe.dev%2Fdeploy%2Fcode-interpreter.md%20from%20top%20to%20bottom.%0A%0AThe%20values%20at%20the%20top%20of%20this%20message%20are%20mine%3A%0A-%20EXE_TOKEN%3A%20the%20exe.dev%20API%20key%20that%20%60ssh%20exe.dev%20ssh-key%20generate-api-key%60%20printed%2C%20%60exe1....%60%0AAsk%20me%20for%20any%20that%20still%20say%20%3Cfill-this-in%3E%20before%20you%20need%20them.%0A%0AThe%20recipe%27s%20example%20VM%20name%20is%20my-code-interpreter.%20This%20VM%27s%20name%20is%20the%20output%20of%20%60hostname%60%3A%20use%20it%20wherever%20the%20recipe%20writes%20my-code-interpreter.%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%20code%20interpreter%20at%20https%3A%2F%2Fmy-code-interpreter.exe.xyz%2F%2C%20private%20behind%20exe.dev%20login%2C%20as%20code-interpreter.service%2C%20restarted%20on%20failure%20and%20after%20a%20reboot%0A-%20The%20exe.dev%20API%20key%20in%20%2Fetc%2Fcode-interpreter.env%2C%20mode%20600%2C%20and%20on%20no%20other%20VM%0A-%20%60curl%20-s%20localhost%3A8000%2Fapi%2Fhealth%60%20on%20the%20VM%20answers%20%60%22exe%22%3A%22ok%22%60%20and%20%60%22llm%22%3A%22ok%22%60%0A-%20A%20test%20question%20answered%20by%20code%20run%20in%20a%20sandbox%20VM%2C%20and%20that%20test%20conversation%20and%20its%20sandbox%20deleted%0A%0ARun%20%22Set%20up%20by%20hand%22%20from%20%22On%20the%20VM%22%2C%20skipping%20VM%20creation%20and%20SSH.%0AEXE_TOKEN%20is%20a%20secret%3A%20put%20it%20only%20in%20%2Fetc%2Fcode-interpreter.env%2C%20and%0Anever%20print%20it.%20If%20the%20health%20check%20shows%20401%20or%20%60invalid%20token%60%2C%20tell%0Athe%20owner%20the%20key%20was%20refused%20and%20how%20to%20make%20one%20%28%22Before%20you%20start%22%29.%0AThen%20run%20%22Check%20it%20from%20the%20VM%22.%20Finish%20by%20pointing%20the%20owner%20to%20%22Ask%0Ait%20something%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.dev/deploy/code-interpreter/prompt.txt | ssh exe.dev new --prompt=/dev/stdin
```

Or follow the steps below by hand.

A code interpreter lets a model answer with code: it writes Python, a
sandbox runs it, and the model reads the output and goes on.

This recipe builds one on exe.dev. A small chat app on one VM reaches the
model through the [LLM gateway](/docs/integrations-llm), with no API key,
and never runs the model's code itself. Each conversation gets a sandbox VM
of its own with a Jupyter kernel: variables carry over between cells, charts
come back as images, and you can upload files and download what the model
saves. The code is three short files:
[app.py](https://exe.dev/deploy/code-interpreter/app.py) (the app and the
tool-calling loop),
[sandbox.py](https://exe.dev/deploy/code-interpreter/sandbox.py) (a sandbox
VM with a kernel, using exe.dev's [Python SDK](/deploy/python-sandboxes)),
and [index.html](https://exe.dev/deploy/code-interpreter/index.html).

## Before you start

The app makes sandbox VMs with an exe.dev API key that can touch nothing
else. On your computer:

```bash
ssh exe.dev ssh-key generate-api-key --label=code-interpreter --tag=code-sandbox \
  --cmds=new,ls,rm,ssh,cp,pause,resume,comment --exp=1y
```

It prints a key that starts with `exe1.`. Every VM it creates is tagged
`code-sandbox`, and it can see and touch no VM without that tag, including
the app's own. To revoke it, `ssh exe.dev ssh-key remove code-interpreter`.

## Set it up

Deploy sets it up on a new VM and asks for the key. To do it yourself:

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

On your computer, create the VM (any free name) and connect:

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

On the VM, download the app:

```bash
mkdir -p ~/code-interpreter && cd ~/code-interpreter
for f in app.py sandbox.py index.html ask.py; do
  curl -fsSLO https://exe.dev/deploy/code-interpreter/$f
done
```

Put the key in a file only root can read, filling in yours:

```bash
sudo install -m 600 /dev/null /etc/code-interpreter.env
sudo tee /etc/code-interpreter.env > /dev/null <<'EOF'
EXE_TOKEN=exe1....
EOF
```

Run it as a service on port 8000:

```bash
sudo tee /etc/systemd/system/code-interpreter.service > /dev/null <<'EOF'
[Unit]
Description=Code interpreter (a sandbox VM per conversation)
Wants=network-online.target
After=network-online.target

[Service]
User=exedev
WorkingDirectory=/home/exedev/code-interpreter
EnvironmentFile=/etc/code-interpreter.env
ExecStart=/usr/local/bin/uv run --script app.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now code-interpreter
```

The first start downloads the app's packages, which takes a few seconds.
Then check that both the key and the model work:

```bash
sleep 15; curl -s localhost:8000/api/health
```

Expect `{"sandboxes":0,"exe":"ok","llm":"ok"}`.

### Check it from the VM

This asks one question in a new conversation, which starts the base VM
and a sandbox, then deletes the conversation and its sandbox:

```bash
ID=$(curl -s -X POST localhost:8000/api/conversations | jq -r .id)
curl -sN localhost:8000/api/conversations/$ID/messages \
  -H 'Content-Type: application/json' \
  -d '{"text": "What is the 1000th prime? Compute it."}' | jq -c '{type, text, code, stdout}'
curl -s -X DELETE localhost:8000/api/conversations/$ID
```

Expect lines of `code` and `output`, and an `assistant` line that says
7919. The first question takes about 20 seconds, because the base VM is
set up first; after that, a new conversation's sandbox is ready in about 6.

</details>

## Ask it something

Open https://my-code-interpreter.exe.xyz/ and sign in with exe.dev. Try
"Simulate 10,000 rolls of two dice and plot the distribution of the sums",
or click **Upload** to send a CSV and ask about it. You see each cell the
model runs, its output, and its charts as they happen, and the files it
saves as download links. Follow-ups carry on with the data already loaded.

Only you can open the page until you [share the VM](/docs/sharing), and
everyone it's shared with can run code on your account.

## How it works

- **The first cell** of a conversation creates its sandbox, a copy of a
  base VM that the app sets up once with Python packages. It's ready in
  about 6 seconds.
- **Each cell** runs in the sandbox's IPython kernel in about half a second
  plus the code's own time. The model sees the output, the last value, and
  any error; charts go to you. A cell past `CELL_TIMEOUT` is interrupted, as
  with Ctrl-C, and the kernel keeps its variables.
- **Idle sandboxes** pause after 30 minutes, keeping the kernel's memory,
  and resume at the next cell. After 3 idle days, or when you delete the
  conversation, the sandbox is deleted.
- **Conversations** are JSON files in `~/code-interpreter/data`.

## Use it in your own code

`sandbox.py` works without the app. Put it beside your code and
`uv add exedev`:

```python
import exedev
from sandbox import Sandbox

exe = exedev.Client()  # reads EXE_TOKEN

with Sandbox.create(exe) as sb:  # a new VM, deleted at the end of the block
    sb.run_code("import pandas as pd\ndf = pd.DataFrame({'x': [1, 2, 3]})")
    print(sb.run_code("df.x.sum()").text)  # np.int64(6)
    print(len(sb.run_code("df.x.plot()").pngs))  # 1, a base64 PNG
```

[ask.py](https://exe.dev/deploy/code-interpreter/ask.py) is the whole
tool-calling loop in 50 lines, with a sandbox for one question. On an
exe.dev VM, beside sandbox.py:

```bash
EXE_TOKEN=exe1.... uv run ask.py "What is the 1000th prime, and the sum of its digits?"
```

## Lock it down

A sandbox is private like any VM and can't reach the app's VM or the key.
It can reach:

- **The internet.** `SANDBOX_EGRESS=integrations` cuts new sandboxes off
  (see [Egress policy](/docs/internet-access)); `%pip install` then fails,
  so add packages to `PACKAGES` in sandbox.py first.
- **Integrations attached to `auto:all`**, such as the LLM gateway (billed
  to you), even with `SANDBOX_EGRESS=integrations`. To keep them off
  sandboxes, attach them by tag to your own VMs instead; see
  [Attaching integrations](/docs/integrations-attach).

## Settings

In /etc/code-interpreter.env, then `sudo systemctl restart code-interpreter`:

| Setting | Default | What it does |
| --- | --- | --- |
| `MODEL` | `gpt-5.5` | The model, from `curl https://llm.int.exe.xyz/v1/models` |
| `SANDBOX_EGRESS` | (all) | `integrations` cuts new sandboxes off the internet |
| `PAUSE_AFTER_MINUTES` | 30 | Pause a sandbox after this long idle |
| `DELETE_AFTER_DAYS` | 3 | Delete a sandbox after this long idle |
| `CELL_TIMEOUT` | 300 | Seconds before a cell is interrupted |

## Back up and update

Conversations are in `~/code-interpreter/data`; back them up with
`tar czf ~/code-interpreter-$(date +%F).tgz -C ~/code-interpreter data` and
copy the file off the VM. Sandboxes aren't backed up.

To update, on the VM:

```bash
cd ~/code-interpreter
for f in app.py sandbox.py index.html ask.py; do
  curl -fsSLO https://exe.dev/deploy/code-interpreter/$f
done
sudo systemctl restart code-interpreter
```

After changing `PACKAGES`, delete the base VM (`ssh exe.dev ls` shows it
with the comment `code-interpreter-base`), and the next sandbox sets up a
new one.

## Troubleshooting

`journalctl -u code-interpreter` is the app's log, and
`curl -s localhost:8000/api/health` checks the key and the model.

- `401` or `invalid token`: the key is wrong or expired. Make a new one and
  restart.
- `command not allowed by ... permissions`: the key is missing a command
  from the `--cmds` in [Before you start](#before-you-start).
  Without `pause`, idle sandboxes keep running until they're deleted.
- A VM limit error: delete old conversations, or other VMs.
- `no model gpt-5.5`: set `MODEL` to one the gateway lists.

## Frequently asked questions

### How is it different from E2B?

E2B runs sandboxes on its servers and bills them by the second, with a time limit on each. Here each sandbox is an ordinary exe.dev VM on your plan: it lasts until you delete it, pauses with its kernel's memory intact, and your app and your sandboxes stay in one account. `run_code()` returns the same kind of result as E2B's: output, the last value, PNGs, and the error.

### Can the code reach the internet?

Yes, by default, so `%pip install` and downloads work. Set `SANDBOX_EGRESS=integrations` to cut new sandboxes off; then only the preinstalled packages work.

### What does it cost?

Sandboxes share your plan's CPU and memory rather than being billed one by one, but each counts toward the plan's VM limit (50 on [Personal](/docs/billing/personal)), paused or not. Model calls are billed to your exe.dev account through the [LLM integration](/docs/integrations-llm). Delete conversations you're done with, and their sandboxes go too.

### Which models can it use?

Any model the LLM gateway serves through the OpenAI Chat Completions API with tool calls: `gpt-5.5` by default, and others from `curl https://llm.int.exe.xyz/v1/models`, such as `fireworks/glm-5p3`. Set `MODEL` in /etc/code-interpreter.env.


## Related

- [Create sandboxes from Python](https://exe.dev/deploy/python-sandboxes.md): Full VMs as sandboxes. A with block creates one, runs your code, and deletes it.
- [Create sandboxes from TypeScript](https://exe.dev/deploy/typescript-sandboxes.md): Full VMs as sandboxes. Create, control, and destroy them from your code.
- [Self-host Open WebUI](https://exe.dev/deploy/open-webui.md): A private ChatGPT for you and your team. No API keys.
- [Self-hosted sandbox for Claude Managed Agents](https://exe.dev/deploy/claude-managed-agents.md): Anthropic runs the agent. Each session's tools run on a VM of its own.
- [Running Agents](https://exe.dev/deploy/agents.md): Use exe.dev VMs as a sandbox
- [Run Claude Code on a remote server](https://exe.dev/deploy/claude-code.md): A computer for Claude. A breather for your laptop.
