Use the vz (macOS) backend
Run a disposable ZTD guest locally on macOS — on Apple’s
Virtualization.framework via lima, instead of KVM (which
macOS doesn’t have). This is the vz backend.
vz is the native local backend on macOS, the way kvm is on Linux — so on
a Mac, local resolves to vz automatically and a bare ./ztd up (no TARGET)
does the right thing. TARGET=vz is just the explicit form; the examples below
spell it out for clarity, but you can drop it on macOS.
Status: available, validated end-to-end on Apple Silicon.
TARGET=vz ./ztd check | up | ssh | status | run | audit | down | testall route to the host-side lima driver; the guest gets its own Docker daemon + Node 20 + Claude Code, the working tree live-shares with operator-owned files, and the agent lifecycle runs over the forwarded-port SSH. The one gap isztd authon macOS (Keychain — see below). See the build plan for how it was brought up.
Prerequisites
Only Docker + lima — no socket_vmnet, no privileged setup (the guest is
reached over lima’s forwarded SSH port, not a bridged IP):
brew install limaApple Silicon (arm64) is the primary target.
Boot a guest
# from your project directory (the working tree is live-shared into the guest)
TARGET=vz ./ztd check # macOS + lima preflight (setup is a no-op for vz)
TARGET=vz ./ztd upup renders lima/ztd.yaml.tmpl into a per-instance lima config, generates the
per-VM ephemeral SSH key under .ztd/keys/ if absent (the guest’s sole
trusted identity — your personal key is never injected), then limactl starts the
guest and waits until it’s SSH-reachable.
What the guest comes with
up provisions the same agent stack as the other backends, so the guest is
ready to run Claude Code in YOLO mode inside its isolation boundary:
- the guest’s own Docker daemon (never the host’s — that’s the core isolation guarantee),
- Node 20 + Claude Code (
claude), git,rsync, andtmux(tmux hosts the detachableztd runsession),- a
devuser with passwordless sudo, trusting only the ephemeral key.
The install steps are the canonical cloud-init/install-agent-stack.sh, embedded
into the lima provision script (lima generates cloud-init from its own config, so
there’s no raw cloud-init file here). up blocks until provisioning finishes, so
the stack is ready the moment up returns.
Connect and inspect
TARGET=vz ./ztd ssh # shell in as dev, over the ephemeral key
TARGET=vz ./ztd status # target / state / endpoint
TARGET=vz ./ztd ip # -> "127.0.0.1 <port>" (lima's forwarded SSH endpoint)
TARGET=vz ./ztd tunnel 8080 # forward a guest port to localhostThe endpoint is 127.0.0.1:<port>; the port is assigned by lima per boot and is
re-resolved on every call (it changes across a stop/start), so don’t hard-code
it. (ztd ip prints host port for vz rather than a bare IP.)
The repo live-share
Your working tree is live-shared into the guest at /home/dev/<dirname> (the
basename of the directory you ran from), the same live-mount model as the local
kvm backend. It’s bidirectional and operator-owned: edit on the host and the
guest sees it instantly; a file the agent creates in the guest lands on the host
owned by you, not root.
On macOS this comes from lima’s vz virtiofs mount, which squashes guest file
ownership to the host operator’s uid — so ZTD’s ownership guarantee ports to macOS
with no ACLs and no uid shim (unlike the kvm backend, where virtiofsd
running as root does the reflection). It Just Works.
Run an agent
The full agent lifecycle works on vz, over the same ephemeral-key SSH (threaded through lima’s forwarded port):
TARGET=vz ./ztd auth # sync your Claude login into the guest*
TARGET=vz ./ztd skills # push ~/.claude skills/commands into the guest
TARGET=vz ./ztd run "do the thing" # launch the agent in a tmux session on ztd/<run-id>
TARGET=vz ./ztd attach # reattach to a running session
TARGET=vz ./ztd audit # export the run record (before down)mount and fetch are no-ops on vz — the working tree is already live-shared
(the guest’s .git is your .git), so a guest-side commit on the result branch is
immediately visible to a host git log. Note that, exactly like the local kvm
backend, ztd run checks out the ztd/<run-id> result branch in your real
working tree (via the shared mount) — the isolated cattle model is the remote
backends.
*
ztd authneeds the operator’s Claude login as a file (~/.claude/.credentials.json). On macOS the login lives in the Keychain, soztd authis not yet supported there; log in inside the guest, or use a remote backend, until Keychain export lands.
Tear it down
TARGET=vz ./ztd downCattle, not pets: down deletes the lima instance entirely (and discards the
ephemeral key). Run the whole acceptance suite with TARGET=vz ./ztd test.
How it differs from the kvm backend
- Host-side, not Terraform. lima drives Virtualization.framework on the host,
so
vzis alimactl-driven shell backend (scripts/vz.sh) — there is no Terraform and no toolbox container in this path. - Reachability is a forwarded SSH port, not a guest IP. (A real bridged IP via
socket_vmnetis a possible future opt-in; it needs a one-time privileged setup.) - Guest setup is a lima
provisionscript, not a raw cloud-init file — lima generates cloud-init from its own config. - Image: the slim Debian
genericcloudimage is fine here (the kvm backend needs the fullergenericimage for libvirt virtio-fs).