Skip to content

Instantly share code, notes, and snippets.

@aessam
Last active February 22, 2026 21:12
Show Gist options
  • Select an option

  • Save aessam/aa9c32af6900123277c36d4d0ac7f73d to your computer and use it in GitHub Desktop.

Select an option

Save aessam/aa9c32af6900123277c36d4d0ac7f73d to your computer and use it in GitHub Desktop.
A Spec for ClawVM, repurpose Apple VM example to install and prepare image ready to be used for OpenClaw

ClawOS — Engineering Spec

macOS VM provisioning on Apple Silicon, from IPSW to SSH-ready in one command.

This spec documents the working implementation as of Feb 2026. It covers architecture, every gotcha encountered, and decisions made. Use it as a blueprint if you're building something similar.


1. What ClawOS Does

ClawOS provisions a fully-configured macOS VM from a single command:

sudo createClawOS --config claw.yml

The output is a .clawbundle directory containing a bootable macOS VM with:

  • An admin user account with SSH key authentication
  • Homebrew, GUI apps, npm tools — whatever you configure
  • VNC (Screen Sharing) enabled
  • A SwiftUI control panel for managing the VM

No pre-baked images. No third-party registries. Fresh IPSW restore every time.


2. Why sudo?

Disk injection (Stage 4) requires root for:

  • hdiutil attach / diskutil operations on the VM disk image
  • chown to set file ownership (UID 0 for LaunchDaemons, UID 501 for user files)
  • Writing to dslocal (/private/var/db/dslocal/nodes/Default/users/)
  • Setting permissions on SSH host keys (0600)

After injection completes, ClawOS restores the bundle ownership back to $SUDO_USER so the GUI app can access it without root.


3. Tech Stack

Layer Choice Why
VM Runtime Virtualization.framework Apple-native, no deps, supports macOS guests on Apple Silicon
Language Swift VZ.framework is Swift/ObjC native
CLI parsing swift-argument-parser --help generation, type-safe flags
YAML parsing Yams claw.yml is nested with arrays — hand-rolling is fragile
SSH /usr/bin/ssh shell-out Zero deps, inherits host SSH agent, no libssh2 needed
UI SwiftUI Control panel, live metrics, VM display via VZVirtualMachineView

Starting point: Apple's official sample project: "Running macOS in a Virtual Machine on Apple Silicon". We kept the Xcode project and restructured it.


4. Config File (claw.yml)

vm:
  cpu: 4                    # vCPU count (clamped to host physical cores)
  ram: 8GB                  # Parsed: "8GB" → 8589934592 bytes
  disk: 100GB               # Sparse disk, only uses space as needed
  macos: latest             # "latest" uses VZMacOSRestoreImage.fetchLatestSupported()
  display:                  # Specific versions query ipsw.me API
    width: 1920
    height: 1080
    dpi: 144

access:
  username: admin
  password: admin
  ssh_key: ~/.ssh/id_ed25519.pub   # Public key → authorized_keys
  vnc_password: clawos             # Max 8 chars (macOS limitation)

tools:
  homebrew: true
  packages: [git, node, python3, wget]        # brew install
  casks: [visual-studio-code, google-chrome]  # brew install --cask
  npm_global:                                 # npm install -g
    - "@anthropic-ai/claude-code"
    - "@google/generative-ai"
    - "@openai/codex"
    - "openclaw@latest"
    - "pnpm"
  custom:                                     # Shell commands, run in order
    - "git clone https://github.com/openclaw/openclaw.git ~/openclaw"
    - "cd ~/openclaw && pnpm install --frozen-lockfile"
    - "cd ~/openclaw && pnpm build"

Size parsing: "8GB" → uppercase → match suffix → multiply. Supports TB/GB/MB/KB.


5. Pipeline Stages

PHASE 1 — OFFLINE (no VM running)

  Stage 1: RESOLVE
    Find IPSW URL for the requested macOS version.

  Stage 2: DOWNLOAD
    Download IPSW with SHA256 verification. Cached in ~/ClawOS/Cache/.

  Stage 3: RESTORE
    Create disk image, run VZMacOSInstaller.

  Stage 4: INJECT
    Mount disk offline, write user account, SSH, VNC, Setup Assistant bypass.

PHASE 2 — ONLINE (VM running)

  Stage 5: BOOT
    Start VM, discover IP, wait for SSH.

  Stage 6: PROVISION
    SSH in, run generated provisioning script.

  Stage 7: VERIFY
    Check every tool is installed, write manifest.json.

  Stage 8: DONE
    Print connection info.

