AI sandbox
AI sandbox
Ephemeral macOS guest VMs for AI agent execution. A canonical base bundle is provisioned once; every session is an APFS clone that boots, runs commands over vsock, and gets destroyed. Hardware isolation, not container isolation.
On this page
Why hardware VMs for AI agents
Running an AI agent against untrusted prompts is dangerous. The agent will read, write, execute, and exfiltrate based on whatever language model output looks plausible. Anything you give the agent — disk, network, env vars — is in scope for that output.
Containers were the obvious choice for years. The honest accounting:
- Container escape is a regular CVE. Docker, runc, containerd, kata — all have a recent escape history.
- Containers share the kernel. A kernel exploit in the guest is an escape to root on the host. Hypervisors put a hardware boundary between guest and host.
- Containers don't restrict speculation. Side-channel attacks across containers are well-documented; cross-VM attacks require defeating EPT/SLAT.
- Containers can't run macOS guests. If your agent needs to test macOS-specific behaviour (Xcode, code signing, AppKit), a container is useless.
SecVF's AI sandbox trades a few milliseconds of session-start cost for a real hardware boundary — and uses APFS CoW to make that cost essentially zero.
Base + session topology
Two layers:
~/.avf/AISandbox/
├── ai-sandbox-base-v1.bundle/ ← provisioned ONCE
│ ├── Disk.img (~60 GB sparse)
│ ├── NVRAM
│ ├── MachineIdentifier
│ ├── auxiliary.bin / hardwareModel.bin
│ ├── <version>.ipsw (kept for re-restore)
│ └── metadata.json
└── sessions/
├── session-2026-05-10T14-22-00Z-a7f3.bundle/ ← clones
├── session-2026-05-10T14-25-11Z-b1c2.bundle/
└── session-2026-05-10T14-30-44Z-e9d8.bundle/
The base bundle is the canonical macOS guest: installed once, hardened, frozen. The sessions are ephemeral clones — created on demand, destroyed at end of session.
APFS copy-on-write — how it costs nothing
APFS is the macOS filesystem, and APFS supports file-level CoW via the F_CLONEFILE fcntl. When SecVF clones a session bundle, it's not actually copying anything — it's calling clonefile() on each file, which creates a new inode that shares disk extents with the source.
// Roughly what AISandboxVMSession.cloneBase() does
let base = URL(fileURLWithPath: "~/.avf/AISandbox/ai-sandbox-base-v1.bundle").resolvingSymlinksInPath()
let session = URL(fileURLWithPath: "~/.avf/AISandbox/sessions/session-.bundle")
try FileManager.default.copyItem(at: base, to: session)
// copyItem on APFS uses clonefile() under the hood — extents are shared
// until either side writes, at which point the modified blocks diverge.
Concrete consequences:
- Cloning a 60 GB base bundle takes 1–3 ms, not minutes.
- Disk usage doesn't increase. Both bundles point at the same extents.
- When the session writes a block — e.g. boots and writes a few hundred MB of system state — only those new blocks are unique on disk.
- A typical session bundle, after a 5-minute task, occupies 50–400 MB of unique data.
- Destroying the session is a normal
rm -rf; only the divergent blocks are freed.
~/.avf sits on an HFS+ volume (rare on modern Macs, but possible if you mounted an external disk), the AI sandbox falls back to a full copy and you lose the speed. SecVF detects this at startup and warns.
Provisioning the base bundle
First-time setup is a one-shot install. The process:
- Download IPSW from
*.cdn-apple.com(allowlist enforced, TLS 1.2+). - Restore via
VZMacOSInstallerintoai-sandbox-base-v1.bundle. - First boot — wait for Setup Assistant.
- Automated provisioning script attaches over vsock and configures:
- Disables iCloud login screen (uses
AuthAuthoritytweak — see commit log) - Creates non-admin user
agentwith workspace-only write access - Installs Claude Code CLI as the runtime (staged-tarball fallback if network is restricted)
- Sets up VirtioFS mount points (the workspace will land at
/Users/Shared/workspace) - Configures vsock service on port 2222 (the command channel)
- Installs DTrace probes (system call trace, file activity)
- Disables Spotlight indexing on shared dirs (avoids host fingerprinting)
- Disables iCloud login screen (uses
- Shut down the guest. The bundle is now the immutable base.
Session lifecycle API
The host-side API is small and explicit:
// Spin up a session
let session = try await AISandboxVMSession.cloneBase() // APFS clone, ~ms
try await session.boot() // VM boot, ~5–8s
defer { Task { try? await session.destroy() } } // wipe at end
// Run a command — synchronous-style API, vsock under the hood
let result: CommandResult = try await session.run(
"clang --version && sw_vers"
)
print(result.stdout)
print(result.stderr)
print(result.exitCode)
// Mount a workspace (read-write inside guest, scoped to one host path)
try await session.shareWorkspace(at: URL(fileURLWithPath: "/Users/me/sandbox-work"))
// Stream a long-running process — yields output as it arrives
for try await line in session.stream("npm install && npm test") {
print(line) // line-delimited stdout/stderr from guest
}
// Tear down
try await session.destroy() // rm -rf the clone
Each method is a throws async function. They're safe to call concurrently from different host tasks; the session uses an internal serial queue for vsock RPC ordering.
vsock command channel
The Virtio socket (vsock) is a low-overhead L4 socket between host and guest. No IP stack. No NAT. No DNS. Just a CID+port pair where:
- The guest is CID 3 (the conventional "guest" CID for a single-VM host).
- The host is CID 2 (always — defined by the framework).
- SecVF's command service listens on guest port 2222.
// host // guest (daemon, written in Swift)
let dev = vm.socketDevices.first! while let conn = try await listener.accept() {
let conn = try await dev.connect( let req = try await conn.read(decode: Request.self)
cid: 3, port: 2222) let res = try await execute(req) // runs the command
try await conn.write(encode: res)
await conn.close()
}
Why vsock instead of SSH?
- No network stack involvement — vsock doesn't traverse the virtual switch, doesn't need IP, can't be exfiltrated through DNS rebinding or similar tricks.
- No auth overhead — the CID is hypervisor-attested; only the host process that owns the VM can talk to it.
- Lower latency — round-trip is ~150 µs vs SSH's ~5 ms.
- No port-forwarding gymnastics — vsock is point-to-point.
The wire protocol
Length-prefixed JSON, both directions. Schema:
// Request
{
"id": "uuid-string",
"cmd": ["bash", "-c", "..."], // argv-style
"env": { "PATH": "..." },
"cwd": "/Users/agent",
"timeoutSeconds": 300,
"stdin": "" // optional base64
}
// Response
{
"id": "uuid-string",
"stdout": "...", // base64 to preserve bytes
"stderr": "...",
"exitCode": 0,
"elapsedMs": 1247,
"killed": false
}
Workspace sharing via VirtioFS
The agent needs files. VirtioFS gives the guest a directory mount that's backed by a host directory — high-throughput, page-cache-aware, no SMB or NFS in sight.
let share = VZSharedDirectory(
url: URL(fileURLWithPath: "/Users/me/sandbox-work"),
readOnly: false
)
let device = VZVirtioFileSystemDeviceConfiguration(tag: "workspace")
device.share = VZSingleDirectoryShare(directory: share)
config.directorySharingDevices = [device]
// Inside the guest, mount with: mount -t virtiofs workspace /Users/Shared/workspace
Hardened defaults:
- One share, one directory. The agent sees nothing outside the workspace path.
- Non-admin guest user. The mount is owned by
agent(uid 501-ish). System dirs are read-only by POSIX permission. - Chown/chmod on mount is best-effort. Host owns the inodes; VirtioFS proxies the metadata. Some chown operations from inside the guest return EPERM — by design.
- No mount of host root, ever. Even with explicit config — SecVF rejects shares rooted at
/,/Users, or/Volumesat the framework boundary.
DTrace + Endpoint Security telemetry
Sessions emit two telemetry streams:
Inside the guest — DTrace
A pre-installed DTrace script runs as the agent boots. It traces:
syscall::exec*— every process exec'd, with full argvio:::start— file I/O above a threshold- Network attempts (vsock isn't traced; only IP traffic)
Output is fed through vsock back to the host and appended to the session's audit log at ~/.avf/AISandbox/sessions/<session>.bundle/audit.log.
On the host — Endpoint Security Framework
SecVF subscribes to ESF events about the guest's VM process:
- Memory pressure events
- Unusual file open patterns on the bundle directory
- Host-side network attempts the guest's vmnet user-space helper makes (if any)
Anything anomalous raises a CRITICAL event in security-YYYY-MM-DD.log.
Performance budget
| Operation | Time (M2 Pro) | What's happening |
|---|---|---|
| cloneBase() | ~1–3 ms | APFS clonefile() on every file in the bundle |
| boot() | ~5–8 s | Cold boot of a macOS guest, framework restore + login |
| run() round-trip (small) | ~200 µs + cmd time | vsock send + receive + JSON marshal |
| VirtioFS read (cached) | ~3 µs / 4 KB block | Host page cache hit |
| VirtioFS read (cold) | ~30 µs / 4 KB block | NVMe-backed |
| destroy() | ~50–200 ms | rm -rf on a CoW clone — only divergent blocks freed |
Bottom line: the marginal cost of a fresh isolated environment is dominated by boot time, not clone. Pre-boot a warm pool of sessions if your workload needs sub-second start.
vs. Docker / Firecracker / lima
| Tool | Isolation | Start time | macOS guest? |
|---|---|---|---|
| SecVF AI sandbox | Hardware VM (Apple VZ) | ~5–8 s | Yes |
| Docker / runc | Namespaces + cgroups | ~100 ms | No (Linux only, or VM-backed on macOS) |
| Firecracker | Hardware VM (KVM) | ~125 ms | No (Linux only) |
| lima | QEMU VM with VirtioFS | ~10–20 s | No (Linux only) |
| Apple containerization | Linux VM per container, MacM-style | ~1 s | No (Linux only) |
SecVF wins on isolation strength (hardware boundary on the official Apple framework, not a generic hypervisor) and macOS-guest support (the only option for this category of work on Apple Silicon). It loses on raw cold-start speed against Firecracker — but agent workloads are rarely throughput-bound on cold starts.