Your AI coding agent doesn't execute code. It predicts text.
Everything you actually care about — editing a file, running a test, executing
npm install, or pushing a commit — happens in the unprivileged shell underneath it. The model is just the brain deciding what to run. The execution layer is the muscle deciding what's possible.Confusing the two is why most agent security strategies fail.
Teams dump months into system prompt tuning and eval benchmarks, but a 100% aligned model won't save you if a hijacked prompt or a malicious npm postinstall script executes
rm -rf ~in your host terminal. You can have perfect alignment and still lose your host machine.
What We Are Solving Today
Running --dangerously-skip-permissions is non-negotiable if you want an agent to do real, unattended, multi-step work — approving every file write and shell command by hand doesn't scale. But flipping that flag on your bare host, with your SSH keys, your .env files, and your home directory sitting right there, isn't a risk tradeoff. It's recklessness with extra steps.
The rest of this post builds the fix, one layer at a time:

Each layer closes exactly the gap the previous one left open. Here's where it starts.
1. The Starting Point: Brain vs. Execution Environment
Claude Code, like any terminal-based agent, is a loop: the model proposes a tool call, something executes it, the result goes back into context. The "something" is a shell — usually backed by Node or Bun — spawning subprocesses, touching the filesystem, hitting the network. That execution layer is where all the actual risk lives.
Anthropic's default posture reflects this. Claude Code ships read-only by default and requires manual approval for nearly every consequential tool call — file writes, shell commands, network requests. That's the correct default. It's also the reason almost everyone eventually turns it off.
Approval fatigue is real. Once you're running an agent through a multi-step refactor or a long dependency upgrade, you're approving dozens of near-identical prompts per session. The rational response, under fatigue, is --dangerously-skip-permissions — "YOLO mode." The flag name is doing you a favor by being honest about what it does: it removes the approval loop entirely and gives the agent unmediated access to your host shell.
At that point you're exposed to three concrete failure modes, not hypothetical ones:
- Destructive commands — an agent confidently executing
rm -rfagainst the wrong path, or a migration script against the wrong database, because it misread context. - Prompt injection — untrusted content (a scraped webpage, a malicious
README, an issue description) contains instructions that get interpreted as commands once they're in the model's context window. - Supply-chain compromise — a transitive npm dependency with a postinstall script that phones home or grabs credentials from environment variables the moment
npm installruns.
None of these require the model to be "misaligned." They require the execution environment to have no boundary. Fixing this is an infrastructure problem, not a prompting problem.

To stop the agent from wiping your machine, the obvious first step is to fence off process permissions at the OS level — which brings us to local OS-native sandboxing.
2. Local Execution and OS-Native Safety
Before reaching for heavier isolation, it's worth being precise about what a fast local agent loop actually needs, and what's already available on the OS.
High-throughput agent runners increasingly use lightweight JS runtimes — Bun over Node, for the startup latency alone — because tool-call round-trip time compounds. A 200-step agentic session with a 400ms shell-spawn tax per step is a session nobody waits for. Speed isn't a nice-to-have here; it directly determines whether unattended, multi-step agent work is viable at all.
But raw execution speed with zero boundary is just a fast way to damage a host. This is where OS-native process sandboxing earns its place as a default:
| Mechanism | Platform | Boundary type | Overhead |
|---|---|---|---|
Apple Seatbelt (sandbox-exec) | macOS | Kernel-enforced process policy | Near-zero |
bubblewrap | Linux | User namespaces + seccomp | Near-zero |
| Container (shared kernel) | Both | Namespace isolation | Low |
| MicroVM (hypervisor) | Both | Hardware-virtualized boundary | Low-moderate |
Seatbelt and bubblewrap are policy engines, not virtualization. They constrain a single process's view of the filesystem and its syscall surface — the agent's shell can be told "you may read/write inside this directory tree and nowhere else," enforced by the kernel, with no VM boot time and negligible overhead. For interactive local sessions where the agent is trusted-ish and the risk is mostly "overeager file write," this is a genuinely good default. It's cheap, fast, and closes the most common failure mode (writing or deleting outside the intended workspace) without touching your architecture.
What it doesn't give you: a boundary that survives a process-level compromise. Seatbelt and bubblewrap still share your kernel. If the sandboxed process finds a policy gap or a kernel-level escape, the blast radius is the host, because there was never a hypervisor between the sandbox and the OS — only a policy. For a supervised, interactive coding session, that's an acceptable trade. For an unattended background agent running with full tool autonomy, it isn't.
If we can't share the host kernel, we need to bring in a real hardware-virtualized hypervisor — enter Docker Sandboxes (sbx).
3. Elevating Security to Docker Sandboxes (sbx MicroVMs)
The failure mode that OS-native sandboxing doesn't cover is exactly the one that matters for unattended agent runs: you're not standing over the terminal watching each command anymore. The agent is running in the background, executing an arbitrary number of tool calls, and you find out what happened after the fact. At that point, "the kernel enforced a policy" is a weaker guarantee than "the agent was never in the same kernel as your host."
That's the shift Docker Sandboxes (sbx) make. Instead of a shared-kernel container or a policy-fenced host process, sbx run claude launches Claude Code inside an ultra-lightweight microVM:
# Launch Claude Code inside an isolated microVM
sbx run claude
The distinction between "container" and "microVM" is not marketing — it's the actual security-relevant boundary:
| Isolation type | Shares host kernel? | Escape impact | Typical use |
|---|---|---|---|
| Raw host shell | Yes (is the host) | Full host compromise | Never, for untrusted code |
| OS sandbox (Seatbelt / bubblewrap) | Yes | Full host compromise if bypassed | Interactive, supervised sessions |
| Container (namespaces/cgroups) | Yes | Full host compromise if kernel exploit found | Trusted, known workloads |
| MicroVM (hypervisor-backed) | No — own kernel | Contained to the VM | Untrusted / unattended agent execution |
A microVM boots its own minimal kernel under a hypervisor boundary. A container-escape-class bug doesn't help an attacker, because there's no shared kernel to escape into — the next layer out is virtualized hardware, not the host OS. This is the same isolation model (Firecracker-class microVMs) that public cloud providers use to run untrusted multi-tenant workloads on shared hardware. Applying it to a single-tenant local agent session is arguably overkill for a supervised session and exactly correct for an unattended one.

