Three boundaries, three layers

The useful way to tell these apart isn't "how strict is it" — it's where the boundary actually lives. App Sandbox restricts a process within the same kernel. A container isolates a process into its own namespace, still inside the same kernel. A VM runs an entirely separate kernel. Each step out is a step further from "same computer" and closer to "different computer that happens to be software" — and that's what actually determines what a compromise inside each one can reach.

App Sandbox: same kernel, same process, restricted by profile

macOS's App Sandbox is deny-by-default: a sandboxed process starts with almost no capability to touch the system, and specific capabilities are restored one at a time through entitlements declared at code-signing time. The mechanism is checked early in process startup (macOS's libsystem_secinit looks for the com.apple.security.app-sandbox entitlement), and once present, the system applies a sandbox profile that defines exactly what the process is allowed to do. Anything outside that profile fails with EPERM rather than succeeding — the sandbox daemon, sandboxd, logs the violation rather than silently allowing it.

Velo Workspaces itself is a concrete example: its own entitlements file carries com.apple.security.app-sandbox, plus narrow, specific grants for exactly what it needs — user-selected file access for ISO import and shared folders, security-scoped bookmarks to persist that access across relaunches, microphone passthrough for guest audio, outbound network access for AI Bridge reachability checks. Notably absent: a USB-passthrough entitlement, intentionally left out of the app's own entitlements file until Apple approval for it is viable — the sandbox model means that's not just a feature the app chooses not to build, it's a capability the OS itself won't grant to a sandboxed process without an explicit, approved entitlement for it.

What this boundary doesn't do: it doesn't give you a different kernel, or even a different set of running processes on the system — it's the same machine, the same kernel, with one process's reach narrowed. A kernel-level compromise isn't something App Sandbox is built to stop at all.

Containers: a separate namespace, the same kernel — and on a Mac, a hidden VM underneath

A container uses Linux namespaces and cgroups to isolate a process's view of the filesystem, network, and process table from everything else, while every container on a machine still shares that machine's actual kernel. That's a real, useful boundary — but it's fundamentally softer than a hardware-enforced one, covered in more depth in the Docker Desktop / OrbStack comparison: a kernel vulnerability can affect every container sharing that kernel at once, and no amount of additional hardening — seccomp profiles, AppArmor policies — fully closes that gap, because the boundary is still enforced in software by a kernel the isolated process is also talking to directly.

On macOS specifically, there's an extra wrinkle worth being explicit about: macOS has no native container runtime of its own, because namespaces and cgroups are Linux kernel features. Every container tool on a Mac — Docker Desktop, OrbStack, Apple's own container tool — runs a lightweight Linux VM first, invisibly, and the containers live inside that VM's shared Linux kernel. "Just a container" on a Mac is never actually "just a container" the way it is on a Linux server; there's a VM underneath it whether or not the tool shows it to you.

A full VM: a genuinely separate kernel

A virtual machine boots and runs its own kernel entirely, with the hypervisor — not another piece of software inside the same kernel — enforcing the boundary at the hardware level, using the CPU's own virtualization extensions. Covered in depth in the VM fundamentals post and what determines VM performance on ARM: this is the boundary that holds even against a kernel-level compromise inside the guest, because the compromised kernel was never the one enforcing the boundary in the first place. A disposable VM adds one more property on top: the guest is thrown away and recreated fresh rather than reused, so even a successful compromise doesn't persist past the session it happened in.

Side by side

App SandboxContainerVM
KernelSharedSharedSeparate
Isolation boundaryProcess, via entitlement profileNamespace, via the kernelHardware, via the hypervisor
Enforced byThe OS, at the syscall levelThe shared kernel itselfThe CPU's virtualization extensions
Survives a kernel exploit inside it?Not applicable — same kernelNoYes
Best forRestricting what your own trusted app can doPackaging and dependency isolationRunning something you don't fully trust

Where this matters most: code you didn't write yourself

All three of these get harder to reason about the moment what's running inside isn't your own code — an AI coding agent executing generated commands is the sharpest version of that problem, and it deserves its own threat-model treatment rather than a table row here: see what actually goes wrong when an AI coding agent runs unsandboxed for real 2026 incidents mapped against exactly which isolation level would have contained each one.

Which one to reach for

App Sandbox is the right tool for restricting what your own trusted code can do to the rest of the system — it's not designed to contain code you don't trust at all. A container is the right tool for dependency and environment isolation when you trust the code but want a clean, reproducible filesystem and process view. A VM — disposable, ideally — is the right tool the moment "what if this goes wrong" means something worse than a namespace escape: untrusted installers, unfamiliar scripts, or generated code you haven't reviewed.

Related reading: what a VM on Apple Silicon actually is, do you need a VM, or is a container enough?, and what actually goes wrong when an AI agent runs unsandboxed. Or download Velo Workspaces and try it yourself.