Simon Willison just hit a deployment wall that every agent developer will recognize: the sandbox that works perfectly on your laptop fails silently in production because the cloud environment doesn’t expose /dev/kvm. His research into smolvm as an untrusted code execution layer for agent-generated Python and JavaScript exposed the nested virtualization gap between local dev and hosted runtimes.
The interesting part is not the failure. It’s how Claude Fable autonomously detected the constraint, pivoted to GitHub Actions runners (which do expose KVM), and completed the test battery without human intervention.
The Sandbox Requirements
Willison’s goal was straightforward: execute user-provided data transformation tasks with hard resource limits, no network access, and scoped filesystem permissions. The threat model assumes malicious or buggy code (infinite loops, memory bombs, filesystem traversal).
smolvm promises:
- Hardware-isolated VMs instead of shared-kernel containers
- Offline local images (no registry pulls during execution)
- CPU and RAM limits enforced at the hypervisor layer
- Guest-enforced timeouts
- Storage quotas
- Read-only input mounts and writable output mounts
--unprivilegedmode for non-root execution
Cold starts clocked in at 0.6 to 1.5 seconds. Warm executions hit 50 milliseconds. For agent-generated code that runs once or twice per user request, that latency is acceptable.
Why smolvm Requires /dev/kvm
smolvm is built on KVM (Kernel-based Virtual Machine), which requires:
/dev/kvmdevice node- CPU virtualization extensions (Intel VT-x or AMD-V, exposed as
vmxorsvmflags in/proc/cpuinfo) - Nested virtualization support if the host is already a VM
When you run smolvm machine run, it spawns a lightweight VM using QEMU with KVM acceleration. Without KVM, QEMU falls back to software emulation, which is 10x to 100x slower and unsuitable for interactive agent workloads.
The Claude Code for web environment runs on Firecracker, Amazon’s microVM manager. Firecracker itself is a KVM consumer. The guest OS (Linux 6.18.5-fc-v20) sees 4 vCPUs and 15GB of RAM, but no /dev/kvm and no virtualization flags. Nested virtualization is disabled.
# What Claude Fable saw inside the Claude Code container
$ ls /dev/kvm
ls: cannot access '/dev/kvm': No such file or directory
$ grep -E 'vmx|svm' /proc/cpuinfo
# (no output)
$ smolvm machine run python:3.12 -- python -c "print('hello')"
Error: kvm not available
This is not a bug. It’s a deliberate security and performance boundary. Firecracker guests are designed to be ephemeral, single-tenant, and isolated. Exposing nested virtualization would increase attack surface and complicate resource accounting.
The GitHub Actions Pivot
Claude Fable’s autonomous workaround:
- Detected the missing
/dev/kvmin the Claude Code environment - Recognized that GitHub Actions
ubuntu-latestrunners expose KVM - Created a temporary workflow file in a branch
- Pushed the branch, triggered the workflow, collected logs
- Deleted the workflow file in the final commit
The workflow installed smolvm, ran the full test battery (Python and JavaScript execution, resource limits, filesystem isolation), and captured output. The agent then synthesized results from the GitHub Actions logs.
This is a concrete example of tool-aware planning. The agent didn’t ask for help or fail gracefully. It identified an alternative execution environment with the required primitives and used it.
Architecture Comparison: KVM vs. WASM vs. Docker
| Primitive | smolvm (KVM) | WASM Sandbox | Docker (unprivileged) |
|---|---|---|---|
| Isolation | Hardware VM | Software runtime | Kernel namespaces + cgroups |
| Nested virt required | Yes | No | No |
| Cold start | 0.6-1.5s | <10ms | 0.5-2s |
| Warm execution | ~50ms | <1ms | ~50ms |
| Filesystem access | Mount-based, scoped | Virtual FS or WASI | Bind mounts, volume isolation |
| Network isolation | Hypervisor-enforced | Runtime policy | Network namespaces |
| CPU/RAM limits | Hypervisor cgroups | Runtime config | cgroups v2 |
| Deployment constraint | Needs /dev/kvm | Runs anywhere | Needs Docker daemon |
Willison previously built a 362KB MicroPython WASM sandbox for similar use cases. WASM has zero nested virtualization requirements and runs in browsers, serverless runtimes, and CI environments without special permissions. The trade-off is language support: WASM sandboxes are limited to languages that compile to WASM or have WASM interpreters. smolvm supports any language with a Linux binary.
Docker in unprivileged mode (rootless, no --privileged flag) provides namespace isolation without hardware virtualization. The kernel is shared, so a kernel exploit in the container could escalate to the host. For agent-generated code, this is a meaningful risk if the agent is compromised or tricked into generating malicious payloads.
Deployment Shape and Failure Modes
If you’re building an agent that generates and executes code, you need to map your execution environment to the sandbox primitive:
Local development (bare metal or VM with KVM pass-through)
smolvm works. You get hardware isolation and full language support.
Cloud VMs (AWS EC2 bare metal, GCP n2d, Azure Dv5)
smolvm works if you choose instance types that expose nested virtualization. AWS Nitro-based instances support it. Firecracker guests do not.
Serverless (AWS Lambda, Google Cloud Run, Azure Functions)
smolvm does not work. No /dev/kvm. Use WASM sandboxes (Wasmtime, WasmEdge) or gVisor.
Kubernetes (GKE, EKS, AKS)
smolvm works only if nodes expose /dev/kvm and you run privileged pods or use KubeVirt. Most managed Kubernetes clusters do not expose KVM to pods by default.
GitHub Actions (ubuntu-latest runners)
smolvm works. Runners are bare metal or VMs with KVM enabled. This is why Claude Fable’s pivot succeeded.
Failure modes:
- Silent degradation: QEMU falls back to software emulation if KVM is missing but the binary doesn’t check. Execution becomes unusably slow.
- Permission errors:
/dev/kvmexists but the process lacks read/write permissions. Requireschmodor adding the user to thekvmgroup. - Kernel module not loaded:
modprobe kvmandmodprobe kvm_intel(orkvm_amd) must succeed before/dev/kvmappears.
Observability and State Management
smolvm execution is stateless by design. Each smolvm machine run invocation:
- Boots a fresh VM from a cached image
- Mounts specified input directories as read-only
- Mounts output directories as writable
- Executes the command
- Shuts down the VM
- Returns exit code and stdout/stderr
There is no persistent state between runs unless you explicitly mount a shared volume. This is ideal for agent workflows where each tool call should be isolated.
For observability, you rely on:
- Exit codes (0 for success, non-zero for failure)
- Stdout and stderr capture
- Execution time (measured externally)
- Resource usage (if you wrap smolvm in a monitoring harness)
smolvm does not expose internal VM metrics (CPU usage, memory pressure, syscall counts) by default. If you need that telemetry, you would instrument the guest OS or parse hypervisor logs.
Security Boundaries
smolvm’s security model assumes:
- The host kernel is trusted
- The hypervisor (KVM + QEMU) is trusted
- The guest OS image is trusted (you control what goes into the Python or Node.js image)
- The user-provided code is untrusted
The attack surface is the KVM/QEMU boundary. Historical vulnerabilities (VM escape bugs) exist, but they are rare and patched quickly. The risk is lower than shared-kernel containers but higher than pure WASM sandboxes (which have no kernel at all).
Network isolation is enforced by not configuring a network interface in the VM. Filesystem isolation is enforced by mount namespaces. CPU and RAM limits are enforced by cgroups applied to the QEMU process.
If an attacker controls the code inside the VM, they can:
- Consume CPU and RAM up to the configured limits
- Read and write files in mounted directories
- Attempt VM escape exploits (low probability, high impact)
They cannot:
- Access the host filesystem outside mounted directories
- Make network requests
- Persist state between executions (unless you mount a writable volume)
Code Example: Running Untrusted Python with smolvm
# Install smolvm (requires /dev/kvm)
curl -fsSL https://smolmachines.com/install.sh | sh
# Create input and output directories
mkdir -p /tmp/input /tmp/output
echo '{"numbers": [1, 2, 3, 4, 5]}' > /tmp/input/data.json
# Run untrusted Python code with resource limits
smolvm machine run \
--cpu 1 \
--memory 512M \
--timeout 5s \
--mount /tmp/input:/input:ro \
--mount /tmp/output:/output:rw \
--unprivileged \
python:3.12 -- python3 -c "
import json
with open('/input/data.json') as f:
data = json.load(f)
result = sum(data['numbers'])
with open('/output/result.txt', 'w') as f:
f.write(str(result))
"
# Check output
cat /tmp/output/result.txt
# 15
The --unprivileged flag runs the VM without root inside the guest. The --timeout flag kills the VM after 5 seconds. The --mount flags scope filesystem access.
When to Use smolvm for Agent Sandboxes
Use smolvm when:
- You need hardware-level isolation for untrusted code
- You require full language support (Python, Node.js, Ruby, Go, Rust, etc.)
- You control the deployment environment and can provision
/dev/kvm - Cold start latency under 2 seconds is acceptable
- You are running on bare metal, EC2 bare metal, or GitHub Actions
Avoid smolvm when:
- You are deploying to serverless platforms (Lambda, Cloud Run)
- You are running inside a VM that does not expose nested virtualization (Firecracker, most Kubernetes pods)
- You need sub-10ms cold starts (use WASM instead)
- You only need to sandbox JavaScript or Python (WASM runtimes are simpler)
- You cannot tolerate the KVM/QEMU attack surface
Technical Verdict
smolvm is a solid choice for agent-generated code execution when you control the infrastructure and can guarantee /dev/kvm access. The hardware isolation is stronger than Docker, the language support is broader than WASM, and the performance is acceptable for interactive agent workflows.
The deployment constraint is real. If your agent runs in a managed environment (serverless, hosted notebooks, Firecracker-based platforms), you will hit the same wall Claude Fable did. The workaround (GitHub Actions, self-hosted runners, or a dedicated VM fleet) adds operational complexity.
For production agent systems, the decision tree is:
- Can you provision bare metal or VMs with nested virtualization? Use smolvm.
- Can you limit execution to JavaScript or Python? Use a WASM sandbox (Wasmtime, Extism).
- Do you need arbitrary language support in a serverless environment? Use gVisor or Firecracker (accept the complexity).
Claude Fable’s autonomous pivot to GitHub Actions is a preview of how agents will route around infrastructure constraints. The agent didn’t fail. It found a different execution environment with the required primitives and completed the task. That adaptability is the real story.