Letting an Agent Run Code Without Letting It Wreck Your Machine
A few months ago I watched an agent decide that the cleanest way to "reset the test database" was rm -rf on a path it had constructed from a variable that turned out to be empty. The command it actually proposed was rm -rf /$DIR, and $DIR was nothing. I caught it because I was running in default mode and it asked first. I clicked No, fixed the script, and went and made coffee with my hands shaking slightly.
That was on my real laptop. Same machine that has my SSH keys, my ~/.aws/credentials, my Stripe restricted key in a .env two directories up, and the only copy of a side project I hadn't pushed yet. The agent wasn't malicious. It was just wrong, the way a fast junior is wrong, except it types at 200 commands a minute and never gets tired enough to slow down.
The thing that took me too long to internalize: the danger isn't the agent going rogue. It's the boring intersection of a confident wrong command, a credential in an environment variable, and a network connection that can reach the open internet. You don't need a villain. You need an accident and an exfiltration path, and most setups ship both by default.
The short version: isolation is a spectrum, not a switch. The cheapest layer is the agent's own permission prompts. The strongest is a microVM with its own kernel. For day-to-day work on code you wrote, you want OS-level guards plus permission gates. For anything you didn't write — a random repo, a scraped script, code another agent generated — you want a real boundary the operating system can't be talked out of. Below is the whole ladder and where I actually stand on each rung.
The accident model, not the villain model
Threat-model this honestly and the picture gets simpler. There are three things that can go wrong when an agent runs code:
- It destroys local state. Deletes files, overwrites the wrong thing, force-pushes over your branch.
- It reads a secret and sends it somewhere. Greps
~/.ssh, finds a token in an env var,curls it to a domain. This is the one people underrate. - It runs code that was never yours to trust. A dependency's post-install script, a generated payload, a repo you cloned to "just take a look."
Permission prompts address (1) and partly (3). They do almost nothing for (2), because by the time a command runs, the secret is already in the process environment. So the move isn't "find the one tool that solves everything." It's: stack a cheap layer that stops dumb destruction, then add a real boundary underneath for the cases where being talked out of a prompt isn't enough.
Rung one: the agent's own permission gates
Coding agents already ship a permission model, and it's the cheapest containment you have. Claude Code, for example, has distinct permission modes: default (reads run free, everything else prompts), acceptEdits (file edits and a handful of filesystem commands like mkdir, mv, cp auto-approve inside your working directory), plan (research only, no edits), auto (a separate classifier model reviews each action and blocks escalations), dontAsk (only pre-approved tools run, for locked-down CI), and bypassPermissions — the --dangerously-skip-permissions flag, which the docs are blunt about: "Only use this mode in isolated environments like containers, VMs, or dev containers without internet access."
These modes are real and useful, but understand what they are: a decision made before the command runs, based on the command string and, in auto mode, a model's judgment. That's a policy layer, not a wall. It can be wrong, and a creative command can do more than its name suggests. I lean on plan mode for exploration and default for anything touching infrastructure, but I don't treat any of them as a security boundary for code I don't trust. (I've written about Claude Code hooks as guardrails elsewhere — same caveat: a hook is policy enforced in your process, not isolation.)
Rung two: OS-level guards that the model can't argue with
This is the rung most solo builders skip, and it's the one with the best effort-to-safety ratio. The idea: let the operating system enforce a filesystem and network boundary on every shell command and its children, regardless of what the model decided to run.
Claude Code ships this as a sandboxed Bash tool. On macOS it uses the built-in Seatbelt framework — nothing to install. On Linux and WSL2 it uses bubblewrap for filesystem isolation plus socat to route traffic through a proxy. By default, sandboxed commands can write only to your working directory and the session temp dir, and network access is deny-by-default: "no domains are pre-allowed. The first time a command needs a new domain, Claude Code prompts for approval."
The reason this beats a permission prompt: the docs say it plainly. "The operating system enforces the sandbox boundary on the running process, so it holds regardless of what the model chose to run and even if an allowed command does more than its name suggests." That's the difference between policy and enforcement.
Two caveats worth reading twice, both straight from the docs. First, the default read policy still allows reading credential files — "this default still allows reading credential files such as ~/.aws/credentials and ~/.ssh/." You have to add them to denyRead yourself. Second, the network proxy enforces the allowlist by hostname and "does not terminate or perform TLS inspection on outbound traffic," so a broad allow like github.com can become an exfiltration path via domain fronting. Effective sandboxing, in their words, "requires both filesystem and network isolation" — widen one side and you can quietly undo the other.
My take: turn this on, deny-read your credential directories, and keep the network allowlist tight. It's free and it stops the most common accidents. But it's a hardening layer on your real machine, not a clean room.
Rung three: containers
A container is the obvious next step, and for trusted-but-messy work it's fine. You run the agent inside Docker, mount only the project directory, pass in only the env vars that project needs, and the blast radius is the container. Delete the container, the damage is gone.
The honest limitation is the one containers always had: they share the host kernel. A container is namespaces and cgroups, not a separate machine. For code you wrote, that's plenty — the realistic risk is an accident, and the namespace boundary contains accidents well. For genuinely untrusted code, a kernel exploit inside the container reaches the host: a real, if uncommon, class of attack. There's a middle option too — gVisor from Google intercepts a container's syscalls in a userspace kernel before they hit the host, shrinking the attack surface without a full VM. Stronger than a plain container, weaker than hardware virtualization, with a measurable I/O tax.
The practical container setup for an agent: a dev container that runs the agent as a non-root user, with the project mounted and nothing else. That non-root detail matters — Claude Code refuses to start in bypass-permissions mode as root, and the dev container config exists partly to give you a non-root user so autonomous runs are safe to leave alone.
Rung four: microVMs, the actual wall
When you need to run code you do not trust, the gold standard in 2026 is a microVM: a real virtual machine with its own kernel, booted in milliseconds, with the device model stripped down to almost nothing. Firecracker, the open-source VMM AWS built for Lambda and Fargate, is the one everyone uses. Its whole design philosophy is a small attack surface — it "excludes unnecessary devices and guest-facing functionality to reduce the memory footprint and attack surface area of each microVM," boots a minimal kernel with no BIOS and no full device model, and runs each guest on KVM hardware virtualization.
The security argument is structural: each workload gets its own kernel on a hardware virtualization boundary, so a kernel exploit inside one microVM can't reach the host or a neighboring VM. That's the boundary a container can't give you, because the container has no kernel of its own. The cost used to be startup time; Firecracker's ~125ms boots erased most of that.
You almost certainly don't want to operate Firecracker yourself. That's what the hosted services are for.
Rung five: hosted sandbox services
If "give the agent a computer that isn't mine" sounds right, that's a hosted sandbox, and the space matured a lot in the last year.
Vercel Sandbox went generally available on January 30, 2026 (announcement). It runs each sandbox in a Firecracker microVM with its own filesystem and network, on Amazon Linux 2023, with node26/node24/node22/python3.13 runtimes and sudo access. The selling point for the secret-leak problem: code in a sandbox is isolated from your infrastructure, so it can't reach your project's environment variables, database connections, or cloud resources — that isolation is the product, not a setting you have to remember to turn on. You drive it with a JS or Python SDK, or a CLI. Persistence (auto-save and resume) is on by default as of the GA release.
E2B is the other obvious one, aimed squarely at running AI-generated code. It's open-source under Apache-2.0, has Python and JavaScript SDKs, uses a Dockerfile to build a microVM template (not to run a container), and — the part I like — is self-hostable via Terraform on AWS, GCP, Azure, or your own Linux boxes. So you can start on their cloud and pull it in-house later without a rewrite.
The tradeoff is the usual one: latency, cost per run, and a network round trip between your agent and the code. For a tool that runs user-submitted or agent-generated snippets, that's a fair price for a clean kernel boundary and automatic secret isolation. For editing your own repo all day, it's overkill and the friction will annoy you.
What I actually run
Here's the concrete setup, mapped to the two cases that matter.
For my own code, day to day: Claude Code on my real machine, in default or plan mode, with the OS sandbox enabled. The non-negotiable config is denying reads on credential directories and keeping the network allowlist short:
{
"sandbox": {
"enabled": true,
"filesystem": { "denyRead": ["~/.aws", "~/.ssh", "~/.config/gh"] },
"allowedDomains": ["registry.npmjs.org", "github.com"]
}
}
Secrets live in a .env that the agent's working directory can't read, and I scrub provider credentials out of subprocesses (CLAUDE_CODE_SUBPROCESS_ENV_SCRUB) so a stray curl in a build script can't grab my API key from the environment. This is hardening, not a wall, and I treat it that way.
For anything I didn't write — a repo I'm auditing, generated code, a dependency I don't trust, an agent running unattended overnight — it goes in a microVM. In practice that means a hosted sandbox (Vercel Sandbox or E2B) so the kernel boundary and the secret isolation come for free, or a dev container running the agent as non-root with bypassPermissions inside the box where there's nothing valuable to wreck. The rule I follow: the strength of the boundary should match how much I trust the code, not how convenient the boundary is.
The mistake I made early was treating this as one decision — "is sandboxing on or off." It's two. Trusted code on my machine wants cheap guards that stop accidents. Untrusted code wants a wall the model can't be argued past. Pick the rung that matches the trust, deny-read your secrets either way, and you can let the thing run without watching every command — which was the entire point of hiring an agent.
