Skip to content

Run AI-generated code safely

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

Deploy on exe.dev

Shelley handles the setup. About 10 minutes.

Prefer a terminal?
curl -fsSL https://exe-staging.dev/deploy/code-interpreter/prompt.txt | ssh exe.dev new --prompt=/dev/stdin

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, 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 (the app and the tool-calling loop), sandbox.py (a sandbox VM with a kernel, using exe.dev's Python SDK), and index.html.

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
What's running, and where
Private https://my-code-interpreter.exe.xyz/ the chat, for you and people you share the VM with
my-code-interpreter.exe.xyz exe.dev VM
Code interpreter app.py (FastAPI), run by uv as code-interpreter.service; the model through exe.dev's LLM gateway :8000~/code-interpreter/data
Sandbox VMs one per conversation, copied from a base VM, each running an IPython kernel ~/work on each sandbox

Deploy with Shelley#

Click Deploy and Shelley will set up Code interpreter on a new VM, then check that it works. No config-file wrangling required.

Have these ready; Shelley asks for them:

  • EXE_TOKEN: the exe.dev API key that ssh exe.dev ssh-key generate-api-key printed, exe1....
Prefer a terminal?
curl -fsSL \
  https://exe-staging.dev/deploy/code-interpreter/prompt.txt \
  | ssh exe.dev new --prompt=/dev/stdin
What Shelley is told
EXE_TOKEN=<fill-this-in>

Set up Code interpreter on this VM by following the exe.dev recipe at https://exe-staging.dev/deploy/code-interpreter.md from top to bottom.

The values at the top of this message are mine:
- EXE_TOKEN: the exe.dev API key that `ssh exe.dev ssh-key generate-api-key` printed, `exe1....`
Ask me for any that still say <fill-this-in> before you need them.

The recipe's example VM name is my-code-interpreter. This VM's name is the output of `hostname`: use it wherever the recipe writes my-code-interpreter. 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 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

Run "Set up by hand" from "On the VM", skipping VM creation and SSH.
EXE_TOKEN is a secret: put it only in /etc/code-interpreter.env, and
never print it. If the health check shows 401 or `invalid token`, tell
the owner the key was refused and how to make one ("Before you start").
Then run "Check it from the VM". Finish by pointing the owner to "Ask
it something".

Finish with the URLs I should open.

Also at /deploy/code-interpreter/prompt.txt. The recipe it follows is /deploy/code-interpreter.md.

Before you start#

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

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:

Set up by hand

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

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

On the VM, download the app:

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

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

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:

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:

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:

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.

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, 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:

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 is the whole tool-calling loop in 50 lines, with a sandbox for one question. On an exe.dev VM, beside sandbox.py:

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); %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.

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:

cd ~/code-interpreter
for f in app.py sandbox.py index.html ask.py; do
  curl -fsSLO https://exe-staging.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. 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), paused or not. Model calls are billed to your exe.dev account through the LLM integration. 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.

Updated Oct 28, 2026 · Markdown · manifest.json · Something wrong? Tell us