Documentation
From a clone of the repo to a running macOS VM
One thing is worth understanding before you start: macOS is installed once, into a shared base image, and every VM after that is a copy-on-write layer on top of it.
Requirements
- An Apple silicon Mac. Intel Macs cannot run macOS guests.
- macOS 27 or newer on the host, the base-image format uses DiskImageKit, which shipped in macOS 27. Linux guests work on older hosts.
- Xcode 27 to build from source.
- Roughly 40 GB free for the first base image, plus whatever each VM writes.
Check the host before you start:
spook doctor
Install
Build from source today. There are no signed binary releases yet; when there are, they will appear on the repository's Releases page.
git clone https://github.com/Spooky-Labs/spooktacular.git
cd spooktacular && ./build-app.sh release
That produces Spooktacular.app, with the CLI inside it. Put
it on your PATH:
sudo ln -sf "$PWD/Spooktacular.app/Contents/MacOS/spook" /usr/local/bin/spook
The app and the CLI are one engine rather than two implementations, so a VM created in either is a first-class citizen of the other.
Your first VM
The first macOS create does two expensive things once: it downloads a restore image, and it installs macOS into the shared base image. It also needs root once, to write the provisioner into that base.
sudo spook create dev
Resolving the latest macOS restore image… Downloading IPSW (100%) Building the macOS base image — one time, 10-20 minutes. Installing base: 100% Injecting provisioner into the base… Sealing the base image read-only… ✓ Base image ready. ✓ VM created from the cached base image. Address 192.168.64.3
Then start it:
spook start dev
root:wheel. It goes into the
base image, so it is written once rather than per VM. On an EC2 Mac,
where Spooktacular runs as a root service, nothing is ever asked. In the
app you can approve the privileged helper once in System Settings
instead of using sudo.
More VMs, instantly
With a base image present, a create is an overlay layer plus two APFS clones:
spook create dev2
spook create dev3 --cpu 8 --memory 16 --disk 120
Each VM's disk starts as a near-empty delta and grows only with what the guest writes. Cloning an existing VM is faster still: a measured 30 ms for a 5 GB VM, consuming no additional disk, because APFS shares the blocks:
spook clone dev2 dev3
Ask a VM for its address at any time. This is a metadata read rather than a probe, because the address is reserved when the VM is created:
spook ip dev2
--remote-desktop,
--openclaw, --user-data, or any
--vm-password, stores that password in the root-owned
System keychain, so it needs root even when the base exists. That is
deliberate: the password never touches disk in plaintext. Creates
without an account need no privileges at all.
Published ports
Every VM gets its own private subnet and a reserved address, so a guest service can be published to the host the way a container's would be:
spook create web --publish 8080:80 --publish 2222:22
--publish takes hostPort:guestPort, or a bare
port to use the same number on both sides, and is repeatable. Two rules
cannot share a host port, and that fails immediately rather than
silently picking a winner. Templates contribute their own defaults: the
OpenClaw template publishes its gateway on 18789 without being asked:
spook create agent --openclaw && open http://localhost:18789
GitHub Actions runners
Store a personal access token with repository admin scope in the
Keychain. This is a PAT, not a runner registration token, create mints a short-lived registration token from it
seconds before the VM boots, so no long-lived credential reaches the
guest.
security add-generic-password -s com.spooktacular.github -a your-org -w <PAT> -U
Then one command creates, provisions, boots and waits for the runner to report online:
spook create runner-01 --github-runner --github-repo your-org/repo --github-token-keychain your-org --ephemeral
| Flag | What it does |
|---|---|
--github-repo | The owner/repo the runner registers against. |
--github-token-keychain | Keychain account holding the PAT, under service com.spooktacular.github. The only accepted source, never a flag, an environment variable, or a file. |
--ephemeral | The runner takes one job and exits. Reset the VM and the next job starts from the base image. |
| Runner labels | Every runner registers with self-hosted, macOS and ARM64. Extra labels are supported by the template but are not yet exposed as a CLI flag. |
--no-start | Create and provision, but do not boot. Useful for pre-warming a pool. |
/Library/Keychains/System.keychain to the
security command above.
Remote desktop
A remote-desktop VM provisions an admin account, skips Setup Assistant, and enables Screen Sharing and Remote Login on first boot.
sudo spook create desktop --remote-desktop --vm-user admin
sudo spook start desktop && open vnc://$(spook ip desktop)
Without --vm-password, a strong password is generated and
printed once at create time. Those account credentials are exactly what
you use to connect over VNC or SSH.
Linux guests
Linux VMs boot from a cloud image and are provisioned by cloud-init, so they need no base image and no privileges at all. Pass a distro alias and the latest aarch64 cloud image is resolved at create time.
spook create linux-01 --os linux --from-image fedora
spook create linux-02 --os linux --from-image debian --github-runner --github-repo your-org/repo --github-token-keychain your-org
fedora and debian resolve live against each
project's release index; you can also pass a path to a local raw image,
optionally .xz-compressed. To boot an installer ISO instead
and walk through it yourself, use --installer-iso. Rosetta
can be exposed to the guest for x86-64 binaries with
--rosetta.
Lifecycle
| Command | What it does |
|---|---|
spook list | Every VM with state, resources and provisioning status. Add --ip for addresses, --json for machine-readable output. |
spook stop | Shut the guest down cleanly. |
spook suspend | Save running state to disk and stop; a later start resumes it. |
spook clone | Copy-on-write duplicate, 30 ms for a 5 GB VM, no extra disk. |
spook snapshot | Manage disk snapshots for a stopped VM. |
spook delete | Remove the VM and its data. The shared base image is untouched. |
Base images
Base images live under the cache directory, one per macOS build:
ls ~/.spooktacular/cache/base/
To force a rebuild. After changing the bundled provisioner, or to reclaim space, delete the build's directory. The next create rebuilds it:
rm -rf ~/.spooktacular/cache/base/<build>
Bases are also rebuilt automatically when the provisioner version changes, so a VM never silently inherits a stale one. A base is sealed read-only after it is built and is never booted, which is what lets a warm pool prove its base is pristine after a reset.
Troubleshooting
Provisioning did not finish
The guest reports its first-boot exit code to the host over vsock, so
start prints a definitive result instead of guessing from a
timeout. When it fails, the script's own output is in the VM bundle:
cat ~/.spooktacular/vms/<uuid>.vm/provision/first-boot.stderr.log
"Creating macOS VMs requires macOS 27 or newer"
The base-and-overlay disk format uses DiskImageKit, which shipped in macOS 27. Linux guests work on older hosts.
"Building the macOS base image requires root"
Run the first create with sudo, or approve the privileged
helper in the app once. Only the base build needs this.
A published port does not answer
Confirm the guest service is listening, then check the VM's reserved
address with spook ip <name>. Forwarding rules are
installed when the VM starts, so a VM whose publications changed needs a
restart to pick them up.
Two VMs answer to the same name
Display names are not unique. Any command given an ambiguous name lists the matching UUIDs and exits; pass the UUID of the one you meant.
Everything else
spook doctor --strict
All commands
Twenty-seven subcommands. Each carries its own help:
spook help <command>.
| Commands | Area |
|---|---|
create | Create a VM, optionally provisioning it |
start stop | Boot or shut down |
suspend discard-suspend | Save running state to disk, or drop a saved state |
list get set | Inspect and change configuration |
clone delete | Duplicate copy-on-write, or remove |
ip ssh exec | Reach into a running VM |
snapshot share bundle | Disks, shared folders, bundle maintenance |
stream socket | Live event stream and its socket path |
service | Per-VM LaunchDaemons |
serve | The HTTP control-plane API |
doctor | Host readiness checks |
rbac identity iam | Roles, Secure-Enclave-bound keys, cloud IAM bindings |
sign-request | Sign an API request for curl or another client |
security-controls | Inventory of shipped controls, with code references |
rosetta | Rosetta 2 utilities for Linux guests |