UTM VM Automation (macOS)
UTM wraps QEMU and Apple's Virtualization.framework. There are two automation
surfaces, and they are not equivalent:
utmctl - bundled CLI. Lifecycle, guest exec/files/IP, clone, USB.
It cannot create, import, export, or configure VMs.
- AppleScript (via
osascript) - strict superset: everything utmctl does,
plus creating VMs, editing hardware config, and keyboard/mouse input injection.
Rule of thumb: lifecycle and guest ops → utmctl; creation and configuration →
AppleScript. Both drive UTM.app over Apple Events (the app auto-launches; there
is no true daemon mode).
UTMCTL=/Applications/UTM.app/Contents/MacOS/utmctl # not on PATH by default
Task routing
| Task |
Tool |
Read |
| Start/stop/status/clone/delete, exec in guest, push/pull files, get IP, USB |
utmctl |
references/utmctl.md |
| Create a VM, attach an ISO, change CPU/RAM/disk/network, port forwards, serial, input injection |
AppleScript |
references/applescript.md |
| Windows 11 on Apple Silicon: get the ISO, install, drivers, enable SSH/RDP, run PowerShell from the host |
both |
references/windows.md |
Everyday commands
$UTMCTL list # UUID / Status / Name
$UTMCTL start "MyVM" # name (exact) or UUID; resumes if suspended
$UTMCTL status "MyVM"
$UTMCTL stop "MyVM" # default is --force (power-off); --request asks guest
$UTMCTL clone "template" --name "worker-1"
$UTMCTL ip-address "MyVM" | head -1 # needs guest agent (see matrix below)
$UTMCTL exec "MyVM" --cmd uname --cmd -a # needs guest agent
echo hi | $UTMCTL file push "MyVM" /tmp/hi # stdin → guest file
$UTMCTL file pull "MyVM" /etc/hostname # guest file → stdout
VMs are identified by UUID or exact, complete name - no partial matching.
Guest agent support matrix - read before promising exec
utmctl exec, file, ip-address (and the AppleScript Guest Suite) require a
QEMU guest agent running inside the guest. Whether that's possible depends on
the guest/backend combination:
| Guest + backend |
Agent works? |
Automation path |
| Linux on QEMU |
✓ apt/dnf/apk install qemu-guest-agent |
utmctl exec or SSH |
| Windows x86_64 on QEMU |
✓ via UTM guest tools ISO |
utmctl exec or SSH |
| Windows ARM64 on QEMU (the normal case on Apple Silicon) |
◑ partial - guest tools ship the x64 qemu-ga (runs emulated): ip-address+file work, AppleScript execute works; utmctl exec returns no stdout. No native ARM64 agent (#5134) |
AppleScript Guest Suite, or SSH over the shared IP / port forward - see windows.md |
| Any guest on Apple VZ backend |
unreliable; no input injection or USB either |
SSH |
Prefer the QEMU backend when creating VMs for automation. For Windows ARM,
don't burn time trying to make the agent work - set up OpenSSH Server in the
guest (reachable on the shared-network IP, or pin a port forward). See
windows.md for the full path, including the gotchas below that cost hours.
Three Windows-ARM traps that look like "it's hung" but aren't fixable by
waiting (all in windows.md):
- A >4 GB Win11 24H2 ISO won't boot - hangs at the firmware "Start boot
option" screen. Repack the installer onto a FAT32 disk image with a split WIM.
virtio-gpu-pci display → "Display output is not active" black screen the
moment Windows boots. Use virtio-ramfb-gl (no Windows driver needed).
- A new scripted disk defaults to VirtIO, which Windows Setup can't see. Use
NVMe for the system disk.
Gotchas that waste hours
- Automation (TCC) permission: the first AppleScript/utmctl call from a new
parent app (Terminal, an agent harness) triggers a macOS consent prompt, or
fails with
-1743 if previously denied. Grant under System Settings →
Privacy & Security → Automation → calling app → UTM. The grant is per
calling binary; there's no supported headless pre-seed - trigger it once
interactively.
- Config is cached: UTM reads each VM's
config.plist once at app launch.
Editing the plist on disk while UTM runs does nothing. Use AppleScript
update configuration (VM must be stopped), or quit UTM → edit → relaunch.
Some fields (e.g. drive ImageType CD-vs-Disk) aren't in the AppleScript
dictionary at all → plist edit is the only way (windows.md).
- Diagnosing boot/firmware issues: set
QEMU.DebugLog true in config.plist
to dump the exact QEMU launch line (how drives are presented, display device)
to Data/debug.log. Far faster than guessing - see windows.md.
- Headless = no display device: there's no headless flag. Remove the
displays entry from the VM config (or omit it at creation) and use SSH or a
serial port; --hide only hides the UTM window.
- Clones share a MAC → same DHCP lease/IP. After
clone, blank the MAC via
AppleScript so UTM regenerates it (snippet in applescript.md).
delete has no confirmation and erases the VM's disks. Confirm with the
user before deleting, and prefer --disposable starts (changes discarded on
shutdown) for throwaway runs.
ping never works from guests (libslirp limitation). Don't gate
"is networking up" on ping; use a TCP check (curl, Test-NetConnection).
- Generated AppleScript must be plain ASCII - the
¬ line-continuation
char breaks under some locales. Use single-line records or variables.
Verifying an automation worked
utmctl status reflects the VM process, not guest readiness. After start,
poll for the thing you actually need: the SSH port accepting connections
(nc -z localhost 2222), the agent responding (utmctl exec ... --cmd true),
or ip-address returning a value. Boot can take 30-90s; poll with a timeout
rather than sleeping a fixed amount.
1---2name: utm3description: Control UTM virtual machines on macOS programmatically - create and configure VMs via AppleScript, manage lifecycle with utmctl, run commands and transfer files in guests, and set up Windows 11 ARM for headless automation (SSH, not the guest agent). Use whenever the user mentions UTM, utmctl, VMs on Apple Silicon, creating/cloning/scripting a virtual machine on a Mac, or wants to run commands inside a local Windows or Linux VM - even if they don't name UTM explicitly.4---5
6# UTM VM Automation (macOS)
7
8UTM wraps QEMU and Apple's Virtualization.framework. There are two automation
9surfaces, and they are not equivalent:
10
11- **`utmctl`** - bundled CLI. Lifecycle, guest exec/files/IP, clone, USB.
12 It **cannot create, import, export, or configure** VMs.
13- **AppleScript** (via `osascript`) - strict superset: everything utmctl does,
14 plus creating VMs, editing hardware config, and keyboard/mouse input injection.
15
16Rule of thumb: lifecycle and guest ops → `utmctl`; creation and configuration →
17AppleScript. Both drive UTM.app over Apple Events (the app auto-launches; there
18is no true daemon mode).
19
20```bash
21UTMCTL=/Applications/UTM.app/Contents/MacOS/utmctl # not on PATH by default
22```
23
24## Task routing
25
26| Task | Tool | Read |
27|---|---|---|
28| Start/stop/status/clone/delete, exec in guest, push/pull files, get IP, USB | `utmctl` | [references/utmctl.md](references/utmctl.md) |
29| Create a VM, attach an ISO, change CPU/RAM/disk/network, port forwards, serial, input injection | AppleScript | [references/applescript.md](references/applescript.md) |
30| Windows 11 on Apple Silicon: get the ISO, install, drivers, enable SSH/RDP, run PowerShell from the host | both | [references/windows.md](references/windows.md) |
31
32## Everyday commands
33
34```bash
35$UTMCTL list # UUID / Status / Name
36$UTMCTL start "MyVM" # name (exact) or UUID; resumes if suspended
37$UTMCTL status "MyVM"
38$UTMCTL stop "MyVM" # default is --force (power-off); --request asks guest
39$UTMCTL clone "template" --name "worker-1"
40$UTMCTL ip-address "MyVM" | head -1 # needs guest agent (see matrix below)
41$UTMCTL exec "MyVM" --cmd uname --cmd -a # needs guest agent
42echo hi | $UTMCTL file push "MyVM" /tmp/hi # stdin → guest file
43$UTMCTL file pull "MyVM" /etc/hostname # guest file → stdout
44```
45
46VMs are identified by UUID or **exact, complete name** - no partial matching.
47
48## Guest agent support matrix - read before promising `exec`
49
50`utmctl exec`, `file`, `ip-address` (and the AppleScript Guest Suite) require a
51QEMU guest agent running *inside* the guest. Whether that's possible depends on
52the guest/backend combination:
53
54| Guest + backend | Agent works? | Automation path |
55|---|---|---|
56| Linux on QEMU | ✓ `apt/dnf/apk install qemu-guest-agent` | `utmctl exec` or SSH |
57| Windows x86_64 on QEMU | ✓ via UTM guest tools ISO | `utmctl exec` or SSH |
58| **Windows ARM64 on QEMU** (the normal case on Apple Silicon) | ◑ partial - guest tools ship the **x64 qemu-ga (runs emulated)**: `ip-address`+`file` work, AppleScript `execute` works; **`utmctl exec` returns no stdout**. No *native* ARM64 agent ([#5134](https://github.com/utmapp/UTM/issues/5134)) | AppleScript Guest Suite, or **SSH** over the shared IP / port forward - see windows.md |
59| Any guest on Apple VZ backend | unreliable; no input injection or USB either | SSH |
60
61Prefer the **QEMU backend** when creating VMs for automation. For Windows ARM,
62don't burn time trying to make the agent work - set up OpenSSH Server in the
63guest (reachable on the shared-network IP, or pin a port forward). See
64windows.md for the full path, including the gotchas below that cost hours.
65
66**Three Windows-ARM traps that look like "it's hung" but aren't fixable by
67waiting** (all in windows.md):
68
69- A **>4 GB Win11 24H2 ISO won't boot** - hangs at the firmware "Start boot
70 option" screen. Repack the installer onto a FAT32 disk image with a split WIM.
71- **`virtio-gpu-pci` display → "Display output is not active"** black screen the
72 moment Windows boots. Use **`virtio-ramfb-gl`** (no Windows driver needed).
73- A **new scripted disk defaults to VirtIO**, which Windows Setup can't see. Use
74 **NVMe** for the system disk.
75
76## Gotchas that waste hours
77
78- **Automation (TCC) permission**: the first AppleScript/utmctl call from a new
79 parent app (Terminal, an agent harness) triggers a macOS consent prompt, or
80 fails with `-1743` if previously denied. Grant under System Settings →
81 Privacy & Security → Automation → *calling app* → UTM. The grant is per
82 calling binary; there's no supported headless pre-seed - trigger it once
83 interactively.
84- **Config is cached**: UTM reads each VM's `config.plist` once at app launch.
85 Editing the plist on disk while UTM runs does nothing. Use AppleScript
86 `update configuration` (VM must be stopped), or quit UTM → edit → relaunch.
87 Some fields (e.g. drive `ImageType` CD-vs-Disk) aren't in the AppleScript
88 dictionary at all → plist edit is the only way (windows.md).
89- **Diagnosing boot/firmware issues**: set `QEMU.DebugLog` true in config.plist
90 to dump the exact QEMU launch line (how drives are presented, display device)
91 to `Data/debug.log`. Far faster than guessing - see windows.md.
92- **Headless = no display device**: there's no headless flag. Remove the
93 `displays` entry from the VM config (or omit it at creation) and use SSH or a
94 serial port; `--hide` only hides the UTM window.
95- **Clones share a MAC** → same DHCP lease/IP. After `clone`, blank the MAC via
96 AppleScript so UTM regenerates it (snippet in applescript.md).
97- **`delete` has no confirmation** and erases the VM's disks. Confirm with the
98 user before deleting, and prefer `--disposable` starts (changes discarded on
99 shutdown) for throwaway runs.
100- **`ping` never works from guests** (libslirp limitation). Don't gate
101 "is networking up" on ping; use a TCP check (`curl`, `Test-NetConnection`).
102- **Generated AppleScript must be plain ASCII** - the `¬` line-continuation
103 char breaks under some locales. Use single-line records or variables.
104
105## Verifying an automation worked
106
107`utmctl status` reflects the VM process, not guest readiness. After `start`,
108poll for the thing you actually need: the SSH port accepting connections
109(`nc -z localhost 2222`), the agent responding (`utmctl exec ... --cmd true`),
110or `ip-address` returning a value. Boot can take 30-90s; poll with a timeout
111rather than sleeping a fixed amount.
112
113<!-- evals/ holds this skill's test prompts (maintainer-facing); intentionally unrouted. -->