VM4A CLI
VM4A ships a single binary vm4a that creates and runs Apple
Virtualization.framework VMs on Apple Silicon from the command line. Each
VM is a directory ("bundle") containing config.json, state.json, a disk
image, and platform identity files. One binary handles the lifecycle, OCI
distribution, and agent channel.
Prerequisites (check first, don't assume)
Before running anything:
Apple Silicon + macOS 13+. x86 Macs are not supported.
CLI must be codesigned with the CLI entitlements, otherwise run
fails silently:
swift build
codesign --force --sign - \
--entitlements Sources/VM4ACLI/VM4ACLI.entitlements \
./.build/debug/vm4a
Bridged networking and Rosetta share need the CLI entitlements file
specifically — not the App one.
macOS 26 (Tahoe) gotcha: com.apple.vm.networking is a restricted
entitlement. Ad-hoc signing (--sign -) a binary that carries it makes
AMFI SIGKILL the process at launch — every vm4a command dies with no
output (exit 137). If that happens, re-sign with an entitlements file that
drops com.apple.vm.networking (NAT-only; keeps com.apple.security.virtualization):
cp Sources/VM4ACLI/VM4ACLI.entitlements /tmp/nat.entitlements
/usr/libexec/PlistBuddy -c "Delete :com.apple.vm.networking" /tmp/nat.entitlements
codesign --force --sign - --entitlements /tmp/nat.entitlements ./.build/debug/vm4a
./.build/debug/vm4a --version # must print, not get killed
Ad-hoc NAT covers spawn/exec/cp/run-code/expose-port/OCI — but NOT bridged
networking and NOT snapshots.
Snapshots on macOS 26 (verified): saveMachineStateTo is gated on
hardened runtime + a real signing identity. Ad-hoc signing fails with
VZErrorSave "permission denied" (so snapshot save, run --save-on-stop,
and reset all fail). validateSaveRestoreSupport() is YES — it's purely a
signing issue. Re-sign with --options runtime + an Apple Development /
Developer ID identity:
codesign --force --options runtime \
--sign "Apple Development: YOUR NAME (TEAMID)" \
--entitlements /tmp/nat.entitlements ./.build/debug/vm4a
Bridged mode additionally needs an identity provisioned for the managed
com.apple.vm.networking entitlement (ad-hoc cannot grant it).
For bridged mode: vm4a network list should print at least one
interface. If empty, re-check codesigning.
Command map (when to reach for each)
Agent-first primitives (prefer these for any agent-driven flow):
| Intent |
Command |
| One-shot create+start, wait for SSH |
vm4a spawn NAME --from ghcr.io/you/img:tag --wait-ssh --output json |
| Run a command in the guest, get JSON |
vm4a exec /path/to/bundle --output json -- python3 -c 'print(1+1)' |
| Copy a file host→guest |
vm4a cp /path/to/bundle ./local.txt :/work/remote.txt |
| Copy a file guest→host |
vm4a cp /path/to/bundle :/var/log/syslog ./syslog.txt |
| Fork a bundle, auto-start, wait for SSH |
vm4a fork SRC DST --auto-start --from-snapshot clean.vzstate --wait-ssh |
| Reset to a saved snapshot for retry |
vm4a reset /path/to/bundle --from clean.vzstate --wait-ip |
| Save a running VM as a named snapshot (stops it) |
vm4a snapshot save /path/to/bundle clean |
| Restore a VM to a named snapshot (~1s) |
vm4a restore /path/to/bundle clean (alias for vm4a snapshot restore) |
| List / delete named snapshots |
vm4a snapshot list /path/to/bundle [--output json], vm4a snapshot rm /path/to/bundle clean |
Classic lifecycle:
| Intent |
Command |
| Create a Linux VM bundle |
vm4a create NAME [--image ISO.iso] [--network bridged --bridged-interface en0] [--rosetta] |
| Install a macOS VM from IPSW (10–20 min, drives VZMacOSInstaller) |
vm4a create NAME --os macOS --image macos-15.ipsw --cpu 4 --memory-gb 8 --disk-gb 80 |
| List bundles in a directory |
vm4a list --storage /tmp/vm4a [--output json] |
| Start a VM in the background |
vm4a run /path/to/bundle |
| Start in foreground (logs to stdout) |
vm4a run /path/to/bundle --foreground |
| Boot macOS recovery |
vm4a run /path/to/bundle --recovery |
| Restore from a VZ snapshot (macOS 14+) |
vm4a run /path/to/bundle --restore state.vzstate |
| Save state on clean shutdown (macOS 14+) |
vm4a run /path/to/bundle --save-on-stop state.vzstate |
| Stop a VM (SIGTERM, then SIGKILL if stuck) |
vm4a stop /path/to/bundle --timeout 20 |
| Clone a bundle (APFS clonefile when possible) |
vm4a clone SRC DST |
| Show Linux ARM64 ISO catalog |
vm4a image list |
| Show host bridged interfaces |
vm4a network list |
| Look up VM's NAT IP |
vm4a ip /path/to/bundle [--output json] |
| SSH into running VM (NAT) |
vm4a ssh /path/to/bundle --user root |
| Push bundle to OCI registry |
vm4a push /path/to/bundle ghcr.io/you/name:tag |
| Pull bundle from OCI registry |
vm4a pull ghcr.io/you/name:tag --storage /tmp/vm4a |
| Check guest agent heartbeat |
vm4a agent status /path/to/bundle |
| Ping guest agent |
vm4a agent ping /path/to/bundle |
| Run as MCP server (stdio JSON-RPC) |
vm4a mcp |
| Run HTTP API server on localhost |
vm4a serve --port 7777 |
| Tag operations into a session for replay/inspection |
vm4a exec ... --session run-42 |
| List/show recorded sessions |
vm4a session list, vm4a session show <id> |
| Define a per-task spawn template |
vm4a pool create py --base /path/to/golden --snapshot clean.vzstate |
| Mint a fresh task VM from a pool |
vm4a pool spawn py --wait-ssh |
Key workflows
Agent loop (recommended — uses v2 primitives)
# 1. First-time bootstrap: pull, start (arm save-on-stop), wait for SSH.
vm4a spawn dev --from ghcr.io/yourorg/python-dev-arm64:latest \
--storage /tmp/vm4a \
--save-on-stop /tmp/vm4a/dev/clean.vzstate \
--wait-ssh --output json
# 2. Install whatever, then stop — VM saves state on shutdown.
vm4a exec /tmp/vm4a/dev -- bash -lc "apt-get install -y ripgrep"
vm4a stop /tmp/vm4a/dev
# 3. Per-task: fork the golden bundle, push code, run, parse JSON.
vm4a fork /tmp/vm4a/dev /tmp/vm4a/task-$JOB_ID \
--auto-start --from-snapshot /tmp/vm4a/dev/clean.vzstate --wait-ssh
vm4a cp /tmp/vm4a/task-$JOB_ID ./step.py :/work/step.py
vm4a exec /tmp/vm4a/task-$JOB_ID --output json --timeout 120 \
-- python3 /work/step.py
# 4. Bad task state? Reset back to the snapshot in <1s.
vm4a reset /tmp/vm4a/task-$JOB_ID --from /tmp/vm4a/dev/clean.vzstate --wait-ip
Spin up a disposable Ubuntu VM (manual / classic flow)
ISO=~/Downloads/ubuntu-24.04-live-server-arm64.iso
vm4a create demo --storage /tmp/vm4a --image "$ISO" \
--cpu 4 --memory-gb 8 --disk-gb 64
vm4a run /tmp/vm4a/demo
vm4a ip /tmp/vm4a/demo # needs VM to have booted + DHCP'd
vm4a ssh /tmp/vm4a/demo # after cloud-init / user setup
vm4a stop /tmp/vm4a/demo
Distribute a pre-baked VM through GHCR
# Push (requires a PAT with write:packages)
export VM4A_REGISTRY_USER=youruser
export VM4A_REGISTRY_PASSWORD=ghp_xxx
vm4a push /tmp/vm4a/base-ubuntu ghcr.io/youruser/base-ubuntu:24.04
# Pull on another machine (anonymous if package is public)
vm4a pull ghcr.io/youruser/base-ubuntu:24.04 --storage /tmp/vm4a
vm4a run /tmp/vm4a/base-ubuntu
Fast fork of a golden image for CI
vm4a clone /tmp/vm4a/golden /tmp/vm4a/job-$CI_JOB_ID
vm4a run /tmp/vm4a/job-$CI_JOB_ID
trap "vm4a stop /tmp/vm4a/job-$CI_JOB_ID && rm -rf /tmp/vm4a/job-$CI_JOB_ID" EXIT
clone uses APFS clonefile(2) on the same volume so it's O(directory
entries), not O(disk image size).
Non-obvious behaviors
- NAT IP only works for VMs using the default NAT attachment. Bridged
VMs don't land in
/var/db/dhcpd_leases; users must pass --host <ip> to
vm4a ssh or use their router's DHCP view.
--rosetta is Linux only and requires
softwareupdate --install-rosetta --agree-to-license before first run.
The CLI warns but doesn't block.
- macOS guests work end-to-end via the CLI, with one caveat: after
vm4a create --os macOS --image foo.ipsw finishes (10–20 min), the
VM lands at Apple's Setup Assistant on first boot. Apple does not
expose a scriptable skip path, so the user needs to click through it
once in VM4A.app: pick region/keyboard, skip Apple ID, create a
user account, then System Settings → General → Sharing → Remote
Login: ON. After that single click-through, every CLI / MCP / HTTP
/ SDK operation works on the macOS bundle exactly like on Linux,
and pulling a published macOS bundle from GHCR skips Setup Assistant
entirely (the user account + Remote Login are baked in).
- Config JSON format starts at
schemaVersion: 1. Old bundles without
the field still load — tolerant decoding treats missing as 1. When
adding new fields, make them optional in Core.swift decoding.
vm4a stop requires a running pid. If vm4a list shows stopped
but stale files exist, just re-run vm4a run; the CLI cleans stale
PID files on the next list.
--output json is available on create, list, ip, agent status,
and on every v2 primitive (spawn, exec, cp, fork, reset).
Output is one JSON object/array per command invocation (not JSONL).
vm4a exec returns JSON with exit_code, stdout, stderr,
duration_ms, timed_out. The exit code is also the process exit code
(so if vm4a exec ... ; then ... works in shell). With --timeout and a
timed-out command, the agent gets timed_out: true and a non-zero exit.
vm4a cp uses : to mark guest paths, not host: like docker cp. So
./local.txt :/work/file.txt is host→guest. Both sides being host or
both being guest is rejected.
vm4a fork re-randomises MachineIdentifier AND the NIC MAC automatically.
Don't hand-roll a clone+identity flow — fork is what clone should have been
for agent loops, with optional --auto-start and --from-snapshot. (Re-randomising
the MAC is what stops parallel forks from colliding on the NAT lease table.)
vm4a ip matches the lease by the bundle's persisted NIC MAC, not by
hostname. New bundles store a fixed MAC in config.json; if no lease matches
it yet, ip returns empty / exit 1 (it no longer dumps the whole lease table).
Legacy bundles without a stored MAC still fall back to hostname matching.
vm4a snapshot save/restore/list/rm (+ top-level vm4a restore) are the
simple path — named .vzstate snapshots stored in <bundle>/.vm4a-snapshots/,
no path juggling. save captures the running VM then stops it (it signals the
worker via SIGUSR1); restore reboots to that state in ~1s. Prefer these over
hand-managing run --save-on-stop / run --restore / reset paths. Needs
the hardened-runtime + real-identity signing (see Prerequisites #2).
vm4a exec parses its options in any position (the command goes after
--): both vm4a exec /b --output json -- cmd and vm4a exec --output json /b -- cmd
work. Same for cluster exec and ssh extra args.
vm4a reset requires a .vzstate file, which means macOS 14+. On
older hosts the agent has to fall back to clone + reinstall.
- Guest agent is a scaffold. Only
ping is implemented. Don't promise
clipboard/shutdown/script execution yet.
Error exit codes (for scripting)
1 VM4AError.message Generic failure (legacy path)
2 VM4AError.notFound Bundle or file missing
3 VM4AError.alreadyExists Destination already exists
4 VM4AError.invalidState VM running when it should be stopped (or vice versa)
5 VM4AError.hostUnsupported macOS version / hardware capability missing
5 VM4AError.rosettaNotInstalled
When the user is stuck
- Every
vm4a command exits with code 137 / no output (macOS 26): AMFI
killed the ad-hoc-signed binary because of the restricted
com.apple.vm.networking entitlement. Re-sign NAT-only (see Prerequisites #2).
run silently exits (but the binary itself runs): check .vm4a-run.log in
the bundle root.
- "No bridged interfaces available": CLI is not signed with
com.apple.vm.networking. Re-run the codesign command from the top.
- "Rosetta is not supported on this host": CPU doesn't expose VMX for
Rosetta translation. Not fixable in software.
push returns HTTP 401: set VM4A_REGISTRY_USER / VM4A_REGISTRY_PASSWORD
(a PAT for GHCR, a Docker token for Docker Hub).
ssh hangs: VM hasn't DHCP'd yet (vm4a ip returns empty). Wait 10-30s
after vm4a run for first-boot initialization.
snapshot save / --save-on-stop / reset fail with VZErrorSave "permission denied" (macOS 26): the binary is ad-hoc signed. VZ state save
needs hardened runtime + a real identity — re-sign with --options runtime
and an Apple Development / Developer ID identity (see Prerequisites #2).
When NOT to use the CLI
Redirect to the GUI app for:
- Walking the user through Setup Assistant interactively (open VM4A.app, pick the bundle, click Run, do the GUI steps; come back to CLI afterward).
- Changing graphics resolution / audio config (GUI has device editors)
- First-time users who want a wizard
What's new vs older guides
Most recent:
Named snapshots:
snapshot save NAME / restore NAME / snapshot list / snapshot rm, plus
top-level vm4a restore — named .vzstate snapshots stored inside the bundle,
no manual path juggling. Requires hardened-runtime + real-identity signing on
macOS 26 (see Prerequisites #2).
v2.0 P1 — MCP server:
mcp (stdio JSON-RPC 2.0 server; register in .mcp.json to expose
every primitive as an MCP tool to Claude Code / Cursor / Cline)
v2.0 P0 — agent-first primitives:
spawn (one-shot create+start with optional --from <oci-ref>, --wait-ssh)
exec (SSH-driven command runner with structured JSON return)
cp (SCP host ↔ guest with : prefix convention)
fork (clone + re-identify + optional auto-start with snapshot restore)
reset (stop + restart from snapshot for retry loops)
For any agent / scripted flow, prefer these over the manual
create → run → ssh chain. They handle PID management,
snapshot wiring, and JSON output by default.
Earlier additions (still relevant):
push / pull (OCI registry support, tart-style)
network list, image list (host introspection)
ip, ssh (NAT convenience)
agent status, agent ping (guest-agent channel, scaffold)
create --rosetta, create --bridged-interface
run --restore / run --save-on-stop (macOS 14+ snapshots)
Source: everettjf/vm4a — distributed by TomeVault.
1---2name: everettjf-vm4a-vm4a3description: VM4A CLI4---56# VM4A CLI78VM4A ships a single binary `vm4a` that creates and runs Apple9`Virtualization.framework` VMs on Apple Silicon from the command line. Each10VM is a directory ("bundle") containing `config.json`, `state.json`, a disk11image, and platform identity files. One binary handles the lifecycle, OCI12distribution, and agent channel.1314## Prerequisites (check first, don't assume)1516Before running anything:17181. **Apple Silicon + macOS 13+**. x86 Macs are not supported.192. **CLI must be codesigned** with the CLI entitlements, otherwise `run`20 fails silently:21 ```bash22 swift build23 codesign --force --sign - \24 --entitlements Sources/VM4ACLI/VM4ACLI.entitlements \25 ./.build/debug/vm4a26 ```27 Bridged networking and Rosetta share need the CLI entitlements file28 specifically — not the App one.2930 **macOS 26 (Tahoe) gotcha:** `com.apple.vm.networking` is a *restricted*31 entitlement. Ad-hoc signing (`--sign -`) a binary that carries it makes32 **AMFI SIGKILL the process at launch** — every `vm4a` command dies with no33 output (exit `137`). If that happens, re-sign with an entitlements file that34 drops `com.apple.vm.networking` (NAT-only; keeps `com.apple.security.virtualization`):35 ```bash36 cp Sources/VM4ACLI/VM4ACLI.entitlements /tmp/nat.entitlements37 /usr/libexec/PlistBuddy -c "Delete :com.apple.vm.networking" /tmp/nat.entitlements38 codesign --force --sign - --entitlements /tmp/nat.entitlements ./.build/debug/vm4a39 ./.build/debug/vm4a --version # must print, not get killed40 ```41 Ad-hoc NAT covers spawn/exec/cp/run-code/expose-port/OCI — but NOT bridged42 networking and NOT snapshots.43 **Snapshots on macOS 26 (verified):** `saveMachineStateTo` is gated on44 **hardened runtime + a real signing identity**. Ad-hoc signing fails with45 `VZErrorSave "permission denied"` (so `snapshot save`, `run --save-on-stop`,46 and `reset` all fail). `validateSaveRestoreSupport()` is YES — it's purely a47 signing issue. Re-sign with `--options runtime` + an Apple Development /48 Developer ID identity:49 ```bash50 codesign --force --options runtime \51 --sign "Apple Development: YOUR NAME (TEAMID)" \52 --entitlements /tmp/nat.entitlements ./.build/debug/vm4a53 ```54 Bridged mode additionally needs an identity *provisioned* for the managed55 `com.apple.vm.networking` entitlement (ad-hoc cannot grant it).563. For bridged mode: `vm4a network list` should print at least one57 interface. If empty, re-check codesigning.5859## Command map (when to reach for each)6061**Agent-first primitives (prefer these for any agent-driven flow):**6263| Intent | Command |64| --- | --- |65| One-shot create+start, wait for SSH | `vm4a spawn NAME --from ghcr.io/you/img:tag --wait-ssh --output json` |66| Run a command in the guest, get JSON | `vm4a exec /path/to/bundle --output json -- python3 -c 'print(1+1)'` |67| Copy a file host→guest | `vm4a cp /path/to/bundle ./local.txt :/work/remote.txt` |68| Copy a file guest→host | `vm4a cp /path/to/bundle :/var/log/syslog ./syslog.txt` |69| Fork a bundle, auto-start, wait for SSH | `vm4a fork SRC DST --auto-start --from-snapshot clean.vzstate --wait-ssh` |70| Reset to a saved snapshot for retry | `vm4a reset /path/to/bundle --from clean.vzstate --wait-ip` |71| Save a running VM as a named snapshot (stops it) | `vm4a snapshot save /path/to/bundle clean` |72| Restore a VM to a named snapshot (~1s) | `vm4a restore /path/to/bundle clean` (alias for `vm4a snapshot restore`) |73| List / delete named snapshots | `vm4a snapshot list /path/to/bundle [--output json]`, `vm4a snapshot rm /path/to/bundle clean` |7475**Classic lifecycle:**7677| Intent | Command |78| --- | --- |79| Create a Linux VM bundle | `vm4a create NAME [--image ISO.iso] [--network bridged --bridged-interface en0] [--rosetta]` |80| Install a macOS VM from IPSW (10–20 min, drives VZMacOSInstaller) | `vm4a create NAME --os macOS --image macos-15.ipsw --cpu 4 --memory-gb 8 --disk-gb 80` |81| List bundles in a directory | `vm4a list --storage /tmp/vm4a [--output json]` |82| Start a VM in the background | `vm4a run /path/to/bundle` |83| Start in foreground (logs to stdout) | `vm4a run /path/to/bundle --foreground` |84| Boot macOS recovery | `vm4a run /path/to/bundle --recovery` |85| Restore from a VZ snapshot (macOS 14+) | `vm4a run /path/to/bundle --restore state.vzstate` |86| Save state on clean shutdown (macOS 14+) | `vm4a run /path/to/bundle --save-on-stop state.vzstate` |87| Stop a VM (SIGTERM, then SIGKILL if stuck) | `vm4a stop /path/to/bundle --timeout 20` |88| Clone a bundle (APFS clonefile when possible) | `vm4a clone SRC DST` |89| Show Linux ARM64 ISO catalog | `vm4a image list` |90| Show host bridged interfaces | `vm4a network list` |91| Look up VM's NAT IP | `vm4a ip /path/to/bundle [--output json]` |92| SSH into running VM (NAT) | `vm4a ssh /path/to/bundle --user root` |93| Push bundle to OCI registry | `vm4a push /path/to/bundle ghcr.io/you/name:tag` |94| Pull bundle from OCI registry | `vm4a pull ghcr.io/you/name:tag --storage /tmp/vm4a` |95| Check guest agent heartbeat | `vm4a agent status /path/to/bundle` |96| Ping guest agent | `vm4a agent ping /path/to/bundle` |97| Run as MCP server (stdio JSON-RPC) | `vm4a mcp` |98| Run HTTP API server on localhost | `vm4a serve --port 7777` |99| Tag operations into a session for replay/inspection | `vm4a exec ... --session run-42` |100| List/show recorded sessions | `vm4a session list`, `vm4a session show <id>` |101| Define a per-task spawn template | `vm4a pool create py --base /path/to/golden --snapshot clean.vzstate` |102| Mint a fresh task VM from a pool | `vm4a pool spawn py --wait-ssh` |103104## Key workflows105106### Agent loop (recommended — uses v2 primitives)107108```bash109# 1. First-time bootstrap: pull, start (arm save-on-stop), wait for SSH.110vm4a spawn dev --from ghcr.io/yourorg/python-dev-arm64:latest \111 --storage /tmp/vm4a \112 --save-on-stop /tmp/vm4a/dev/clean.vzstate \113 --wait-ssh --output json114115# 2. Install whatever, then stop — VM saves state on shutdown.116vm4a exec /tmp/vm4a/dev -- bash -lc "apt-get install -y ripgrep"117vm4a stop /tmp/vm4a/dev118119# 3. Per-task: fork the golden bundle, push code, run, parse JSON.120vm4a fork /tmp/vm4a/dev /tmp/vm4a/task-$JOB_ID \121 --auto-start --from-snapshot /tmp/vm4a/dev/clean.vzstate --wait-ssh122vm4a cp /tmp/vm4a/task-$JOB_ID ./step.py :/work/step.py123vm4a exec /tmp/vm4a/task-$JOB_ID --output json --timeout 120 \124 -- python3 /work/step.py125126# 4. Bad task state? Reset back to the snapshot in <1s.127vm4a reset /tmp/vm4a/task-$JOB_ID --from /tmp/vm4a/dev/clean.vzstate --wait-ip128```129130### Spin up a disposable Ubuntu VM (manual / classic flow)131132```bash133ISO=~/Downloads/ubuntu-24.04-live-server-arm64.iso134vm4a create demo --storage /tmp/vm4a --image "$ISO" \135 --cpu 4 --memory-gb 8 --disk-gb 64136vm4a run /tmp/vm4a/demo137vm4a ip /tmp/vm4a/demo # needs VM to have booted + DHCP'd138vm4a ssh /tmp/vm4a/demo # after cloud-init / user setup139vm4a stop /tmp/vm4a/demo140```141142### Distribute a pre-baked VM through GHCR143144```bash145# Push (requires a PAT with write:packages)146export VM4A_REGISTRY_USER=youruser147export VM4A_REGISTRY_PASSWORD=ghp_xxx148vm4a push /tmp/vm4a/base-ubuntu ghcr.io/youruser/base-ubuntu:24.04149150# Pull on another machine (anonymous if package is public)151vm4a pull ghcr.io/youruser/base-ubuntu:24.04 --storage /tmp/vm4a152vm4a run /tmp/vm4a/base-ubuntu153```154155### Fast fork of a golden image for CI156157```bash158vm4a clone /tmp/vm4a/golden /tmp/vm4a/job-$CI_JOB_ID159vm4a run /tmp/vm4a/job-$CI_JOB_ID160trap "vm4a stop /tmp/vm4a/job-$CI_JOB_ID && rm -rf /tmp/vm4a/job-$CI_JOB_ID" EXIT161```162163`clone` uses APFS `clonefile(2)` on the same volume so it's O(directory164entries), not O(disk image size).165166## Non-obvious behaviors167168- **NAT IP only** works for VMs using the default NAT attachment. Bridged169 VMs don't land in `/var/db/dhcpd_leases`; users must pass `--host <ip>` to170 `vm4a ssh` or use their router's DHCP view.171- **`--rosetta` is Linux only** and requires172 `softwareupdate --install-rosetta --agree-to-license` before first run.173 The CLI warns but doesn't block.174- **macOS guests work end-to-end via the CLI**, with one caveat: after175 `vm4a create --os macOS --image foo.ipsw` finishes (10–20 min), the176 VM lands at Apple's Setup Assistant on first boot. Apple does not177 expose a scriptable skip path, so the user needs to click through it178 once in VM4A.app: pick region/keyboard, skip Apple ID, **create a179 user account**, then **System Settings → General → Sharing → Remote180 Login: ON**. After that single click-through, every CLI / MCP / HTTP181 / SDK operation works on the macOS bundle exactly like on Linux,182 and pulling a published macOS bundle from GHCR skips Setup Assistant183 entirely (the user account + Remote Login are baked in).184- **Config JSON format** starts at `schemaVersion: 1`. Old bundles without185 the field still load — tolerant decoding treats missing as 1. When186 adding new fields, make them optional in Core.swift decoding.187- **`vm4a stop` requires a running pid.** If `vm4a list` shows `stopped`188 but stale files exist, just re-run `vm4a run`; the CLI cleans stale189 PID files on the next list.190- **`--output json` is available on `create`, `list`, `ip`, `agent status`,191 and on every v2 primitive (`spawn`, `exec`, `cp`, `fork`, `reset`).**192 Output is one JSON object/array per command invocation (not JSONL).193- **`vm4a exec` returns JSON with `exit_code`, `stdout`, `stderr`,194 `duration_ms`, `timed_out`.** The exit code is also the process exit code195 (so `if vm4a exec ... ; then ...` works in shell). With `--timeout` and a196 timed-out command, the agent gets `timed_out: true` and a non-zero exit.197- **`vm4a cp` uses `:` to mark guest paths**, not `host:` like docker cp. So198 `./local.txt :/work/file.txt` is host→guest. Both sides being host or199 both being guest is rejected.200- **`vm4a fork` re-randomises `MachineIdentifier` AND the NIC MAC automatically.**201 Don't hand-roll a clone+identity flow — `fork` is what `clone` should have been202 for agent loops, with optional `--auto-start` and `--from-snapshot`. (Re-randomising203 the MAC is what stops parallel forks from colliding on the NAT lease table.)204- **`vm4a ip` matches the lease by the bundle's persisted NIC MAC**, not by205 hostname. New bundles store a fixed MAC in `config.json`; if no lease matches206 it yet, `ip` returns empty / exit 1 (it no longer dumps the whole lease table).207 Legacy bundles without a stored MAC still fall back to hostname matching.208- **`vm4a snapshot save/restore/list/rm` (+ top-level `vm4a restore`) are the209 simple path** — named `.vzstate` snapshots stored in `<bundle>/.vm4a-snapshots/`,210 no path juggling. `save` captures the running VM then stops it (it signals the211 worker via SIGUSR1); `restore` reboots to that state in ~1s. Prefer these over212 hand-managing `run --save-on-stop` / `run --restore` / `reset` paths. **Needs213 the hardened-runtime + real-identity signing** (see Prerequisites #2).214- **`vm4a exec` parses its options in any position** (the command goes after215 `--`): both `vm4a exec /b --output json -- cmd` and `vm4a exec --output json /b -- cmd`216 work. Same for `cluster exec` and `ssh` extra args.217- **`vm4a reset` requires a `.vzstate` file**, which means macOS 14+. On218 older hosts the agent has to fall back to `clone` + reinstall.219- **Guest agent is a scaffold.** Only `ping` is implemented. Don't promise220 clipboard/shutdown/script execution yet.221222## Error exit codes (for scripting)223224```2251 VM4AError.message Generic failure (legacy path)2262 VM4AError.notFound Bundle or file missing2273 VM4AError.alreadyExists Destination already exists2284 VM4AError.invalidState VM running when it should be stopped (or vice versa)2295 VM4AError.hostUnsupported macOS version / hardware capability missing2305 VM4AError.rosettaNotInstalled231```232233## When the user is stuck234235- **Every `vm4a` command exits with code `137` / no output (macOS 26):** AMFI236 killed the ad-hoc-signed binary because of the restricted237 `com.apple.vm.networking` entitlement. Re-sign NAT-only (see Prerequisites #2).238- `run` silently exits (but the binary itself runs): check `.vm4a-run.log` in239 the bundle root.240- "No bridged interfaces available": CLI is not signed with241 `com.apple.vm.networking`. Re-run the codesign command from the top.242- "Rosetta is not supported on this host": CPU doesn't expose VMX for243 Rosetta translation. Not fixable in software.244- `push` returns HTTP 401: set `VM4A_REGISTRY_USER` / `VM4A_REGISTRY_PASSWORD`245 (a PAT for GHCR, a Docker token for Docker Hub).246- `ssh` hangs: VM hasn't DHCP'd yet (`vm4a ip` returns empty). Wait 10-30s247 after `vm4a run` for first-boot initialization.248- `snapshot save` / `--save-on-stop` / `reset` fail with `VZErrorSave249 "permission denied"` (macOS 26): the binary is ad-hoc signed. VZ state save250 needs hardened runtime + a real identity — re-sign with `--options runtime`251 and an Apple Development / Developer ID identity (see Prerequisites #2).252253## When NOT to use the CLI254255Redirect to the GUI app for:256- Walking the user through Setup Assistant interactively (open VM4A.app, pick the bundle, click Run, do the GUI steps; come back to CLI afterward).257- Changing graphics resolution / audio config (GUI has device editors)258- First-time users who want a wizard259260## What's new vs older guides261262Most recent:263264**Named snapshots:**265- `snapshot save NAME` / `restore NAME` / `snapshot list` / `snapshot rm`, plus266 top-level `vm4a restore` — named `.vzstate` snapshots stored inside the bundle,267 no manual path juggling. Requires hardened-runtime + real-identity signing on268 macOS 26 (see Prerequisites #2).269270**v2.0 P1 — MCP server:**271- `mcp` (stdio JSON-RPC 2.0 server; register in `.mcp.json` to expose272 every primitive as an MCP tool to Claude Code / Cursor / Cline)273274**v2.0 P0 — agent-first primitives:**275- `spawn` (one-shot create+start with optional `--from <oci-ref>`, `--wait-ssh`)276- `exec` (SSH-driven command runner with structured JSON return)277- `cp` (SCP host ↔ guest with `:` prefix convention)278- `fork` (clone + re-identify + optional auto-start with snapshot restore)279- `reset` (stop + restart from snapshot for retry loops)280281For any agent / scripted flow, prefer these over the manual282`create` → `run` → `ssh` chain. They handle PID management,283snapshot wiring, and JSON output by default.284285Earlier additions (still relevant):286- `push` / `pull` (OCI registry support, tart-style)287- `network list`, `image list` (host introspection)288- `ip`, `ssh` (NAT convenience)289- `agent status`, `agent ping` (guest-agent channel, scaffold)290- `create --rosetta`, `create --bridged-interface`291- `run --restore` / `run --save-on-stop` (macOS 14+ snapshots)292293---294> Source: [everettjf/vm4a](https://github.com/everettjf/vm4a) — distributed by [TomeVault](https://tomevault.io).295<!-- tomevault:4.0:skill_md:2026-07-04 -->