#Running sandboxd locally on a Mac
On macOS 26 or later, on an arm64 Mac, each sandbox is a lightweight VM of the
native container runtime: a kernel of its own, one per sandbox. The API is the
same as on a self-hosted Linux machine or in the cloud. The local endpoint is a
unix socket that only you can open.
Status: written and tested on Linux, against a fake runtime that runs the real guest agent. It has not yet run on a Mac.
scripts/m3/macos/run-all.shchecks everything the backend relies on; its results decide the open points below.
#Install
You need macOS 26 or later on an arm64 Mac, Go 1.25+ and the container
runtime (its signed installer package is on
https://github.com/apple/container/releases).
Build from source. No published release has sandboxd yet: 0.0.1 is the
last release of the container design, and install.sh refuses it rather than
install half of it. Until the rewrite's first release, build all three
binaries from a checkout:
git clone https://github.com/Amitgb14/sandbox-cli && cd sandbox-cli
make build # -> bin/sandbox-cli, bin/sandboxd, bin/sandbox-gateway, bin/sandbox-guestd
file bin/sandbox-guestd # must say: ELF 64-bit LSB executable, ARM aarch64
For Studio, the browser view, run make studio (Node 20+)
before make build: the UI is built into sandbox-cli, so a client built
first serves only Studio's API and a page saying how to build the rest.
sandbox-guestd is a Linux binary even on a Mac: it runs inside the VM, which
is linux/arm64. make build cross-compiles it for Linux on your Mac's
architecture, which must be arm64.
Install the binaries. The launch agent runs /usr/local/bin/sandboxd, and
sandboxd finds the guest agent beside itself, so both go there. That directory
belongs to root and may not exist yet on a new Mac:
sudo install -d -m 0755 /usr/local/bin
sudo install -m 0755 bin/sandboxd bin/sandbox-guestd /usr/local/bin/
install -m 0755 bin/sandbox-cli ~/.local/bin/ # or anywhere on your PATH
Start the runtime, then sandboxd:
container system start # the runtime's own service; the first run offers to install its kernel
cp packaging/launchd/dev.sandbox.sandboxd.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.sandbox.sandboxd.plist
If the launch agent was already loaded (you ran bootstrap before the binary
was in place, or you are upgrading), restart it instead; bootstrap on a loaded
agent fails with Bootstrap failed: 5: Input/output error:
launchctl kickstart -k gui/$(id -u)/dev.sandbox.sandboxd
If you installed the launch agent from an earlier build, copy it again. An
older plist sets no PATH, so under launchd sandboxd cannot find the
container CLI in /usr/local/bin and exits, and /tmp/sandboxd.log fills
with the container CLI: executable file not found in $PATH. An even older one
passes --allow-bind, which sandboxd no longer has, so it refuses to start
rather than run with a flag it does not understand.
Check it. sandboxd listens on ~/.config/sandbox/sandboxd.sock (a launch
agent has no $XDG_RUNTIME_DIR), which is where sandbox-cli looks by default,
so no context needs adding:
sandbox-cli doctor # reaches sandboxd and lists what it offers
sandbox-cli run --network none -- uname -a # a first sandbox: prints an aarch64 Linux kernel
tail -f /tmp/sandboxd.log # sandboxd's log, if either fails
Where it keeps things. On a Mac, images and sandboxes' disks are the
runtime's: container keeps them in its own store, which container system
manages, not in sandboxd's directory. sandbox-cli image pull and Studio's
Images screen pull into it (container image pull); there is no disk to build
after, and the runtime reports no progress or size, so neither is shown. sandboxd's --state-dir (by default
~/.local/share/sandboxd) holds only its records, the audit log and, briefly,
a snapshot's export (snapshot-tmp/), so it is the runtime's store that needs
the disk space.
Another sandboxd beside it (a dev build, say) needs its own
--state-dir and --listen; More than one sandboxd, below, says why.
Upgrading. git pull && make build, install the two binaries again as
above, then launchctl kickstart -k gui/$(id -u)/dev.sandbox.sandboxd.
The guest agent is mounted read-only into every sandbox from beside
sandboxd, so any image works and the agent always matches the server.
#What is different from Linux
| macOS (local) | Linux (self-hosted, cloud) | |
|---|---|---|
| Sandbox | VM of the container runtime |
Firecracker microVM |
| Egress | none, or open if your policy allows it |
none or an allowlist, enforced on the host |
| Live network policy change | no | yes |
| Snapshots | the files only (disk_snapshot) |
whole: memory, processes and disk (memory_snapshot) |
Snapshots. The runtime cannot capture a running VM's memory, so a snapshot
here is the sandbox's files: exported, wrapped as an image named for this
sandboxd's owner (sbx-snapshot/<owner>/<id>) and loaded into the runtime's
store. A sandbox started from one boots afresh with those files, and may ask
for other resources than the original's. Taking one takes about a minute and,
briefly, the size of the sandbox's files in --state-dir; the image then
stays until the snapshot is deleted. sandboxd keeps no snapshot records across
a restart, so on start it removes its own leftover snapshot images, and no one
else's.
Egress. The runtime's own network is open NAT. Until it is measured whether
an allowlist can be enforced here, this backend does not claim one. A request
for an allowlist is refused, never served open. The default is open, as on
Linux; ask for --network none for a sandbox with no network, or set the
default to none in a policy file you pass with --policy.
No host directory. A sandbox has no repository, and nothing of your Mac is
mounted into it but the guest agent, read-only; every process starts in /sandbox/home. The bind mount this
backend used to offer (--allow-bind) was removed with the repository model.
More than one sandboxd. The container runtime is one per Mac and shared.
Each sandbox carries its sandboxd's owner label, from its --state-dir, and a
sandboxd starting removes only what it left itself; give each sandboxd its own
--state-dir (and --listen), and they leave each other alone. A sandbox
from a build before owner labels is named in the log and left for you to
remove.
#Open points, decided by the M3 macOS run
Seen on a Mac with macOS 26.1 (2026-10-03), alpine:3.20: a sandbox with
--network none comes up in under a second. Everything below is still to be
checked.
- Whether the runtime supports
--network noneas rendered. If it does not, creating a sandbox fails, rather than running one with a network. - Whether an allowlist can be enforced: in the guest (iptables or nftables), or outside it.
- The shape of
container ls --format json, which is how leftover sandboxes are found after a restart.