This is what makes YOLO mode legitimate rather than reckless:
# Inside the sandbox, --dangerously-skip-permissions is safe:
# the "danger" is scoped to a disposable VM, not the host.
sbx run claude --dangerously-skip-permissions
The flag still does what it says — it removes the approval loop and gives the agent unmediated tool access. What changes is the territory it has unmediated access to. A compromised agent, a malicious dependency, or a bad tool call can now do maximum damage inside a VM that gets torn down, not inside ~/Documents. You get full agent autonomy and you get to walk away from the terminal, because the worst case is bounded.
Isolation stops host damage, but it doesn't stop data or key exfiltration. To fix that, we must strip raw credentials out of the sandbox completely.
4. Real-World Security Mechanics
The microVM boundary handles compute and filesystem isolation. Three more mechanics close the remaining gaps: credentials, network egress, and code review before merge.
The Middleman Credential Proxy
The naive approach — ANTHROPIC_API_KEY=sk-... sbx run claude — puts a live, unscoped credential directly inside the VM's environment. If the agent (or an injected instruction, or a malicious dependency) can read its own environment, it can read the key. Once it's out, it's out; you're rotating a credential and auditing usage after the fact.
sbx avoids this by never putting the real key inside the sandbox at all:
# Register the credential with the proxy, not the sandbox
sbx secret set -g anthropic ANTHROPIC_API_KEY
# The sandbox talks to a local proxy that injects the real key
# on outbound requests to the Anthropic API — externally, at the edge
sbx proxy managed anthropic
The VM holds a reference, not a secret. Outbound API calls leave the VM addressed to the proxy; the proxy — running outside the sandbox boundary — attaches the real credential and forwards the request. The agent's execution environment never has a raw key to leak, log, or accidentally echo into a commit. This is the same pattern as a cloud metadata-service credential broker: the workload authenticates through something, it never holds the thing.
Layer 7 Egress Network Policy
Filesystem and credential isolation don't stop a compromised agent from making outbound network calls — exfiltrating data over DNS, hitting an attacker-controlled endpoint, or reaching a production database it should never see. That's a network policy problem, and it needs to be enforced at Layer 7 (hostname-aware), not just Layer 3/4 (IP/port), because most of what you want to allow or block is expressed as domains, not addresses.
sbx exposes three network modes and a domain allowlist:
| Mode | Behavior | When to use |
|---|---|---|
open | Unrestricted egress | Local experimentation only |
balanced | Allowlisted domains + common package registries | Default for most agent work |
lockdown | Explicit allowlist only, nothing implicit | Unattended / production-adjacent runs |
# Restrict outbound traffic to exactly what the agent needs
sbx policy allow network "*.anthropic.com"
sbx policy allow network "registry.npmjs.org"
sbx policy set network lockdown
Under lockdown, a compromised agent can talk to Anthropic's API and your package registry — and nothing else. It cannot reach an internal database, a random webhook endpoint, or an attacker's exfil server, because those hostnames were never allowlisted. This turns "the agent tried to do something unauthorized" from a data breach into a blocked connection in a log.

