Starting a VM from a MacOS sandbox | Brent Fitzgerald
Brent Fitzgerald<br>Close
*]:pointer-events-auto" data-astro-cid-bbe6dxrz> May 27, 2026 essay Brent Fitzgerald<br>Starting a VM from a MacOS sandbox<br>Seatbelt allow rules for processes to use virtualization
This is a quick tech note about using Lima to start VMs from MacOS sandboxed processes.
VMs with Lima
Increasingly often I have multiple worktrees in progress, each with an agent working on a specific feature. To isolate all service dependencies and ports, I’ve been starting to use throwaway Linux VMs with Lima, and then I’ll run docker compose and other app stuff inside of the VM.
I decided it would be really helpful to make my coding agents also do this. That way they can do a full development and test lifecycle in full isolation without any docker compose port collisions or shared databases.
Sandboxes with Seatbelt and nono
I run each coding agent in a sandbox. I am a little suspicious of the agent-provided sandboxes, which may change over time and seem a bit too clever with options like auto mode. So I use a separate system.
On a Mac, sandboxing typically uses Apple’s Seatbelt, the kernel-level engine used by sandbox-exec. A seatbelt profile is a bunch of very explicit rules about what to deny and allow.
My sandbox uses a Seatbelt profile that I initially constructed with Agent Safehouse.
However, I’ve also been recently experimenting with nono, a capability-based sandbox that also uses Seatbelt under the hood. Theoretically nono can make it easier to compose capabilities (via groups) and to inherit from and override readymade profiles.
Lima and the Virtualization entitlement
Lima’s driver on Apple Silicon is vz, which uses Apple’s Virtualization framework. The framework requires an entitlement. limactl already ships with it. Here’s how you can see it:
$ codesign -d --entitlements - /opt/homebrew/bin/limactl<br>...<br>[Key] com.apple.security.virtualization<br>[Value]<br>[Bool] true<br>So the binary is allowed to make VMs. So far so good!
The failure
Unfortunately running limactl start with my sandbox profile failed with an error:
[hostagent] Starting VZ<br>[hostagent] Setting up Rosetta share<br>fatal: Error Domain=VZErrorDomain Code=1 Description="Internal Virtualization error. The virtual machine failed to start."<br>Plus some other issues:
failed to detect system DNS, falling back to [8.8.8.8 1.1.1.1] error="open /etc/resolv.conf: operation not permitted"<br>The DNS warning is just an issue of read access on /etc/resolv.conf, and there’s nothing sensitive in there. The real problem is VZErrorDomain Code=1. It says “Internal Virtualization error” but does not say what was denied exactly.
Since the error occurred right after “Setting up Rosetta share” I thought at first it looked like a Rosetta problem (which turned out to not be the case).
The Seatbelt denial was not in Lima’s logs. Lima only sees the generic VZ error. To find the Seatbelt logs I had to look at the MacOS unified log, written by the kernel.
My initial log show queries found nothing. The denials are emitted by the kernel in a specific format and it was hard to figure out the right predicate to use. I eventually found it by filtering on the literal Sandbox: prefix:
log stream --style compact --predicate 'eventMessage CONTAINS "Sandbox: "'<br>I tested this by triggering a denial I understood: read a blocked file under the sandbox. The log showed it:
Sandbox: wc(89150) deny(1) file-read-data /private/etc/sudoers<br>Good. Logging works. The format is Sandbox: () deny(1) .
The denials
With the stream running, I started the VM again. Here’s the deny I saw:
Sandbox: limactl(42303) deny(1) mach-lookup com.apple.Virtualization.VirtualMachine (xpc)<br>So to start a VM, the Virtualization framework tries to look up an XPC service named com.apple.Virtualization.VirtualMachine. That service is the process that actually hosts the guest. The sandbox did not allow the lookup, the lookup failed, and the framework returned Code=1 with no detail.
After some research and with agent assistance, I learned there’s a way to write a rule to allow a specific mach-lookup:
(allow mach-lookup<br>(global-name "com.apple.Virtualization.VirtualMachine"))<br>Then I tried again. I got past that denial, and more denials appeared, in sequence, as we got further:
limactl deny(1) mach-task-name others [com.apple.Virtualization.Virtual(44215)]<br>limactl deny(1) generic-issue-extension extension-class:com.apple.virtualization.extension.fuse<br>limactl deny(1) generic-issue-extension extension-class:com.apple.virtualization.extension.rosetta-directory-share<br>Each one is a step in starting the VM:
mach-task-name took me a bit. It is the framework getting a handle to the host process it just spawned. That process runs outside the sandbox since launchd starts it. The parent needs its task-name to monitor it.
The fuse extension is the virtio-fs directory share, used to mount the host filesystem into the VM.
The rosetta-directory-share extension...