Complete guide for running macOS VMs with guest agent support on Proxmox VE.
| Setting | Value | Why |
|---|---|---|
| Machine | q35 | Required for OpenCore |
| BIOS | OVMF (UEFI) | Required for macOS |
| CPU | host or Nehalem | Nehalem for broadest compatibility |
| Agent | enabled=1,type=isa |
Required. ISA serial — the only channel Apple's built-in VirtIO agent doesn't claim |
| OS Type | other | Prevents PVE from making Linux assumptions |
| Balloon | 0 (disabled) | macOS doesn't support memory ballooning |
| macOS Version | Disk Type | Recommended Flags |
|---|---|---|
| 10.4–10.14 | SATA | discard=on,ssd=1 |
| 10.15+ | VirtIO Block | cache=writeback,discard=on,ssd=1 |
discard=onenables TRIM passthrough for thin provisioningssd=1tells macOS the disk supports TRIMcache=writebackimproves write performance (safe with battery-backed storage or when backups protect against data loss)
| macOS Version | Network Adapter |
|---|---|
| 10.4–10.14 | e1000 |
| 10.15+ | VirtIO (or e1000 as fallback) |
# Create VM
qm create 200 --name macos-vm --memory 8192 --cores 4 \
--cpu Nehalem --machine q35 --bios ovmf --ostype other \
--net0 virtio,bridge=vmbr0
# Add EFI disk
qm set 200 --efidisk0 local-lvm:1,efitype=4m,pre-enrolled-keys=0
# Add main disk (VirtIO for Big Sur+, SATA for older)
qm set 200 --virtio0 local-lvm:64,cache=writeback,discard=on,ssd=1
# Enable ISA serial guest agent
qm set 200 --agent enabled=1,type=isa
# Attach OpenCore and installer ISOs
qm set 200 --ide0 local:iso/OpenCore.iso,media=cdrom
qm set 200 --ide2 local:iso/macos-installer.iso,media=cdrom
# Set boot order
qm set 200 --boot order='ide0;virtio0'Old macOS VMs can't reach GitHub due to TLS incompatibility. Transfer the binary from another machine:
# On a modern machine — download the binary
curl -L -o mac-guest-agent \
https://github.com/mav2287/mac-guest-agent/releases/latest/download/mac-guest-agent
# Copy to the VM
scp mac-guest-agent user@<vm-ip>:/tmp/sudo cp /tmp/mac-guest-agent /usr/local/bin/mac-guest-agent
sudo chmod +x /usr/local/bin/mac-guest-agent
sudo /usr/local/bin/mac-guest-agent --installThe --install command:
- Copies the LaunchDaemon plist to
/Library/LaunchDaemons/ - Creates the freeze hooks directory at
/etc/qemu/fsfreeze-hook.d/ - Installs a default config at
/etc/qemu/qemu-ga.conf.default - Sets up log rotation via newsyslog
- Starts the service
# Inside the VM
sudo mac-guest-agent --self-test
# From the PVE host
qm agent 200 ping
qm agent 200 get-osinfo
qm agent 200 network-get-interfacesPVE calls guest-fsfreeze-freeze before taking a snapshot during backup. The agent responds by:
- Running freeze hook scripts (database flush, service pause)
- Creating an APFS snapshot via
tmutil(10.13+) - Calling
sync()+F_FULLFSYNCto flush all data to disk - Continuously syncing every 100ms during the freeze window
- Restricting commands to prevent new disk writes
After the snapshot, PVE calls guest-fsfreeze-thaw which reverses everything.
# Inside the VM
sudo mac-guest-agent --self-testLook for:
- APFS support: yes — freeze will create a COW snapshot (best consistency)
- tmutil snapshots: available — APFS snapshot mechanism is functional
- hook directory — hooks are installed and validated
- Backup readiness: ready — overall verdict
If APFS is not available (pre-10.13), freeze still works via sync() + F_FULLFSYNC, which flushes all data to physical media. This is the same level of consistency as most Linux VMs without LVM.
Verify freeze/thaw works before relying on it for production backups:
# From the PVE host
qm guest cmd 200 fsfreeze-freeze
qm guest cmd 200 fsfreeze-status # Should show "frozen"
# Take a manual snapshot
qm snapshot 200 test-snapshot
qm guest cmd 200 fsfreeze-thaw
qm guest cmd 200 fsfreeze-status # Should show "thawed"
# Clean up
qm delsnapshot 200 test-snapshotDrop scripts in /etc/qemu/fsfreeze-hook.d/ to flush databases before freeze:
# Example: /etc/qemu/fsfreeze-hook.d/mysql.sh
#!/bin/bash
case "$1" in
freeze) mysql -u root -e "FLUSH TABLES WITH READ LOCK;" ;;
thaw) mysql -u root -e "UNLOCK TABLES;" ;;
esacRequirements for hook scripts:
- Must be owned by root (uid 0)
- Must not be world-writable
- Must be executable
- 30-second timeout per script
- Scripts run alphabetically on freeze, reverse on thaw
See configs/hooks/ for ready-made hooks for common databases.
PVE host:
qm set 200 --virtio0 local-lvm:vm-200-disk-1,discard=on,ssd=1
# Requires VM restart (stop + start, not reboot)macOS VM (one-time, requires reboot):
sudo trimforce enableVerify:
diskutil info disk0 | grep -i "Solid State\|TRIM"
# Should show: Solid State: Yes, TRIM Support: YesAfter this, macOS sends TRIM automatically on every file delete. Free space is reclaimed on the PVE host in real-time. The guest-fstrim command is a no-op because macOS handles TRIM natively.
Space freed before TRIM was enabled needs a one-time manual reclaim:
# Inside the VM (run during maintenance window)
dd if=/dev/zero of=/tmp/.reclaim bs=4m 2>/dev/null; rm -f /tmp/.reclaim; syncAllows standard PVE operations (shutdown, backup freeze, system info) but blocks exec, file I/O, SSH keys, and password changes:
# /etc/qemu/qemu-ga.conf
[general]
allow-rpcs = guest-ping,guest-sync,guest-sync-delimited,guest-info,guest-get-osinfo,guest-get-host-name,guest-get-timezone,guest-get-time,guest-set-time,guest-get-users,guest-get-load,guest-get-vcpus,guest-get-memory-blocks,guest-get-memory-block-info,guest-get-cpustats,guest-get-disks,guest-get-fsinfo,guest-get-diskstats,guest-fsfreeze-status,guest-fsfreeze-freeze,guest-fsfreeze-thaw,guest-network-get-interfaces,guest-network-get-route,guest-shutdownRead-only queries. No modifications, no exec, no freeze:
[general]
allow-rpcs = guest-ping,guest-sync,guest-sync-delimited,guest-info,guest-get-osinfo,guest-get-host-name,guest-get-timezone,guest-get-time,guest-get-users,guest-get-load,guest-get-vcpus,guest-get-memory-blocks,guest-get-memory-block-info,guest-get-cpustats,guest-get-disks,guest-get-fsinfo,guest-get-diskstats,guest-fsfreeze-status,guest-network-get-interfaces,guest-network-get-routeAll commands enabled (default). Equivalent to Linux qemu-ga defaults.
Run this from the PVE host after setting up a macOS VM:
VMID=200
echo "=== PVE macOS VM Validation ==="
# 1. Agent config
echo -n "Agent config: "
grep -q "agent: enabled=1,type=isa" /etc/pve/qemu-server/$VMID.conf && echo "OK (ISA)" || echo "MISSING"
# 2. Disk settings
echo -n "Disk discard: "
grep -q "discard=on" /etc/pve/qemu-server/$VMID.conf && echo "OK" || echo "MISSING"
echo -n "Disk SSD: "
grep -q "ssd=1" /etc/pve/qemu-server/$VMID.conf && echo "OK" || echo "MISSING"
# 3. Agent ping
echo -n "Agent ping: "
qm agent $VMID ping >/dev/null 2>&1 && echo "OK" || echo "FAILED"
# 4. OS info
echo -n "OS info: "
qm agent $VMID get-osinfo 2>/dev/null | grep -q "macOS\|Mac OS" && echo "OK" || echo "FAILED"
# 5. Network
echo -n "Network: "
qm agent $VMID network-get-interfaces 2>/dev/null | grep -q "ip-address" && echo "OK" || echo "FAILED"
# 6. Freeze round-trip
echo -n "Freeze: "
qm guest cmd $VMID fsfreeze-freeze >/dev/null 2>&1 && \
qm guest cmd $VMID fsfreeze-thaw >/dev/null 2>&1 && echo "OK" || echo "FAILED"
echo "=== Done ==="Proxmox's per-VM memory gauge in the web UI for a macOS guest reflects the QEMU process's host-side memory footprint (cgroup RSS of the per-VM scope), not the guest's own view of its RAM usage. This is a structural limitation, not an agent gap. PVE's gauge sources its "memory used" figure from the virtio-balloon device when it can; that path requires a guest-side balloon driver that has negotiated VIRTIO_BALLOON_F_STATS_VQ. Most Linux distributions ship one; macOS does not, on any version. Apple has never shipped a virtio-balloon driver, and the virtio-balloon protocol has no host-pull alternative — without the driver, the balloon stats vq is empty and PVE falls back to cgroup RSS.
This agent does not change that. Installing it does not move the PVE web UI gauge.
What the agent does provide is the guest's memory view, on a separate query path. The guest-get-memory-blocks and guest-get-memory-block-info commands report the guest's memory blocks (block size × online block count), derived from macOS's Mach VM statistics. PVE's pvestatd and web UI don't call those — they're not in the cgroup/balloon code path the gauge reads — but you can call them yourself:
qm agent <vmid> get-memory-block-info # block size in bytes
qm agent <vmid> get-memory-blocks # array of {phys-index, online, can-offline}scripts/verify.sh does this and renders the result as a human-readable ~<used> GB used / ~<total> GB total line, which is the canonical "is the agent's memory path working" check for this project. If you need the same data from inside the VM, sudo mac-guest-agent --self-test-json also surfaces total memory under system_info.memory_bytes.
On reclamation: even if the gauge did read the agent, the host couldn't reclaim unused guest RAM — there is no balloon driver to inflate. The full allocated memory remains reserved by the VM on the host regardless. To free host RAM for other VMs, reduce the macOS VM's memory allocation in PVE.
PVE's qm agent and qm guest cmd only support a hardcoded subset of QGA commands. Newer commands like guest-network-get-route, guest-get-load, guest-get-cpustats, and guest-get-diskstats are not in PVE's allowlist yet.
To use these commands, send raw JSON via the QEMU monitor:
# Via QEMU monitor
qm monitor <vmid> <<< 'guest-network-get-route'
# Or test from inside the VM directly
echo '{"execute":"guest-network-get-route"}' | sudo mac-guest-agent --test
echo '{"execute":"guest-get-load"}' | sudo mac-guest-agent --test
echo '{"execute":"guest-get-cpustats"}' | sudo mac-guest-agent --testAll 45 commands work regardless of PVE's allowlist — PVE just can't invoke them through qm agent until they update their command list. libvirt's virsh qemu-agent-command has no such restriction.
If you followed a guide like klabsdev or similar to set up your macOS VM and now want to add the guest agent, you don't need to rebuild anything:
1. Add the agent to your PVE config:
qm set <vmid> --agent enabled=1,type=isaImportant: Always use
type=isa, even on Big Sur+. The default VirtIO serial channel is claimed by Apple's own built-in guest agent (which only supports 18 basic commands).type=isagives our agent a dedicated channel with all 45 commands including freeze.
2. Stop and start the VM (reboot is not enough — QEMU needs to create the serial device):
qm stop <vmid> && sleep 5 && qm start <vmid>3. Install the binary inside the VM (see Agent Installation above).
The network adapter type (vmxnet3, e1000, virtio) does not affect the guest agent — the agent uses a serial channel, which is a separate device from the network adapter.
-
Check agent is running in the VM:
sudo launchctl list com.macos.guest-agent
-
Check serial device exists:
ls -la /dev/cu.serial*If no serial device, verify PVE config has
agent: enabled=1,type=isaand the VM was fully stopped and restarted (not just rebooted). -
Check agent log:
tail -20 /var/log/mac-guest-agent.log
-
Run self-test:
sudo mac-guest-agent --self-test
-
Check hook scripts:
sudo mac-guest-agent --self-test # Reports hook validation ls -la /etc/qemu/fsfreeze-hook.d/ -
Test freeze manually:
# In the VM echo '{"execute":"guest-fsfreeze-freeze"}' | sudo mac-guest-agent --test echo '{"execute":"guest-fsfreeze-status"}' | sudo mac-guest-agent --test echo '{"execute":"guest-fsfreeze-thaw"}' | sudo mac-guest-agent --test
-
Check if a hook is hanging: hooks have a 30-second timeout. Check the log for timeout messages.
This PVE UI message appears when the agent hasn't responded to a ping within the timeout. Common causes:
- Agent not installed (
sudo mac-guest-agent --install) - Wrong agent type (
type=isarequired on every macOS version, including Big Sur+ where Apple's own agent claims the VirtIO channel — see line ~313 above for the full rationale) - VM needs full stop/start after changing agent config (reboot is not enough)
- Serial device not created (check
ls /dev/cu.serial*in the VM)
- Verify PVE disk has
discard=on,ssd=1 - Verify
trimforce enablewas run in the VM - Verify with
diskutil info disk0 | grep TRIM - Note: VM must be fully stopped and restarted after changing disk flags
| macOS | Binary | Disk | Network | Agent Type | Freeze |
|---|---|---|---|---|---|
| 10.4 Tiger | i386 | SATA | e1000 | type=isa | sync only |
| 10.5–10.6 | i386/x86_64 | SATA | e1000 | type=isa | sync only |
| 10.7–10.12 | x86_64 | SATA | e1000 | type=isa | sync + F_FULLFSYNC |
| 10.13–10.14 | x86_64 | SATA | e1000 | type=isa | sync + APFS snapshot |
| 10.15 | x86_64 | SATA or VirtIO | e1000 or VirtIO | type=isa | sync + APFS snapshot |
| 11.0+ | x86_64 | VirtIO | VirtIO | type=isa (required) | sync + APFS snapshot |