# Everettjf Vm4a Vm4a

> VM4A CLI

- Skill: `tomevault-io/everettjf-vm4a-vm4a` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/everettjf-vm4a-vm4a`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/everettjf-vm4a-vm4a/raw
- Safety review: pending (external: skill-scanner PASS, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/everettjf-vm4a-vm4a

---


# 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:

1. **Apple Silicon + macOS 13+**. x86 Macs are not supported.
2. **CLI must be codesigned** with the CLI entitlements, otherwise `run`
   fails silently:
   ```bash
   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`):
   ```bash
   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:
   ```bash
   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).
3. 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)

```bash
# 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)

```bash
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

```bash
# 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

```bash
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](https://github.com/everettjf/vm4a) — distributed by [TomeVault](https://tomevault.io).
<!-- tomevault:4.0:skill_md:2026-07-04 -->

