| name | utmapp |
| description | Operates the UTM virtual machine app on macOS — lists, creates, starts, stops, suspends, clones, deletes, imports and exports VMs; runs commands inside a guest, transfers files, queries guest IPs, sends keyboard or mouse input, forwards USB devices, and inspects or updates VM configuration. Use when the user mentions UTM, utmctl, .utm bundles, "the UTM app", QEMU on a Mac, or Apple Virtualization.framework via UTM, or wants to automate macOS-hosted VMs from the shell or AppleScript. Covers both the QEMU backend (cross-architecture emulation) and the Apple Virtualization backend (native macOS/Linux on Apple Silicon). |
| license | Apache-2.0 |
| compatibility | macOS with UTM 4.x installed. Automation goes through AppleScript, so a logged-in Aqua session is required — utmctl and `osascript` do not work over plain SSH or before login. The QEMU guest agent must be installed in the guest for `exec`, `file`, and `ip-address` operations. |
| metadata | {"upstream":"https://github.com/utmapp/UTM","utm-website":"https://mac.getutm.app","vm-gallery":"https://mac.getutm.app/gallery/"} |
Using the UTM virtualization app
UTM is a macOS/iOS GUI for running virtual machines. On macOS it has two backends:
- QEMU — full emulation of 30+ architectures (x86_64, ARM64, RISC-V, PPC, …) plus HVF acceleration when host and guest architectures match. Required for Windows, BSDs, classic OSes, and anything cross-architecture. Supports USB pass-through, snapshots, port forwarding, custom QEMU args, and full automation (input, exec, files, IP).
- Apple Virtualization (
Virtualization.framework) — native, very fast, but limited to macOS guests on Apple Silicon and modern Linux guests. No USB pass-through, no scripted input, no guest-agent file/exec.
A VM is stored as a .utm bundle (a directory). User VMs live in ~/Library/Containers/com.utmapp.UTM/Data/Documents/. Backups are just file copies of the bundle.
There are three ways to drive UTM from a script:
utmctl — bundled CLI at /Applications/UTM.app/Contents/MacOS/utmctl (also linked as utmctl in some installs). Best for shell automation. See references/utmctl.md.
- AppleScript / JXA — full scripting dictionary in
UTM.sdef. Needed for input automation, configuration edits, and creating VMs. See references/applescript.md.
- Bundle / config edits — when UTM is closed you can read or rewrite
config.plist inside a .utm bundle directly. See references/configuration.md.
For end-user workflows (installing Linux, Windows, macOS guests; networking; file sharing) see references/workflows.md. For known gotchas and performance tuning see references/troubleshooting.md.
Gotchas — read these before automating
- No SSH / no headless. UTM scripting goes through the AppleScript bridge.
utmctl and osascript only work inside a logged-in graphical session. From SSH you will get permission errors. Workarounds: run a launchd agent, use caffeinate, or use Screen Sharing first.
- VM identifier is a name OR a UUID. Pass either; UTM resolves both. Names with spaces must be quoted:
utmctl start "Ubuntu 24.04".
delete has no confirmation. Always check with utmctl list first.
stop defaults to --force (sends a stop request to the QEMU/VZ backend). Use --request to ask the guest OS to power down cleanly, or --kill only as a last resort.
- Apple-backend VMs do not support
input keystroke, input mouse click, input scan code, USB connect/disconnect, or QEMU guest-agent commands (exec, file pull, file push, ip-address). Detect the backend before calling these — see the recipe below. In particular, do not call utmctl ip-address on an Apple-backend VM even speculatively — it always fails with Operation not supported by the backend. Use ARP on bridge100 or mDNS (<host>.local) instead; see Finding a guest's IP.
exec, file, and ip-address need the QEMU guest agent. Install qemu-guest-agent in Linux (apt install qemu-guest-agent && systemctl enable --now qemu-guest-agent) or virtio-win Guest Tools on Windows. Without it these commands time out or return "no agent".
- Never
utmctl clone a macOS guest. utmctl clone is a bundle deep-copy: the duplicate keeps the original's AuxiliaryStorage and HardwareModel, so two VMs end up sharing one Apple machine identity. The clone may still boot, but iCloud / Apple ID / FaceTime / activation will misbehave on one or both copies, and AuxiliaryStorage cannot be regenerated without a fresh restore. For macOS guests use AppleScript duplicate instead — it regenerates the auxiliary blob. Linux/Windows/QEMU guests are safe to utmctl clone. See the macOS-clone recipe below and references/workflows.md.
- Always name clones/duplicates with a unique identifier. Append a timestamp or date —
macOS-test-2026-05-08, ubuntu-base-clone-20260508-1530, <base>-<purpose>-<YYYYMMDD> — never just -test or -clone. UTM allows duplicate names, so two <base> Clone VMs will silently collide in scripts that resolve by name; a unique identifier also makes it obvious which copy to delete later.
OSStatus error -2700 from utmctl is overloaded — disambiguate by command and trailing line, then check utmctl status. -2700 is the generic AppleScript "event failed" code. UTM emits it for two very different conditions, distinguished by the trailing message:
Operation not available. — usually cosmetic on utmctl start against an Apple-backend VM (especially a freshly duplicated macOS guest). The scripting bridge races its own state check against data.run(), so it raises after the VM has already begun starting. The VM still transitions to started. Verify with utmctl status "<vm>" (or utmctl list) — if status is starting or started, treat the error as noise and continue. Do not retry the start or recreate the VM.
Operation not supported by the backend. — real failure. The Apple Virtualization backend genuinely cannot service the request (e.g. utmctl exec, utmctl file pull/push, utmctl ip-address, or --disposable start). No retry will help; use the documented alternative (SSH, ARP/mDNS, QEMU backend).
Treat utmctl status — not utmctl's exit code or stderr text — as the source of truth for whether the requested transition happened.
- Bridged networking + macOS Sequoia require granting UTM the "Local Network" privacy permission, otherwise the guest gets no IP.
- JIT on iOS is a separate world — see references/troubleshooting.md (UTM SE, AltStore, jailbreak workarounds). All scripting in this skill is macOS only.
Quick start: utmctl
Use utmctl for nearly all routine operations. The binary lives inside the app bundle:
sudo ln -sf /Applications/UTM.app/Contents/MacOS/utmctl /usr/local/bin/utmctl
utmctl list
utmctl start "Ubuntu"
utmctl suspend "Ubuntu" --save-state
utmctl stop "Ubuntu" --request
utmctl stop "Ubuntu"
utmctl stop "Ubuntu" --kill
utmctl status "Ubuntu"
utmctl start "Ubuntu" --disposable
utmctl start "macOS Sonoma" --recovery
utmctl clone "Ubuntu" --name "Ubuntu-test-$(date +%Y%m%d)"
utmctl delete "Ubuntu-test-20260508"
utmctl version
Guest-agent operations (QEMU backend, agent installed):
utmctl exec "Ubuntu" -- /bin/bash -c "uname -a"
utmctl exec "Ubuntu" --env LANG=C -- ls /etc
echo "hello" | utmctl file push "Ubuntu" /tmp/hello.txt
utmctl file pull "Ubuntu" /var/log/syslog > syslog.txt
utmctl ip-address "Ubuntu"
USB pass-through (QEMU backend):
utmctl usb list
utmctl usb connect "Windows" 046D:C016
utmctl usb connect "Windows" 4
utmctl usb disconnect 4
utmctl --help and utmctl <command> --help print full usage. Full reference with every flag, exit code semantics, and edge cases is in references/utmctl.md.
utmctl does not cover VM creation, configuration edits, keystroke/mouse injection, or registry edits — those require AppleScript. See the next section.
Choosing utmctl vs AppleScript
| Need | Use |
|---|
| start/stop/suspend/list/status/delete | utmctl |
| Clone a Linux/Windows/QEMU guest | utmctl clone |
| Clone a macOS guest | AppleScript duplicate (see recipe below) — utmctl clone shares the machine identity |
| exec, file pull/push, ip-address, USB connect/disconnect | utmctl |
| Send keystrokes / text / mouse clicks into the guest | AppleScript (input keystroke, input mouse click, input scan code) |
| Create a new VM from scratch | AppleScript (make new virtual machine) |
| Read or update VM configuration (RAM, CPU, drives, network, ports) | AppleScript (configuration of …, update configuration) |
| Wait for a VM to reach a state, build retry loops | shell + utmctl status polling, OR AppleScript |
| Rebind shared host directories | AppleScript (update registry — replaces ALL shares; see references/applescript.md#registry-suite) |
| Mount/unmount a removable ISO at runtime | Not exposed via scripting — requires the GUI |
If you need both kinds of operations in one script, drive everything from osascript -l JavaScript (JXA) — it is the only place where input automation, configuration, and lifecycle commands all coexist.
Backend-aware automation pattern
Many commands are silently a no-op or error on the Apple backend. Always branch:
backend=$(osascript -e 'tell application "UTM" to get backend of virtual machine named "MyVM" as text')
case "$backend" in
qemu) utmctl exec "MyVM" -- /bin/sh -c 'whoami' ;;
apple) echo "Apple backend: skipping guest-agent exec" ;;
*) echo "VM unavailable" >&2; exit 1 ;;
esac
Or in JXA (osascript -l JavaScript):
const utm = Application("UTM");
const vm = utm.virtualMachines.byName("MyVM");
if (vm.backend() === "qemu") {
}
Duplicating a macOS guest
utmctl clone is a bundle deep-copy. For macOS guests on the Apple backend that breaks Apple-services identity (see gotcha above). Use AppleScript duplicate instead — it tells UTM to regenerate the auxiliary identity blob. Always tag the new name with a timestamp / date so you can tell duplicates apart and avoid name collisions:
NEW_NAME="macOS-test-$(date +%Y%m%d-%H%M)"
osascript -e "tell application \"UTM\" to duplicate virtual machine named \"macOS-base\" with properties {configuration:{name:\"$NEW_NAME\"}}"
Or in JXA:
osascript -l JavaScript -e "
const utm = Application('UTM');
const stamp = new Date().toISOString().slice(0,10);
utm.duplicate(utm.virtualMachines.byName('macOS-base'), { withProperties: { configuration: { name: 'macOS-test-' + stamp } } });
"
Before duplicating, confirm the source VM is on the Apple backend and is fully shut down (utmctl status "macOS-base" → stopped). Even with duplicate, Apple's macOS licence only permits two macOS guests running concurrently per host. See references/applescript.md for the full duplicate signature.
Wait-until-ready recipe
utmctl start returns once the backend has launched, not when the guest is booted. It may also print a cosmetic OSStatus error -2700 / Operation not available on Apple-backend VMs (see Gotchas) — check utmctl status rather than the exit code. To wait until the guest is reachable, poll either status or — better — the guest agent:
utmctl start "Ubuntu" || true
for i in $(seq 1 60); do
if utmctl ip-address "Ubuntu" 2>/dev/null | grep -qE '^[0-9]+\.'; then
echo "Guest up after ${i}s"; break
fi
sleep 2
done
For Apple-backend VMs there is no guest agent, so utmctl ip-address is unusable — see the next section for the right approach.
Finding a guest's IP
Pick the path by backend — do not just call utmctl ip-address and hope:
- QEMU backend with
qemu-guest-agent installed → utmctl ip-address "<vm>". This is the only path that returns the IP directly. Returns IPv4 first, then IPv6, one per line.
- Apple backend (always) and QEMU backend without the guest agent →
utmctl ip-address will fail with Operation not supported by the backend (Apple) or time out with "no agent" (QEMU). Do not run it. Use ARP or mDNS instead.
Detect the backend first:
backend=$(osascript -e 'tell application "UTM" to get backend of virtual machine named "MyVM" as text')
UTM's default Shared (NAT) network on macOS lives on host interface bridge100 with subnet 192.168.64.0/24. Both Apple-backend and QEMU-backend "Shared" guests appear here; bridged-mode guests appear on the host's primary LAN instead.
Scope the ARP lookup to bridge100 so you get UTM guests only — a bare arp -a returns every neighbor on every interface:
arp -a -n -i bridge100
arp -a -n -i bridge100 \
| awk '$2 != "(192.168.64.1)" && $4 != "incomplete" && $2 ~ /^\(/ { gsub(/[()]/, "", $2); print $2 }'
If the guest advertises mDNS (most Linux distros and macOS guests do by default):
dscacheutil -q host -a name myhost.local
dns-sd -B _ssh._tcp local.
Hovering over the network icon in UTM's status bar shows the IP for the focused VM and is the simplest fallback when scripting is overkill.
Common shapes of work
- "Run command X in VM Y, return output" →
utmctl exec. See references/utmctl.md#exec.
- "Spin up a fresh VM from a template" →
utmctl clone --name, then utmctl start --disposable for ephemeral runs. macOS guests only: use AppleScript duplicate instead of utmctl clone — see the macOS-clone recipe above.
- "Type something into the login screen" → AppleScript
input keystroke / input scan code. See references/applescript.md.
- "Change VM configuration" → AppleScript
update configuration (VM must be stopped).
- "Rebind a shared host directory" → AppleScript
update registry (replaces every shared dir at once; this command does NOT cover removable-media swaps — those require the GUI).
- "Backup a VM" → stop it,
cp -R "MyVM.utm" /backup/. Nothing else is required.
- "Install Ubuntu / Windows / macOS guest" → see references/workflows.md.
Map of the references directory
| File | When to read it |
|---|
| references/utmctl.md | Authoring or debugging shell automation, mapping flags, exit codes |
| references/applescript.md | Sending input, creating VMs, editing configuration, JXA examples |
| references/configuration.md | Hand-editing .utm/config.plist while UTM is closed; understanding bundle layout |
| references/workflows.md | Walking a user through installing Linux, Windows ARM, Windows x86, macOS, or wiring up file sharing and networking |
| references/troubleshooting.md | "Why doesn't this work?" — JIT/iOS, performance, network, snapshots, GPU |
Read each on demand. Do not preload them.