Bind Mounts vs. Git Worktree Clone Mode
The last question is how agent-generated changes get from the sandbox back onto your host, and sbx gives you two models depending on how much you trust the run:
Direct bind-mount — the sandbox mounts your working directory directly. File edits appear on the host in real time, no export step, no diff to reconcile. This is right for supervised sessions where you're watching the agent work and want immediate feedback — you get the isolation benefits (credentials, network, compute) without changing your workflow.
Clone mode — for anything unattended:
sbx run --clone claude
Instead of mounting your working tree, sbx clones the repo into an isolated Git worktree on a generated branch (sandbox-...). The agent works entirely inside that worktree. Nothing touches your actual working directory until you explicitly fetch the branch and review it — functionally identical to reviewing a pull request from a contributor you don't yet trust:
git fetch origin sandbox-<run-id>
git diff main sandbox-<run-id>
This is the correct default for background or scheduled agent runs: the agent gets full autonomy inside its own branch, and the host repository is never touched until a human has looked at the diff.
You can lock down every byte of infrastructure, but you can't sandbox a moving AI model. To handle model drift, we need provenance tracking.
5. The Moving Brain and Reproducibility Receipts
Lock down compute, credentials, and network, and you've solved every infrastructure risk. One variable is still unsolved: the model itself. "Claude" is not a fixed artifact — model weights, routing, and system behavior can shift between runs, sometimes silently from the caller's perspective. A sandboxed agent that behaved one way on Monday is not guaranteed to behave identically on Friday, even with an unchanged prompt and an unchanged sandbox image.
You can't sandbox your way out of that, because it isn't an execution-environment problem — it's a provenance problem. The fix is to stop treating "the model" as an assumption and start treating it as a logged fact, on every run, the same way you'd log a container image digest in a deployment pipeline.
A reproducibility receipt is a small metadata snapshot, generated automatically alongside every agent run or eval suite, that answers "what, exactly, produced this output" after the fact:
Show code
import hashlib import json import subprocess from dataclasses import dataclass, asdict from datetime import datetime, timezone @dataclass class ReproducibilityReceipt: model_id: str # e.g. "claude-sonnet-5-20260115" system_prompt_hash: str # sha256 of the exact prompt sent runtime_hash: str # sha256 of the agent harness / runtime version sandbox_image_digest: str # sbx / OCI image digest for the run environment timestamp: str def sha256_of(content: str) -> str: return hashlib.sha256(content.encode("utf-8")).hexdigest() def sandbox_image_digest(image: str = "sbx-claude:latest") -> str: # Resolve the exact content-addressed digest of the running sandbox image — # a tag can move; a digest can't. result = subprocess.run( ["docker", "inspect", "--format", "{{index .RepoDigests 0}}", image], capture_output=True, text=True, check=True, ) return result.stdout.strip() def capture_receipt(model_id: str, system_prompt: str, runtime_version: str) -> ReproducibilityReceipt: return ReproducibilityReceipt( model_id=model_id, system_prompt_hash=sha256_of(system_prompt), runtime_hash=sha256_of(runtime_version), sandbox_image_digest=sandbox_image_digest(), timestamp=datetime.now(timezone.utc).isoformat(), ) if __name__ == "__main__": receipt = capture_receipt( model_id="claude-sonnet-5-20260115", system_prompt=open("SYSTEM_PROMPT.md").read(), runtime_version="claude-code-cli@2.4.1", ) with open("receipt.json", "w") as f: json.dump(asdict(receipt), f, indent=2)
Four fields, none of them optional if you want an eval result — or an incident — to be explainable later: which model actually answered, exactly which system prompt it saw (hashed, not stored verbatim, so the receipt itself isn't a secrets liability), which runtime executed the tool calls, and which sandbox image enforced the isolation boundary at the time. Store the receipt next to the run's output, or next to the commit it produced. When a result looks different from last week's, the receipt tells you in seconds whether the model moved, the prompt drifted, the runtime updated, or the sandbox image changed — instead of a debugging session that starts from zero.
Further Reading
- Docker Sandboxes — the
sbxCLI and its microVM isolation model for AI coding agents - Firecracker — the open-source microVM hypervisor (built by AWS) that this isolation model is based on
- bubblewrap — the unprivileged Linux sandboxing tool used by Flatpak and similar projects
- macOS Sandbox internals — Apple never published official docs for
sandbox-exec/Seatbelt; it's an undocumented, now-deprecated private API, so this is the best available technical reference - Claude Code security & permissions — Anthropic's own documentation on the default approval model and
--dangerously-skip-permissions
Where This Leaves You
None of these layers are independently sufficient, and that's the point — they're not alternatives, they're a stack:
- MicroVM isolation bounds compute and filesystem blast radius, making
--dangerously-skip-permissionsa reasonable choice instead of a reckless one. - Credential proxying means there's never a raw key inside the boundary to leak in the first place.
- Layer 7 egress policy bounds what a compromised agent can reach, turning exfiltration attempts into blocked connections.
- Clone-mode worktrees keep unattended agent output on a branch until a human reviews it, same as any other untrusted contribution.
- Reproducibility receipts cover the one thing infrastructure can't fix — the model itself changing under you.
The system prompt tunes what the agent wants to do. This stack determines what it's actually able to do if something goes wrong. For unattended, full-autonomy agent execution, the second one is the one that matters.
What's your actual blast radius if your agent's next tool call is malicious?
Found this useful? Let's go deeper.
Book a free 15-minute call to discuss your cloud, DevOps, or AI strategy challenges.