6. IPSW Resolution (Stage 1)

Two paths:

macos: latest — Uses VZMacOSRestoreImage.fetchLatestSupported(). Apple's own API. Returns a URL, build number, and supported hardware model. This is the simplest and most reliable path.

macos: "15.2" — Queries the ipsw.me REST API:

GET https://api.ipsw.me/v4/device/VirtualMac2,1

Parses the response to find the firmware matching the requested version. Returns URL, SHA256, size, and build number.

Important: The ipsw.me API uses VirtualMac2,1 as the device identifier for Apple Silicon VMs.


7. IPSW Download (Stage 2)

  • Standard URLSession download task
  • SHA256 verification via CryptoKit after download
  • IPSWs are cached at ~/ClawOS/Cache/restore-<version>-<build>.ipsw
  • Re-runs skip download if cache hit exists
  • Progress callback for CLI/UI display

8. VM Restore (Stage 3)

Disk Image Format

macOS 16+ (Tahoe): Uses Apple Sparse Image Format (ASIF). Created via:

try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)
// VZ creates the ASIF automatically during restore

macOS 15 and earlier: Uses RAW disk images. Created with dd or truncated file:

FileManager.default.createFile(atPath: diskURL.path, contents: nil)
let handle = try FileHandle(forWritingTo: diskURL)
try handle.truncate(atOffset: diskSizeBytes)

Restore Flow

  1. Load the IPSW → VZMacOSRestoreImage
  2. Extract mostFeaturefulSupportedConfiguration → hardware model
  3. Create VZMacPlatformConfiguration with hardware model + machine identifier + aux storage
  4. Build VZVirtualMachineConfiguration (CPU, RAM, display, network, storage)
  5. Run VZMacOSInstaller(virtualMachine:restoringFrom:) with progress callback
  6. Save hardware model + machine identifier to config.json in bundle (needed for boot)

CRITICAL LESSON: Wait After Restore

After VZMacOSInstaller completes, you must wait ~3 seconds before touching the disk image. VZ holds a lock on the file. Attempting to mount immediately will fail.

try await Task.sleep(nanoseconds: 3_000_000_000)

9. Disk Injection (Stage 4) — THE HARD PART

This is where 80% of the bugs lived. Every subsection below contains a lesson learned the hard way.

9a. Mounting the Disk Image

ASIF (macOS 16+):

diskutil image attach disk.img -nomount -plist

Returns a plist with the physical device path (e.g., /dev/disk44).

RAW (macOS 15-):

hdiutil attach disk.img -owners on -nomount -plist

Returns plist with system-entitiesdev-entry.

9b. Finding the Data Volume — HOST SAFETY CRITICAL

