Three boundaries, three layers
The useful way to tell them apart isn't how strict each one is, but where the boundary lives. App Sandbox restricts a process inside the same kernel. A container puts a process in its own namespace, still in the same kernel. A VM runs a separate kernel. Each step outward is a step from "same computer" towards "a different computer that happens to be software", and that decides what a compromise inside each one can reach.
App Sandbox: same kernel, restricted by profile
macOS's App Sandbox denies by default. A sandboxed process starts with almost no access to the system, and specific capabilities are granted back one at a time through entitlements set at code-signing time. The system checks for the com.apple.security.app-sandbox entitlement early in process start-up and applies a profile that defines what the process may do. Anything outside it fails with EPERM, and sandboxd logs the violation.
Velo Workspaces is itself an example. Its entitlements file carries com.apple.security.app-sandbox plus narrow grants for what it needs: access to files you pick (installer images, shared folders), security-scoped bookmarks to keep that access across launches, the microphone for guest audio input, and outbound network access to download images and reach AI Bridge servers. There's no USB passthrough entitlement, and in a sandboxed app that isn't a feature the app chose to skip; the OS won't grant the capability without an approved entitlement.
What this boundary doesn't do is give you a different kernel. It's the same machine and the same kernel, with one process's reach narrowed. App Sandbox isn't built to stop a kernel-level compromise.
Containers: a separate namespace, the same kernel, and on a Mac, a hidden VM
A container uses Linux namespaces and cgroups to give a process its own view of the filesystem, network and process table, while every container on the machine shares the same kernel. That's a useful boundary, but a softer one than hardware, as the Docker Desktop and OrbStack comparison covers: a kernel vulnerability can affect every container on that kernel at once, and hardening like seccomp and AppArmor doesn't fully close the gap, because the boundary is enforced in software by a kernel the contained process talks to directly.
macOS adds a twist. It has no native container runtime, because namespaces and cgroups are Linux kernel features. Docker Desktop and OrbStack run a lightweight Linux VM first and put the containers inside its shared kernel; Apple's own container tool goes further and gives each container its own small VM. Either way, a container on a Mac always has a VM underneath, whether the tool shows it or not.
A full VM: a separate kernel
A virtual machine runs its own kernel, and the boundary is enforced by the hypervisor using the CPU's virtualization extensions, not by software inside the same kernel. The VM fundamentals post and the ARM performance deep dive cover how. This boundary holds even against a kernel-level compromise inside the guest, because that kernel was never the one enforcing it. A disposable VM adds one more property: the guest is thrown away and recreated rather than reused, so even a successful compromise doesn't outlive the session.
Side by side
| App Sandbox | Container | VM | |
|---|---|---|---|
| Kernel | Shared | Shared | Separate |
| Boundary | A process, through its entitlement profile | A namespace, through the kernel | Hardware, through the hypervisor |
| Enforced by | The OS, at the system-call level | The shared kernel itself | The CPU's virtualization extensions |
| Survives a kernel exploit inside it? | Not applicable: same kernel | No | Yes |
| Best for | Limiting what your own trusted app can do | Packaging and dependency isolation | Running something you don't fully trust |
Where it matters most: code you didn't write
All three get harder to reason about once what runs inside isn't your own code. An AI coding agent executing generated commands is the sharpest version of that, and it gets its own treatment in what goes wrong when AI coding agents run unsandboxed, which maps real 2026 incidents to the isolation level that would have contained each one.
Which one to reach for
App Sandbox is for limiting what your own trusted code can do to the rest of the system; it isn't designed to contain code you don't trust. A container is for dependency and environment isolation when you trust the code but want a clean, reproducible filesystem and process view. A VM, ideally a disposable one, is the tool once "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 is, do you need a VM, or is a container enough?, and Apple Containers vs. Linux VMs, a container tool that blurs the line by giving every container its own VM. Or download Velo Workspaces and try it yourself.