Skip to main content

Unattended Install (Beta)

The unattended driver finishes the macOS install for you. It waits for the OpenCore boot picker, boots recovery, erases the VM disk, runs startosinstall, and answers the picker on every reboot until macOS sits at Setup Assistant.

Nothing is installed inside the VM to make this work. The driver watches the console with qm screendump and types with qm sendkey, so it works on a stock Proxmox host.

warning

This is beta, and it erases the target VM disk without asking. Point it at a freshly created VM, never at one that holds data. Leave the VM console alone while it runs.

Entry Points

SurfaceHow to start it
TUI wizardStep 4 (VM Configuration): tick Unattended install (BETA)
CLIosx-next-cli install-unattended --vmid 910
Bash scriptAnswer yes to the UNATTENDED INSTALL (BETA) prompt in advanced settings

The bash prompt only appears when you also chose to start the VM after creation.

CLI

Run it right after apply --execute, while the VM is running and sitting at the boot picker.

osx-next-cli apply --execute \
--vmid 910 --name macos-sequoia --macos sequoia \
--cores 8 --memory 16384 --disk 128 \
--bridge vmbr0 --storage local-lvm

osx-next-cli install-unattended --vmid 910
FlagTypeRequiredDescription
--vmidintYesVM ID to drive
--disk-gbintNoTarget disk size in GB (default: read virtio0 from the VM config)

The command refuses to start if the VM is not running, or if the disk size cannot be read and --disk-gb was not given (exit code 2).

TUI

When the checkbox is ticked, the wizard spawns the driver as a detached process after the VM is created, so it keeps running when you close the TUI. Progress goes to a log file:

tail -f /var/log/osx-next-unattended-910.log

How It Works

PhaseWhat the driver does
Wait for pickerPolls the frame size until a 1080p OpenCore picker frame appears
Boot recoveryPresses ret on the first picker entry, waits for a smaller macOS frame
SettleSleeps while the recovery utilities window finishes loading
Open TerminalWalks the menu bar with ctrl-f2 and arrow keys to Utilities, Terminal
Erase and installTypes one line that erases the disk and launches the installer
Detach recoveryOn the installer's first reboot: stops the VM, qm set --delete ide2, starts it again
Answer rebootsPresses ret on every later 1080p frame, counting boots
Detect doneReturns once no picker frame has appeared for the quiet period

Why recovery gets detached

At the installer's first reboot the picker would otherwise list both the recovery volume and the installer. Walking entries blind with right, right, ret proved flaky: one dropped keystroke boots recovery again and the install stalls with no error. Detaching ide2 leaves exactly one real entry, so a blind ret can never pick the wrong volume.

The command it types

The target disk is matched by the size diskutil prints for it, never by a hardcoded /dev/disk node:

d=$(diskutil list | awk '/\*137.4 GB/{print $NF; exit}') && \
diskutil eraseDisk APFS MACOS $d && \
"/Install macOS "*.app/Contents/Resources/startosinstall \
--agreetolicense --volume /Volumes/MACOS --nointeraction

The size in that pattern is derived from --disk-gb, since QEMU sizes are binary GiB and diskutil prints decimal GB (128 GiB shows as 137.4 GB).

Frame-size heuristic

The driver tells screens apart by the byte size of the screendump, not by reading pixels:

ScreenResolutionFrame size
OpenCore picker1920x1080above 5,000,000 bytes
Recovery, installer, Setup Assistant1280x800below 4,500,000 bytes

Timeouts

Every phase is time-bounded. The Python and bash implementations share the same values, and tests/test_unattended.py diffs them so they cannot drift apart.

Python constantBash constantValuePurpose
PICKER_TIMEOUTUNATTENDED_PICKER_TIMEOUT600 sWait for the OpenCore picker
RECOVERY_TIMEOUTUNATTENDED_RECOVERY_TIMEOUT900 sWait for recovery to start
RECOVERY_SETTLEUNATTENDED_RECOVERY_SETTLE240 sLet the utilities window load
INSTALL_REBOOT_TIMEOUTUNATTENDED_INSTALL_REBOOT_TIMEOUT1800 sWait for the installer's first reboot
DONE_QUIETUNATTENDED_DONE_QUIET900 sNo picker for this long means the install is done
TOTAL_BUDGETUNATTENDED_TOTAL_BUDGET10800 sHard ceiling on the whole run
POLL_INTERVALUNATTENDED_POLL6 sScreendump polling interval

Verified Versions

Verified end to end on Ventura 13, Sonoma 14, Sequoia 15 and Tahoe 26, on both Intel and AMD hosts. A run takes 30 to 60 minutes.

When a Phase Times Out

The driver stops and leaves the VM exactly where it is, so you can take over on the Proxmox console. The CLI prints which phase failed and exits 1.

SymptomLikely cause
Picker never appearedVM was not at the boot picker when the driver started, or boot order is wrong
Recovery never startedRecovery image missing or attached to a slot other than ide2
Installer never rebootedThe erase step failed, usually a --disk-gb value that does not match the real disk
Budget exhaustedInstall is genuinely slower than 3 hours; finish it by hand on the console

After It Finishes

Complete Setup Assistant on the VM console, then lock in the boot order:

osx-next-cli post-install --vmid 910 --execute
note

The driver already detaches recovery at the first reboot, so post-install afterwards sets the OpenCore-first boot order (ide0;virtio0) and restores the picker's 15 second auto-boot, which the boot image disables during install.