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 | test all 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 is ztd auth on 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 lima

Apple 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 up

up 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, and tmux (tmux hosts the detachable ztd run session),
  • a dev user 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 localhost

The 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 auth needs the operator’s Claude login as a file (~/.claude/.credentials.json). On macOS the login lives in the Keychain, so ztd auth is not yet supported there; log in inside the guest, or use a remote backend, until Keychain export lands.

Tear it down

TARGET=vz ./ztd down

Cattle, 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 vz is a limactl-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_vmnet is a possible future opt-in; it needs a one-time privileged setup.)
  • Guest setup is a lima provision script, not a raw cloud-init file — lima generates cloud-init from its own config.
  • Image: the slim Debian genericcloud image is fine here (the kvm backend needs the fuller generic image for libvirt virtio-fs).