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
Apple's licence sets the ceiling macOS permits two virtual machines per host. Spooktacular enforces that rather than letting you discover it partway through a build. Linux guests are unlimited.

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
first create
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
Why root, once The provisioner that runs your first-boot scripts is a LaunchDaemon written into the guest disk as 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
One create still needs root A create that provisions a guest account, --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
FlagWhat it does
--github-repoThe owner/repo the runner registers against.
--github-token-keychainKeychain account holding the PAT, under service com.spooktacular.github. The only accepted source, never a flag, an environment variable, or a file.
--ephemeralThe runner takes one job and exits. Reset the VM and the next job starts from the base image.
Runner labelsEvery 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-startCreate and provision, but do not boot. Useful for pre-warming a pool.
On an EC2 Mac The PAT must live in the System keychain, because the service runs as root and root cannot read a user's login keychain. Add /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

CommandWhat it does
spook listEvery VM with state, resources and provisioning status. Add --ip for addresses, --json for machine-readable output.
spook stopShut the guest down cleanly.
spook suspendSave running state to disk and stop; a later start resumes it.
spook cloneCopy-on-write duplicate, 30 ms for a 5 GB VM, no extra disk.
spook snapshotManage disk snapshots for a stopped VM.
spook deleteRemove the VM and its data. The shared base image is untouched.
A clone inherits its source's machine identifier The installer personalizes auxiliary storage against that identifier, so a fresh one would pair boot state with an identity it was never signed for. Apple warns against running two VMs with the same identifier at once, so treat a clone as a replacement for its source rather than a sibling to run beside it. MAC addresses are regenerated, so clones stay distinct on the network.

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>.

CommandsArea
createCreate a VM, optionally provisioning it
start stopBoot or shut down
suspend discard-suspendSave running state to disk, or drop a saved state
list get setInspect and change configuration
clone deleteDuplicate copy-on-write, or remove
ip ssh execReach into a running VM
snapshot share bundleDisks, shared folders, bundle maintenance
stream socketLive event stream and its socket path
servicePer-VM LaunchDaemons
serveThe HTTP control-plane API
doctorHost readiness checks
rbac identity iamRoles, Secure-Enclave-bound keys, cloud IAM bindings
sign-requestSign an API request for curl or another client
security-controlsInventory of shipped controls, with code references
rosettaRosetta 2 utilities for Linux guests