A native QEMU Guest Agent for macOS and OS X virtual machines. The missing guest agent for Mac — enables Proxmox VE, libvirt, plain QEMU, and UTM to manage macOS guests through the standard QGA protocol.
42 commands, real filesystem freeze, zero dependencies. Supports Mac OS X 10.4 Tiger through macOS 26 Tahoe. Works as both a macOS guest agent and an OS X guest agent for legacy VMs.
1. On the PVE host — enable the guest agent:
qm set <vmid> --agent enabled=1,type=isa
qm stop <vmid> && sleep 5 && qm start <vmid>Why
type=isa? As of v2.5.0 the agent supports ISA serial only; VirtIO transport was removed. macOS guests run either under Apple's Virtualization.framework (UTM Virtualize,vz_run, anythingVZVirtualMachine-backed) — where Apple's own 18-commandAppleQEMUGuestAgentclaims the VirtIO console channel — or under plain QEMU/KVM (Proxmox, libvirt, raw QEMU, UTM Emulate; typically OpenCore-booted) where Apple's agent never launches but VirtIO would still let an image silently start behaving differently if it ever moved between host classes. Restricting to ISA serial gives one transport, one install, one launchd config, and one channel-detection list everywhere —Apple16X50Serial.kexthas shipped on every macOS from 10.4 Tiger onwards with an identical PCI class match, so this is universally satisfiable. See docs/COMPATIBILITY.md for the full evidence and CHANGELOG v2.5.0 BREAKING for the v2.4.x → v2.5.0 migration.
2. In the macOS VM:
# Download the tri-fat binary to /tmp (i386 + x86_64 + arm64 in one file) —
# covers macOS 10.4 Tiger through 26 Tahoe; one URL for every host.
# Note: on Tiger / Leopard / older Snow Leopard guests, the VM's TLS stack
# usually cannot reach GitHub directly. Download on a modern machine and
# copy the file to /tmp on the VM (scp / shared folder / USB).
curl -fL -o /tmp/mac-guest-agent https://github.com/mav2287/mac-guest-agent/releases/latest/download/mac-guest-agent
# Install. --install does everything: extracts the host-native slice in-process
# (no lipo, no Developer Tools), places it at /usr/local/bin, then writes and
# loads the LaunchDaemon. The /tmp file is only the source — it is NOT the
# install location; delete it afterwards or let it clear on the next reboot.
chmod +x /tmp/mac-guest-agent
sudo /tmp/mac-guest-agent --install
rm -f /tmp/mac-guest-agent # optional: the daemon now lives in /usr/local/binThe download is one tri-fat binary so a single URL covers every host; the
installed file at /usr/local/bin/mac-guest-agent is the single slice your
kernel actually runs (e.g. i386 on Tiger), extracted in-process. That matters
beyond saving disk: the installed daemon's arch then matches the system tools it
must exec — on a Tiger 10.4.7+ guest XNU would otherwise grade the x86_64
slice of a fat binary, which can't execve Tiger's i386-only /bin (issue #9).
Don't hand-place the binary with lipo/mv/cp; let --install do it.
Or use the bootstrap wrapper (downloads, then runs --install for you; also
supports --local PATH for air-gapped installs and the VirtIO override):
curl -fsSL https://raw.githubusercontent.com/mav2287/mac-guest-agent/main/scripts/install.sh | sudo bashVirtIO override (unsupported): if your orchestrator hardcodes VirtIO (kubevirt-style) and ISA isn't available, see docs/NO_ISA_OVERRIDE.md — sudo ./mac-guest-agent --install --virtio runs the gated override path (macOS 11+ only, requires SIP disabled).
3. Verify:
# From PVE host
qm agent <vmid> ping
qm agent <vmid> get-osinfo
# From inside VM
sudo mac-guest-agent --self-testTo upgrade an existing install, download the new binary the same way and run it
with --upgrade — the running daemon self-sources, so you don't pass a path:
# Download (or transfer) the new release to /tmp, then on the VM:
curl -fL -o /tmp/mac-guest-agent https://github.com/mav2287/mac-guest-agent/releases/latest/download/mac-guest-agent
chmod +x /tmp/mac-guest-agent
sudo /tmp/mac-guest-agent --upgrade
rm -f /tmp/mac-guest-agent--upgrade backs up the current binary, extracts the native slice to
/usr/local/bin, regenerates the plist, restarts the daemon, and verifies it —
rolling back to the backup on verify failure. (--install on top of an existing
install is refused; use --upgrade.)
The agent communicates via an ISA serial port (16550 UART) using Apple's built-in Apple16X50Serial.kext driver, present on every macOS since 10.4. No custom kexts, no SIP changes, no code signing.
Note: macOS Big Sur and newer ship Apple's own
AppleQEMUGuestAgent(18 commands, no filesystem freeze). It only launches on Apple Virtualization.framework hosts — its IOKit match (AppleVirtIOAgentDevice) is set by theapplevirtio.consoledriver, which doesn't load under QEMU/OpenCore — so on Proxmox the VirtIO channel is actually free. We still default to ISA for symmetry across both host classes (see the Quick Start callout for the trade-off). Our agent provides 42 commands plus a real per-filesystem freeze dispatch — see docs/design/FREEZE_SEMANTICS.md for what "freeze" actually does perf_fstypename.
Single download: mac-guest-agent (i386 + x86_64 + arm64 in one tri-fat Mach-O, covers macOS 10.4 Tiger through 26 Tahoe). dyld picks the right slice at load time. (v2.5.0 published this as mac-guest-agent-darwin-universal for one day; v2.5.1+ drops the suffix.)
ISA serial driver (Apple16X50Serial.kext) verified present with identical PCI class match on every macOS from 10.4 Tiger (2005) through 26.3 Tahoe (2026). See the compatibility matrix for per-version evidence.
Tiger / Leopard (10.4 / 10.5):
/usr/local/binis not in the default shell PATH. Invoke the binary by its absolute path (sudo /usr/local/bin/mac-guest-agent ...) or add it to PATH for the session:export PATH=/usr/local/bin:$PATH. The LaunchDaemon itself uses the absolute path and is unaffected.
Open an issue at https://github.com/mav2287/mac-guest-agent/issues/new with as much of the following as you can collect. Items 1-3 work even when the binary won't launch (dyld rejection); items 4-6 require the binary to start successfully.
Loader-safe (no execution needed):
sw_vers— macOS version, buildfile /usr/local/bin/mac-guest-agent— shows the installed native slice (e.g.Mach-O executable i386on Tiger).--installthins the download to a single slice, so this is not a fat binary; the wrong arch here (or a fat binary) points at a slice-extraction problem.file ./mac-guest-agenton the downloaded file (before install) — confirms the tri-fat download is intact (Mach-O universal binary with 3 architectures).
If the binary starts:
mac-guest-agent --versionmac-guest-agent --self-test-json— pipe to a file orgrep selected_archdirectly (do NOT pipe throughpython -m json.toolon Tiger/Leopard — those versions lack a reliable stdlibjsonmodule)tail -50 /var/log/mac-guest-agent.log
The universal binary is the supported install path; if it doesn't load on your specific host configuration we want to fix it, not work around it.
sudo launchctl list com.macos.guest-agent # Status
sudo launchctl start com.macos.guest-agent # Start
sudo launchctl stop com.macos.guest-agent # Stop
tail -f /var/log/mac-guest-agent.log # Logs
sudo mac-guest-agent --uninstall # Removemake build # Current architecture (no SDK needed)
make build-arm64 # Apple Silicon (11.0+, no SDK needed)
make test # Run all testsThe legacy slices (i386 for 10.4+, x86_64 for 10.4+) require the macOS 10.13
SDK because modern Xcode SDKs no longer expose pre-10.7 deployment targets.
Download it once, then pass it to make:
curl -fL -o /tmp/sdk.tar.xz \
https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.13.sdk.tar.xz
# SHA256 1d2984acab2900c73d076fbd40750035359ee1abe1a6c61eafcd218f68923a5a
tar xf /tmp/sdk.tar.xz -C /tmp
make build-i386 LEGACY_SDK=/tmp/MacOSX10.13.sdk # 10.4+ (32-bit)
make build-x86_64 LEGACY_SDK=/tmp/MacOSX10.13.sdk # 10.4+ (64-bit, Tiger-compatible via -ld_classic)
make build-universal LEGACY_SDK=/tmp/MacOSX10.13.sdk # tri-fat (release shape)The legacy slices also depend on Apple's ld-classic linker (-Wl,-ld_classic
in the Makefile) to honor -mmacosx-version-min for entry-point selection;
the default ld-prime hardcodes LC_MAIN on x86_64 and produces binaries
that 10.4/10.5 dyld cannot load (issue #4). See .github/workflows/build.yml
for the full toolchain story.
| Guide | Description |
|---|---|
| Proxmox VE | VM settings, backup config, TRIM, security profiles, troubleshooting |
| libvirt / virt-manager | Domain XML, virsh commands, quiesced snapshots, troubleshooting |
| UTM | Local macOS VM automation, utmctl integration, CI/CD workflows |
| All Platforms | Plain QEMU setup, ISA serial transport |
| VM Configs & Troubleshooting | Per-OS known-good VM configs, and is it the VM or the agent? symptom→fix catalog |
| Commands | All 42 commands with status, Linux parity, privilege requirements |
| Compatibility | Support tiers, kext timeline, verification evidence per version |
| Backup & Freeze | Filesystem freeze, APFS snapshots, hook scripts, TRIM |
| Freeze Semantics | What "freeze" actually does per f_fstypename; divergences from upstream QGA |
| Security | Trust model, recommended profiles, freeze-state restrictions |
| Architecture | Data flow, protocol spec, macOS API usage |
| CLI Reference | All flags, config file format, examples |
| Changelog | Release history |
| Contributing | Build, style, testing guidelines |
MIT