The VM disk contains an APFS container with System and Data volumes. You need the Data volume (that's where /private/var/db/, /Users/, /Library/ live via firmlinks).

THE DANGER: If you scan /Volumes looking for a volume with /private/var/db, you might match your HOST's Data volume and write to it. This is catastrophic.

SOLUTION — Volume Snapshot Approach:

1. Snapshot /Volumes BEFORE mount → Set A
2. Run `diskutil mountDisk /dev/diskNN`
3. Wait 3 seconds (APFS synthesized volumes need time)
4. Snapshot /Volumes AFTER mount → Set B
5. New volumes = B - A (these are OURS, not the host's)
6. Among new volumes, find the one with /private/var/db → Data volume

APFS SYNTHESIZED DISKS: When you attach an APFS disk image, macOS creates a "synthesized" disk. Physical disk is /dev/disk44, but the APFS container creates /dev/disk45 with the actual volumes. diskutil list disk44 only shows the physical partition map — it won't show the APFS volumes. You must use diskutil mountDisk which handles APFS container synthesis automatically.

FALLBACK: If no new volumes appear after mountDisk, use diskutil apfs list -plist to find APFS volumes on the synthesized disk and mount them individually.

9c. Enable Ownership

CRITICAL: After mounting, you MUST enable ownership on the volume or chown silently does nothing:

diskutil enableOwnership /dev/diskNN          # on the device
diskutil enableOwnership "/Volumes/Data Vol"  # on the mount point

Without this, all files appear as owned by the current user regardless of what chown sets.

9d. Skip Setup Assistant — 6 Layers

Setup Assistant is aggressive. A single marker file is not enough on macOS 26. We use 6 layers:

  1. .AppleSetupDone — Primary marker. Must be root-owned, mode 0400:

    <mount>/private/var/db/.AppleSetupDone
    
  2. .SetupRegComplete — Legacy marker:

    <mount>/Library/Receipts/.SetupRegComplete
    
  3. .skipbuddy — User template marker:

    <mount>/Library/User Template/English.lproj/.skipbuddy
    <mount>/Library/User Template/Non_localized.lproj/.skipbuddy
    
  4. Per-user com.apple.SetupAssistant.plist — All DidSee* keys set to true:

    <mount>/Users/<username>/Library/Preferences/com.apple.SetupAssistant.plist
    

    Keys: DidSeeCloudSetup, DidSeeSiriSetup, DidSeePrivacy, DidSeeAppearanceSetup, DidSeeAccessibility, DidSeeScreenTime, DidSeeTouchIDSetup, DidSeeTrueTonePrivacy, DidSeeActivationLock, LastSeenCloudProductVersion: "99.99"

  5. System-wide MDM-style managed plist — MUST be in /Library/Preferences/ (NOT per-user):

    <mount>/Library/Preferences/com.apple.SetupAssistant.managed.plist
    

    Keys: SkipAppearance, SkipCloudSetup, SkipiCloudStorageSetup, SkipPrivacySetup, SkipSiriSetup, SkipTrueTone, SkipScreenTime, SkipTouchIDSetup

    BUG WE HIT: Initially wrote this to the per-user preferences directory. Setup Assistant checks the SYSTEM /Library/Preferences/ location, not per-user. Must be root:wheel owned.

  6. System-wide com.apple.SetupAssistant.plist — Same DidSee* keys at system level:

    <mount>/Library/Preferences/com.apple.SetupAssistant.plist
    
  7. Firstboot script kill loop — Even with all offline markers, Setup Assistant can still appear on macOS 26. The firstboot LaunchDaemon kills it repeatedly for 15 seconds:

    for i in $(seq 1 15); do
        killall "Setup Assistant" 2>/dev/null || true
        sleep 1
    done &

    Plus writes all the defaults write keys from inside the running VM.

9e. Create User Account (dslocal)

Write a binary plist to:

<mount>/private/var/db/dslocal/nodes/Default/users/<username>.plist

The plist structure (all values are arrays — dslocal convention):

name: [username]
realname: [RealName]
uid: ["501"]
gid: ["20"]
home: [/Users/username]
shell: [/bin/zsh]
generateduid: [UUID]
passwd: ["********"]
ShadowHashData: [<binary plist with SALTED-SHA512-PBKDF2>]
authentication_authority: [";ShadowHash;HASHLIST:<SALTED-SHA512-PBKDF2>"]
_writers_passwd: [username]
_writers_hint: [username]
...

ShadowHashData is a nested binary plist:

{
  "SALTED-SHA512-PBKDF2": {
    "entropy": <derived key, 128 bytes>,
    "iterations": 39527,
    "salt": <random salt, 32 bytes>
  }
}

Generated using CCKeyDerivationPBKDF with kCCPRFHmacAlgSHA512.

Admin group: Update <mount>/private/var/db/dslocal/nodes/Default/groups/admin.plist:

  • Add username to GroupMembership array
  • Add user GUID to groupmembers array
  • Add username to users array

DO NOT create com.apple.access_ssh group. Its absence means ALL users can SSH. Creating it RESTRICTS SSH to only listed users.

Ownership: dslocal files must be root:wheel (0:0), mode 0600. User home must be 501:20.

9f. Enable SSH — 3 Mechanisms

  1. launchd disabled.plist override — Write com.openssh.sshd: false to:

    <mount>/private/var/db/com.apple.xpc.launchd/disabled.plist
    

    (false means "not disabled" = enabled)

  2. Backup LaunchDaemon — Custom com.clawos.sshd.plist that runs /usr/sbin/sshd -D:

    <mount>/Library/LaunchDaemons/com.clawos.sshd.plist
    

    This is a safety net if the system sshd doesn't start.

  3. Firstboot script — Runs systemsetup -setremotelogin on from inside the VM. This is the definitive online method.

SSH host keys: Pre-generate ssh_host_{rsa,ecdsa,ed25519}_key offline via ssh-keygen. Private keys must be 0600, public keys 0644, owned by root:wheel.

sshd_config: Write to <mount>/private/etc/ssh/sshd_config:

Port 22
PermitRootLogin yes
PubkeyAuthentication yes
PasswordAuthentication yes    # Hardened to "no" in provisioning Stage 6
AuthorizedKeysFile .ssh/authorized_keys
UsePAM yes

9g. Autologin (kcpassword)

To skip the login screen entirely, write the XOR-encoded password to:

<mount>/private/etc/kcpassword

XOR key: [0x7D, 0x89, 0x52, 0x23, 0xD2, 0xBC, 0xDD, 0xEA, 0xA3, 0xB9, 0x1F]

Algorithm:

  1. UTF-8 encode password + null terminator
  2. Pad to multiple of 12 bytes with zeros
  3. XOR each byte with the repeating key

Also write autoLoginUser to:

<mount>/Library/Preferences/com.apple.loginwindow.plist

9h. Firstboot LaunchDaemon

A shell script + LaunchDaemon that runs once at first boot as root:

<mount>/Library/Scripts/clawos-firstboot.sh
<mount>/Library/LaunchDaemons/com.clawos.firstboot.plist

The firstboot script handles everything that can't be done offline:

  • Step 0: Kill Setup Assistant aggressively (killall loop + defaults write)
  • Step 1: Create user via dscl if offline dslocal injection didn't work
  • Step 2: Add to admin group via dseditgroup
  • Step 3: Enable SSH via systemsetup -setremotelogin on
  • Step 4: Plant SSH authorized_keys
  • Step 5: Enable VNC / Screen Sharing
  • Step 6: Configure passwordless sudo (/etc/sudoers.d/clawos-<username>)
  • Step 7: Write PROVISIONING_READY marker, then self-destruct

CRITICAL: The PROVISIONING_READY marker MUST be written BEFORE launchctl bootout. bootout kills the running script. Anything after it never executes.

echo "PROVISIONING_READY"         # ← BEFORE self-destruct
rm -f /Library/LaunchDaemons/com.clawos.firstboot.plist
launchctl bootout system/com.clawos.firstboot 2>/dev/null || true

LaunchDaemon ownership: MUST be root:wheel, mode 0644. launchd refuses to load LaunchDaemons not owned by root.

9i. Detaching the Disk

LESSON: diskutil image has NO detach subcommand. Only attach exists. You must use hdiutil detach for both ASIF and RAW formats:

hdiutil detach /dev/diskNN -force

Fallback: diskutil unmountDisk force /dev/diskNN

9j. File Ownership

FileManager.setAttributes with .ownerAccountName FAILS on foreign APFS volumes. Always use /usr/sbin/chown with numeric UID:GID:

/usr/sbin/chown 0:0 /path     # root:wheel
/usr/sbin/chown 501:20 /path  # first user:staff

10. VM Boot + IP Discovery (Stage 5)

Boot

Load hardware model + machine identifier from config.json in the bundle. Reconstruct VZVirtualMachineConfiguration, create VZVirtualMachine, call start().

IP Discovery

The VM gets an IP via DHCP from the host's vmnet framework. Two discovery methods:

  1. DHCP lease file: Parse /var/db/dhcpd_leases. Match by MAC address (which we set during VM creation). This is fast but sometimes stale.

  2. ARP scan fallback: Ping the broadcast address 192.168.64.255, then parse the ARP table (arp -a -n). Check SSH port 22 on each candidate. First responder wins.

Wait for SSH

Poll every 5 seconds. The firstboot script needs time to:

  1. Boot macOS
  2. Run the firstboot LaunchDaemon
  3. Set up sudo
  4. Kill Setup Assistant
  5. Start sshd
  6. Write PROVISIONING_READY

We wait up to 60 seconds for the PROVISIONING_READY marker in /var/log/clawos-firstboot.log before starting provisioning. This ensures sudo is configured (needed for brew install).


11. SSH Provisioning (Stage 6)

Script Execution — stdin Corruption Bug

DO NOT pipe scripts via ssh user@host bash -s < script.sh or echo script | ssh ... bash -s.

Child processes (git, brew, curl) inherit stdin and consume remaining script text. Homebrew's install script reads from stdin. Git operations read from stdin. This causes random lines of your script to be eaten, producing bizarre failures where bash tries to execute fragments of your own script as commands.

SOLUTION: Write the script to a remote temp file, then execute the file:

// Encode script as base64, write to remote temp file
let b64 = Data(script.utf8).base64EncodedString()
ssh.execute("echo '\(b64)' | base64 -d > /tmp/provision.sh && chmod +x /tmp/provision.sh")

// Execute as a file (stdin is /dev/null — no corruption)
ssh.execute("bash /tmp/provision.sh")

Script Structure — Per-Step Error Isolation

DO NOT use set -euo pipefail at the script top. One failed step kills everything.

Instead, wrap each step in a subshell:

echo "=== [3/10] Installing brew packages ==="
(
  set -e
  eval "$(/opt/homebrew/bin/brew shellenv)"
  brew install git node python3 wget
)
if [ $? -ne 0 ]; then
    echo "=== STEP_FAILED 3/10 Installing brew packages ==="
    FAILURES=$((FAILURES + 1))
else
    echo "=== STEP_COMPLETE 3/10 ==="
fi

Provisioning Steps (Generated from Config)

  1. Set hostname
  2. Install Xcode Command Line Tools (required before Homebrew)
  3. Install Homebrew (clean /opt/homebrew first to prevent stale git locks)
  4. brew install packages
  5. brew install --cask GUI apps
  6. npm install -g globals
  7. Custom commands (each as its own step)
  8. Enable Screen Sharing / VNC
  9. SSH hardening (disable password auth)

Xcode CLT Installation

Homebrew requires Xcode CLT. Install non-interactively:

sudo touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
CLT=$(softwareupdate -l | grep -o '.*Command Line Tools.*' | head -1 | sed 's/^ *//')
sudo softwareupdate -i "$CLT" --verbose
sudo rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress

The .installondemand.in-progress touch file makes softwareupdate list CLT as available.

Homebrew Stale Git Lock

If a previous brew install was interrupted, /opt/homebrew/.git/index.lock persists and all subsequent installs fail. Always clean before install:

sudo rm -rf /opt/homebrew
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

VNC on macOS 26

The kickstart Perl script is broken on macOS 26 (Tahoe):

Can't call method "print" on an undefined value at kickstart line 695

Fallback approach:

launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist
defaults write /Library/Preferences/com.apple.RemoteManagement VNCLegacyConnectionsEnabled -bool true
defaults write /Library/Preferences/com.apple.RemoteManagement LoadRemoteManagementPreferences -bool true

Custom Commands

Each custom command runs as its own provisioning step with:

  • brew PATH sourced (eval "$(/opt/homebrew/bin/brew shellenv)")
  • Its own subshell for error isolation
  • Individual STEP_COMPLETE/STEP_FAILED markers

12. Tool Verification (Stage 7)

SSH in and check each configured tool:

Tool type Verification method
brew packages which <pkg> && <pkg> --version
CLI casks (e.g., VS Code) which code && code --version
GUI casks (e.g., Chrome) test -d '/Applications/Google Chrome.app' && defaults read '.../Info' CFBundleShortVersionString
npm CLI tools (e.g., claude) which claude && claude --version
npm libraries (e.g., @google/generative-ai) npm list -g @google/generative-ai

Important: Prepend /opt/homebrew/bin to PATH in all verification commands. Brew-installed tools aren't in the default PATH for non-login SSH sessions.

Writes manifest.json to the VM bundle:

{
  "tools": [
    {"name": "Homebrew", "version": "5.0.14", "installed": true},
    {"name": "git", "version": "git version 2.53.0", "installed": true},
    ...
  ],
  "timestamp": "2026-02-21T21:31:26Z",
  "vmName": "claw"
}

13. VM Bundle Structure

~/ClawOS/VMs/my-vm.clawbundle/
  disk.img           Sparse APFS disk image (ASIF on macOS 16+, RAW on 15-)
  aux.img            EFI auxiliary storage (NVRAM, boot config)
  config.json        Hardware model + machine identifier (needed for boot)
  manifest.json      Installed tools with versions

14. SwiftUI Control Panel

Tabs:

  • Display — Live VM screen via VZVirtualMachineView
  • Metrics — CPU, memory, disk, network (SSH-polled every 2s)
  • Logs — Scrolling provisioning output
  • Connect — SSH/VNC commands with copy buttons
  • Tools — Manifest viewer (tool name, version, installed status)
  • Provisioning — Stage-by-stage progress during build

Status bar: green/orange/red indicator, IP address, Start/Stop/Restart buttons.

The app uses NavigationSplitView with a sidebar listing all .clawbundle directories.


15. Project Structure

Sources/
  CLI/
    CreateCommand.swift        @main ParsableCommand, orchestrates pipeline
    ConfigParser.swift         YAML → ClawConfig via Yams
  Core/
    IPSWResolver.swift         Resolve IPSW URL (VZ API or ipsw.me)
    IPSWDownloader.swift       URLSession download + SHA256
    VMRestorer.swift           VZMacOSInstaller wrapper
    VMConfigBuilder.swift      Build VZVirtualMachineConfiguration
    DiskInjector.swift         Offline disk injection engine (~820 lines)
    PasswordHasher.swift       SHA512-PBKDF2 for dslocal ShadowHashData
    VMRunner.swift             VM lifecycle + IP discovery
    SSHProvisioner.swift       SSH command/script execution
    ProvisionScriptGenerator.swift  Generate provision.sh from config
    ToolVerifier.swift         Verify tools, write manifest
    VMBundle.swift             Bundle paths + listing
    Logger.swift               Stage-aware logging
  Models/
    ClawConfig.swift           Codable struct mirroring claw.yml
    PipelineStage.swift        Stage enum + progress tracking
    VMState.swift              @Observable VM state for UI
  UI/
    ClawOSApp.swift            @main SwiftUI App
    ControlPanelView.swift     Status bar + tab container
    VMDisplayView.swift        VZVirtualMachineView wrapper
    MetricsView.swift          CPU/RAM/disk/network gauges
    LogStreamView.swift        Scrolling log
    QuickConnectView.swift     SSH/VNC copy buttons
    ToolsManifestView.swift    Installed tools list
    ProvisioningProgressView.swift  Stage progress
    BundleListView.swift       Sidebar bundle list

16. Entitlements

com.apple.security.virtualization
com.apple.security.network.client
com.apple.security.network.server
com.apple.security.files.user-selected.read-write
com.apple.security.device.audio-input    (microphone passthrough)

17. Requirements

Requirement Minimum
Host Mac Apple Silicon (M1+)
Host OS macOS 15+ (macOS 16+ for ASIF disk format)
Xcode 16+
Free disk ~60 GB (IPSW ~13 GB + VM disk)
Host RAM 16 GB (to give VM 8 GB comfortably)
SSH key ~/.ssh/id_ed25519 key pair

18. Known Gotchas Summary

Issue Root cause Fix
Host volume targeted during injection /Volumes scan matches host Data volume Volume snapshot (before/after mount diff)
diskutil image detach doesn't exist Only attach subcommand exists Use hdiutil detach for both ASIF and RAW
chown silently ignored Ownership not enabled on volume diskutil enableOwnership after mount
FileManager.setAttributes fails Doesn't work on foreign APFS volumes Shell out to /usr/sbin/chown with numeric UID:GID
LaunchDaemon refused by launchd Not owned by root:wheel chown 0:0, chmod 644
Setup Assistant still appears Offline markers insufficient on macOS 26 6 offline layers + firstboot kill loop
Managed plist ignored Written to per-user dir instead of /Library/Preferences/ Write to system-wide location, root-owned
bash -s script corruption Child processes consume stdin Write script to remote temp file, execute as file
set -euo pipefail kills pipeline One step failure aborts all remaining Per-step subshell with error isolation
Homebrew fails on re-run Stale .git/index.lock from interrupted install sudo rm -rf /opt/homebrew before install
PROVISIONING_READY never written launchctl bootout kills script first Write marker BEFORE self-destruct
Provisioning runs before sudo ready Firstboot not complete Wait for PROVISIONING_READY in firstboot log
VNC kickstart broken on macOS 26 Perl script error in kickstart Fallback to defaults write + launchctl
npm tools not found during verify brew PATH not in SSH session Prepend /opt/homebrew/bin in all verification
google-chrome shows MISSING It's a .app, not a CLI binary Check /Applications/Google Chrome.app
@google/generative-ai shows MISSING It's a library, not a CLI Use npm list -g instead of which
APFS volumes not found after attach Synthesized disks not in diskutil list Use diskutil mountDisk, then diskutil apfs list fallback

19. Out of Scope (v1)

  • Intel Mac support (VZ macOS VMs = Apple Silicon only)
  • Snapshots / cloning (fresh restore by design)
  • Multiple concurrent VMs
  • iCloud login automation
  • Windows / Linux guests
  • Custom IPSW path (always downloads from Apple)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment