On this page · 6 min
An agent writes a script. The script imports a package nobody on your team has heard of, let alone read. Something now has to run it.
At that point you are making a bet, and the bet is not really about whether the code turns out to be hostile. It is about where the damage stops. The sandbox question worth arguing over is where the wall sits, and what the wall is made of.
Most sandboxes put that wall inside the operating system. We put it underneath.
A kernel of its own
Every boxd machine is a KVM microVM. Its own kernel, its own network stack, its own disk. Code inside has root over that machine and no path to the host or to machines belonging to other tenants.
This is the same hypervisor boundary your laptop uses when it runs a VM. Nothing exotic, which is exactly the point. The boundary is old, boring, and enforced below the operating system rather than by it.
Containers make a different bargain. They share the host's kernel, and that shared kernel is precisely the surface untrusted code attacks. A microVM hands the workload a kernel of its own and stops caring what happens to it.
Which changes what a sandbox can afford to allow. Code inside can run Docker, load kernel modules, restart systemd, rewrite /etc, open ports, and break the operating system completely. Let it. The wreckage is confined to one machine you were going to throw away.
Docker deserves its own paragraph, because on a lot of sandbox platforms it is the thing that does not work. Here the machine has a real kernel and a real systemd, so the daemon starts without nesting tricks. See Run Docker.
And now the honest part, because a boundary is a boundary and not a force field. Outbound internet is the one surface every machine shares. Code with network access can still reach the internet, and anything you hand the sandbox can leave by that route. That is a separate problem, and it deserves to be treated as one rather than waved at.
But VMs are slow to boot
They are. That is the trade everyone expects to make for a hardware boundary, and it is why most people reach for a container even when they would rather not. Completely reasonable objection.
It is also the thing we spent our engineering on. A fresh machine boots in under 10ms. A fork lands in under 200ms.
At those numbers the arithmetic changes shape. You stop budgeting for startup, and you start creating a machine in the places where you would previously have created a process.
One sandbox per task
The smallest useful shape is three lines. Make a machine, run the thing, delete the machine.
boxd machine new task-1 --isolated
boxd machine exec task-1 -- 'python3 generated_script.py'
boxd machine remove task-1 -yThe same shape from TypeScript, for when the untrusted work happens inside a program rather than a shell:
import { Boxd } from "@boxd-sh/sdk";
const boxd = new Boxd(); // reads BOXD_API_KEY
const machine = await boxd.machines.create({ name: "task-1", isolated: true });
await boxd.machines.waitUntilReady(machine.id);
const result = await boxd.machines.exec(machine.id, {
command: ["python3", "generated_script.py"],
});
console.log(result.stdout);
await boxd.machines.delete(machine.id);A longer session skips the delete. Keep the sandbox and let it sleep between uses: a suspended machine comes back in sub-millisecond time with its filesystem and its processes exactly as they were. See Suspend, resume, and hibernate.
Fleets
One sandbox is a demo. The workloads that actually justify this want dozens at a time: background jobs, runners, test matrices, agent fleets, RL rollouts.
So create them concurrently. Every machine is hardware isolated from every other one, and from the rest of the machines you own.
from concurrent.futures import ThreadPoolExecutor
from boxd import Boxd
boxd = Boxd() # reads BOXD_API_KEY
def sandbox(i: int):
machine = boxd.machines.create(f"sandbox-{i}", isolated=True)
boxd.machines.wait_until_ready(machine.id)
return machine
with ThreadPoolExecutor(max_workers=15) as pool:
machines = list(pool.map(sandbox, range(1, 16)))
print(f"{len(machines)} isolated sandboxes up")That boots the stock Ubuntu image, which stops being what you want somewhere around the second week. Prepare a machine with your toolchain, dependencies, and services already installed, snapshot it, then create every sandbox from there: from_snapshot="my-toolchain" in Python, fromSnapshot: "my-toolchain" in TypeScript, alongside the same isolation flag. Each machine still lands in <10ms, so the fleet is up in seconds.
Accounts start capped at 50 concurrent machines, so fifteen leaves plenty of headroom. Raises for real workloads are usually same-day via contact@boxd.sh. See Resources and limits.
Disposable is one mode. The other is that boxd machines are natively persistent, which most sandbox providers are not. A sandbox lives as long as you keep it, disk and all. That is what a multi-tenant product actually needs: one long-lived isolated machine per customer, rather than a fresh empty one per request. See Agentic SaaS.
What --isolated actually removes
A hardware boundary protects the host and the other tenants. It says nothing whatsoever about your own account. By default, machines you own share a single private network, and each one carries a pre-authenticated in-VM boxd CLI. Across your own infrastructure that is convenient. In a sandbox it is exactly backwards, because the untrusted code inherits a credential that reaches your other VMs.
So --isolated takes all of it away at birth. The in-VM boxd CLI, your connected integrations, your saved coding-agent logins, and the bridge to your laptop are left out of the machine entirely. It never joins the default network, and it never reaches another isolated machine.
What it keeps is four things: outbound internet, its public HTTPS domain, inbound SSH, and its persistent disk. Still a perfectly normal Linux box to work in. It simply cannot see or act on anything else you own.
Two properties are worth memorising, because they are the two that catch people. --isolated is set at creation and cannot be changed afterward. Forks and snapshot restores inherit it, so an isolated lineage stays isolated all the way down. (There is no un-isolate flag. A flag like that is a footgun with a pleasant API.)
The console marks an isolated machine on its row, next to the running status. That matters not at all with three machines and quite a lot with forty.
Handing back one door
Total isolation is usually more than you meant. A sandbox typically needs exactly one real thing from your side: a job queue, say, or a shared database.
Grant that path explicitly with tag-based networks. An isolated machine reaches exactly the non-isolated machines it shares a named network with, and nothing else.
boxd machine networks scratch jobs # grant access to machines on `jobs`, effective immediately
boxd machine networks scratch --clear # cut it off againNetworking changes apply in real time. The machine does not reboot.
That is the whole model, and it fits in three sentences. The hypervisor decides what code can touch on the machine. The isolation flag decides what it can touch in your account. A network tag is how you hand back one door at a time.
The useful consequence is that the question changes. You stop guessing what generated code might break, and start asking what the sandbox is permitted to reach. That second question has a written answer: the isolation flag, plus whatever network tags you granted.
Go and find out what that feels like. boxd machine new scratch --isolated, then break it on purpose, then delete it.



