
Utmapp
- 5 installs
- Updated August 3, 2026
- ljagiello/agent-skills
Helps with ai & agent building tasks.
About
utmapp is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- utmapp
- AI & Agent Building
- AI-coding skill
Utmapp by the numbers
- 5 all-time installs (skills.sh)
- Ranked #13,065 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ljagiello/agent-skills --skill utmappAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| Last updated | August 3, 2026 |
| Repository | ljagiello/agent-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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:
1. `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. 2. AppleScript / JXA — full scripting dictionary in UTM.sdef. Needed for input automation, configuration edits, and creating VMs. See references/applescript.md. 3. 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.
utmctlandosascriptonly work inside a logged-in graphical session. From SSH you will get permission errors. Workarounds: run a launchd agent, usecaffeinate, 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 listfirst. - `stop` defaults to `--force` (sends a stop request to the QEMU/VZ backend). Use
--requestto ask the guest OS to power down cleanly, or--killonly 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 withOperation not supported by the backend. Use ARP onbridge100or mDNS (<host>.local) instead; see Finding a guest's IP. - `exec`, `file`, and `ip-address` need the QEMU guest agent. Install
qemu-guest-agentin Linux (apt install qemu-guest-agent && systemctl enable --now qemu-guest-agent) orvirtio-winGuest Tools on Windows. Without it these commands time out or return "no agent". - Never `utmctl clone` a macOS guest.
utmctl cloneis a bundle deep-copy: the duplicate keeps the original'sAuxiliaryStorageandHardwareModel, 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, andAuxiliaryStoragecannot be regenerated without a fresh restore. For macOS guests use AppleScriptduplicateinstead — it regenerates the auxiliary blob. Linux/Windows/QEMU guests are safe toutmctl 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-testor-clone. UTM allows duplicate names, so two<base> CloneVMs 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 onutmctl startagainst an Apple-backend VM (especially a freshly duplicated macOS guest). The scripting bridge races its own state check againstdata.run(), so it raises after the VM has already begun starting. The VM still transitions tostarted. Verify withutmctl status "<vm>"(orutmctl list) — if status isstartingorstarted, 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--disposablestart). 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:
# Make it accessible (one-time; needs sudo because /usr/local/bin is root-owned)
sudo ln -sf /Applications/UTM.app/Contents/MacOS/utmctl /usr/local/bin/utmctl
# List all VMs (UUID, status, name)
utmctl list
# Start / suspend / stop
utmctl start "Ubuntu"
utmctl suspend "Ubuntu" --save-state
utmctl stop "Ubuntu" --request # ask guest to power off
utmctl stop "Ubuntu" # default = force stop backend
utmctl stop "Ubuntu" --kill # last resort
# Status of one VM
utmctl status "Ubuntu" # → stopped | starting | started | paused | …
# Disposable / recovery boot
utmctl start "Ubuntu" --disposable # discard all changes on stop
utmctl start "macOS Sonoma" --recovery
# Clone, delete, version
utmctl clone "Ubuntu" --name "Ubuntu-test-$(date +%Y%m%d)" # Linux/Windows/QEMU only; always tag with a date/ID
utmctl delete "Ubuntu-test-20260508" # NO confirmation
utmctl versionGuest-agent operations (QEMU backend, agent installed):
# Run a command, capture stdout/stderr/exit code
utmctl exec "Ubuntu" -- /bin/bash -c "uname -a"
utmctl exec "Ubuntu" --env LANG=C -- ls /etc
# Push host stdin to guest file, pull guest file to host stdout
echo "hello" | utmctl file push "Ubuntu" /tmp/hello.txt
utmctl file pull "Ubuntu" /var/log/syslog > syslog.txt
# Get guest IPs (IPv4 first, then IPv6)
utmctl ip-address "Ubuntu"USB pass-through (QEMU backend):
utmctl usb list # discover devices
utmctl usb connect "Windows" 046D:C016 # by VID:PID (hex)
utmctl usb connect "Windows" 4 # by location id
utmctl usb disconnect 4utmctl --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 ;;
esacOr in JXA (osascript -l JavaScript):
const utm = Application("UTM");
const vm = utm.virtualMachines.byName("MyVM");
if (vm.backend() === "qemu") {
// safe to call input/exec/USB
}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 # ignore cosmetic -2700; verify via status below
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
doneFor 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-addresswill fail withOperation 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:
# All running UTM Shared-network guests, by IP and MAC
arp -a -n -i bridge100
# Pick the only guest IP (skip the bridge's own .1 gateway and incomplete entries)
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):
# Resolve a known hostname
dscacheutil -q host -a name myhost.local
# Browse all SSH-advertising guests on the local link
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, thenutmctl start --disposablefor ephemeral runs. macOS guests only: use AppleScriptduplicateinstead ofutmctl 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.
UTM AppleScript / JXA reference
UTM ships a complete AppleScript dictionary (UTM.sdef inside the app bundle). Open it in Script Editor (File → Open Dictionary → UTM) for the live, browsable version. This file documents the surface relevant to automation.
Everything below works with both classic AppleScript and JXA (osascript -l JavaScript). JXA is recommended when the surrounding code is also JavaScript or when manipulating records (configuration dictionaries) is easier with object literals.
Contents
- Suites overview
- Application object
- Lifecycle commands
- Creating, importing, exporting, cloning, deleting
- Configuration suite
- Guest agent suite
- Input automation
- USB suite
- Registry suite
- Serial ports
- JXA recipe collection
Suites overview
| Suite | Purpose | Backend support |
|---|---|---|
| UTM Suite | Core lifecycle: start/suspend/stop/delete/duplicate/import/export | both |
| UTM Guest Suite | execute, query ip, open file, read, write, pull, push, close, get result | QEMU only (requires guest agent) |
| UTM Configuration Suite | Read configuration of …, update configuration | both, but record schemas differ |
| UTM USB Devices Suite | List + connect / disconnect USB devices | QEMU only |
| UTM Registry Suite | update registry (rebind shared dirs / drive bookmarks) | both |
| UTM Input Automation Suite | input scan code, input keystroke, input mouse click | QEMU only |
The suite's access group is com.utmapp.UTM.vm-access — the calling process must be granted UTM scripting access (the user gets a TCC prompt the first time).
Application object
tell application "UTM"
UTM version -- text, read-only
auto terminate -- boolean, read/write
virtual machines -- list of "virtual machine" specifiers
usb devices -- list of "usb device" specifiers
end tellReference a VM by name or by index:
tell application "UTM" to set vm to virtual machine named "Ubuntu"
tell application "UTM" to set vm to first virtual machine whose id is "3f1b2c0a-…-…"Lifecycle commands
start <vm> [saving <bool>] [recovery <bool>]
suspend <vm> [saving <bool>]
stop <vm> [by force | kill | request]
delete <vm> -- no confirmation
duplicate <vm> [with properties {configuration:{name:"new"}}]
import new virtual machine from file <hfs-path>
export <vm> to file <hfs-path>saving defaults to true for start and false for suspend. recovery defaults to false. by defaults to force. The status property reflects the result and can be polled.
tell application "UTM"
set vm to virtual machine named "Ubuntu"
start vm -- cold start or resume
repeat while (status of vm) is not started
delay 1
end repeat
stop vm by request
end tellCreating, importing, exporting, cloning, deleting
tell application "UTM"
set newVM to make new virtual machine with properties {¬
backend: qemu, ¬
configuration: {name:"Test", architecture:"aarch64", memory:2048}}
end tellbackend is one of qemu, apple, or unavailable. For QEMU you must specify at least name and architecture in the configuration record; for Apple you specify name. The schemas of the configuration record are below.
import accepts an HFS file path to a .utm bundle:
tell application "UTM" to import new virtual machine from file "/Users/me/Downloads/Ubuntu.utm"export writes a .utm bundle copy:
tell application "UTM" to export virtual machine named "Ubuntu" to file "/Users/me/Backups/Ubuntu.utm"duplicate returns a new VM specifier and (optionally) renames it:
tell application "UTM" to duplicate virtual machine named "Ubuntu" ¬
with properties {configuration:{name:"Ubuntu Clone"}}Configuration suite
configuration of <vm> -- record (read-only); shape depends on backend
update configuration <vm> with <record> -- VM must be stoppedupdate configuration cannot change the backend.
qemu configuration record
Top-level fields (per UTM.sdef):
name,icon,notes— textarchitecture— text (e.g.x86_64,aarch64,ppc64,riscv64)machine— QEMU machine type (e.g.q35,virt)memory— integer MiBcpu cores— integer (0 = default)hypervisor— boolean (HVF when host arch matches)uefi— booleandirectory share mode—none|WebDAV|VirtFSdrives— list ofqemu drive configurationrecords (id,interface,host size,guest size,raw,source,removable)network interfaces— list ofqemu network configurationrecords (hardware,mode∈emulated/shared/host/bridged,address(MAC),host interfacefor bridged,port forwards)serial ports— list ofqemu serial configurationrecords (hardware,interface,port)displays— list ofqemu display configurationrecords (hardware,dynamic resolution,native resolution,upscaling filter,downscaling filter)qemu additional arguments— list ofqemu argumentrecords (raw QEMU CLI args; each hasargument stringand optionalfile urls)
Port-forward sub-record fields: protocol (TCP/UDP), host address, host port, guest address, guest port.
Important AppleScript naming quirk: the additional-arguments property is qemu additional arguments, not additional arguments. Reading or writing the wrong name silently fails.
apple configuration record
name,icon,notesmemory,cpu coresdrives— list ofapple drive configurationrecords (id,removable,host size,guest size,source(file))network interfaces— list ofapple network configurationrecords (index,mode∈shared|bridged,address,host interface)serial ports— list ofapple serial configurationrecords (index,interface— only PTTY is supported on the Apple backend)displays— list ofapple display configurationrecords (id,dynamic resolution)directory shares— list ofapple directory share configurationrecords — onlyindexandread onlyare exposed via AppleScript. There is no `path` property on this record; the host directory is bound through the registry, not via the configuration record. To rebind a share to a different host folder useupdate registry(see Registry suite).
The QEMU and Apple sub-records all carry an index property used to identify which existing entry an update configuration call should replace. Omit index to create a new entry.
Example: edit RAM and CPU on a stopped QEMU VM
// JXA: osascript -l JavaScript edit-ram.js "Ubuntu" 4096 4
ObjC.import('stdlib');
const [name, memory, cores] = $.NSProcessInfo.processInfo.arguments.js
.slice(4).map(a => a.js);
const utm = Application("UTM");
const vm = utm.virtualMachines.byName(name);
if (vm.status() !== "stopped") { console.log("VM must be stopped"); $.exit(1); }
const cfg = vm.configuration();
cfg.memory = parseInt(memory, 10);
cfg["cpu cores"] = parseInt(cores, 10);
utm.updateConfiguration(vm, { with: cfg });update configuration accepts a partial record — fields you omit are left unchanged, but in practice it is safest to read the current configuration, mutate, and write back.
Guest agent suite
Requires the QEMU guest agent inside the guest. Apple-backend VMs do not respond to these commands.
query ip
tell application "UTM" to query ip for virtual machine named "Ubuntu"
-- returns a list of text, IPv4 addresses firstexecute / get result
tell application "UTM"
set vm to virtual machine named "Ubuntu"
set proc to execute vm at "/bin/sh" with arguments {"-c", "uname -a"} ¬
with environment {"LANG=C"} ¬
output capturing true
repeat
set r to get result of proc
if exited of r then
return (output text of r)
end if
delay 0.5
end repeat
end tellexecute parameters:
| name | type | notes |
|---|---|---|
at | text | absolute path or PATH-resolvable executable |
with arguments | list of text | optional |
with environment | list of NAME=VALUE text | optional |
using input | text | optional stdin |
base64 encoding | boolean | if true, using input is base64 of binary stdin |
output capturing | boolean | required for output text/error text to be filled |
get result returns an execute result record:
exited— boolexit code— intsignal code— int (0 if normal exit)output text,error text— captured stdout/stderr (ifoutput capturing)output data,error data— base64 of stdout/stderr (use for binary)
Guest file I/O
utmctl file push/pull cover the common cases. AppleScript exposes the underlying primitives if you need offsets, partial reads, or persistent handles:
tell application "UTM"
set f to open file for virtual machine named "Ubuntu" at "/var/log/syslog" for reading
set chunk to read f for length 8192 base64 encoding false closing false
close f
end tellCommands: open file, read, write, pull, push, close. read/write accept at offset N plus from start position | current position | end position. The read for length limit is 48 MB.
open file for modes: reading (must exist), writing (truncate or create), appending (create if missing). updating: true allows read+write.
Input automation
QEMU only. The Apple backend silently ignores these (they error with "not supported" on the underlying VM).
input scan code
Send raw PC-AT scan codes (8-bit, with optional 0xE0xx extended). UTM toggles the high 0x80 bit for key release internally if needed.
tell application "UTM" to input scan code virtual machine named "Ubuntu" codes {28}
-- 28 (0x1C) = Enterinput keystroke
ASCII string + optional modifiers. Modifiers are held for the entire string.
tell application "UTM" to input keystroke virtual machine named "Ubuntu" text "ls -la" with modifiers {control}Modifier keys: caps lock, shift, control, option, command, escape.
For a "press Ctrl-Alt-Del" you cannot use input keystroke (it sends ASCII text); use scan codes:
tell application "UTM" to input scan code virtual machine named "Win" codes ¬
{29, 56, 57427, 57427+128, 56+128, 29+128} -- C-A-Del down, Del up, Alt up, Ctrl upinput mouse click
Absolute coordinates inside the SPICE display.
tell application "UTM" to input mouse click virtual machine named "Ubuntu" ¬
at {640, 480} to 1 with mouse button leftto N selects monitor index (1-based); button is left, right, or middle.
USB suite
QEMU only. The application has a usb devices collection of all host devices visible to UTM.
tell application "UTM"
set d to first usb device whose name contains "YubiKey"
connect d to virtual machine named "Win"
-- … later …
disconnect d
end tellUSB device properties (read-only): id (location), name, manufacturer name, product name, vendor id, product id. vendor id/product id are integers — convert to hex if you compare against VID:PID strings.
Registry suite
registry of <vm> -- read: returns a list of file specifiers
update registry <vm> with <list-of-files> -- write: replaces ALL shared-directory entriesThe scripting surface for the registry is narrow. Per UTMScriptingRegistryEntryImpl.swift, it exposes only the VM's shared directories — serializeRegistry() returns registry.sharedDirectories.compactMap { $0.url }, and update registry calls removeAllSharedDirectories() and re-adds the supplied URLs as bookmarks.
Implications:
- It cannot swap a removable-media ISO at runtime via this command.
- It cannot edit external drive bookmarks.
update registryis all-or-nothing for shared dirs — pass the complete list, not a delta. To remove all shares, pass{}.
tell application "UTM"
set vm to virtual machine named "Ubuntu"
-- read existing shares
set shares to registry of vm
-- swap one and rebind everything in one shot
set newShares to {file "/Users/me/projects", file "/Users/me/data"}
update registry vm with newShares
end tellFor other "registry-like" operations (changing the source file behind a removable drive, etc.) there is no scripting hook — they require the GUI.
Serial ports
serial ports of <vm> -- list of "serial port" recordsEach port has id (index), interface (ptty | tcp | unavailable), address, and port.
tell application "UTM"
repeat with p in serial ports of virtual machine named "Ubuntu"
log (interface of p as text) & " " & (address of p) & ":" & (port of p as text)
end repeat
end tellFor ptty, address is the host pseudo-tty path — open with screen /dev/ttysNNN or cu -l. For tcp, connect with nc <address> <port>.
JXA recipe collection
JXA is reachable from the shell as osascript -l JavaScript -e '<code>' or from a .scpt/.js file. Boilerplate:
const utm = Application("UTM");
utm.includeStandardAdditions = true;List VMs as JSON
const utm = Application("UTM");
JSON.stringify(utm.virtualMachines().map(vm => ({
id: vm.id(),
name: vm.name(),
backend: vm.backend(),
status: vm.status(),
})), null, 2);Run with: osascript -l JavaScript list.js.
Start, wait for IP, run a command
const utm = Application("UTM");
const vm = utm.virtualMachines.byName("Ubuntu");
if (vm.status() !== "started") utm.start(vm);
let ip;
for (let i = 0; i < 60 && !ip; i++) {
try {
const ips = utm.queryIp(vm); // throws if agent not ready
ip = ips.find(a => a.includes("."));
} catch (_) { delay(2); }
}
if (!ip) throw new Error("guest never came up");
const proc = utm.execute(vm, {
at: "/bin/sh",
withArguments: ["-c", "uname -srm && hostname"],
outputCapturing: true,
});
let r;
do { delay(0.3); r = utm.getResult(proc); } while (!r.exited);
console.log(`exit=${r.exitCode}\n${r.outputText}`);JXA snake-cases the AppleScript parameter names: with arguments → withArguments, output capturing → outputCapturing, with modifiers → withModifiers, mouse button → mouseButton.
Type into the login screen
const utm = Application("UTM");
const vm = utm.virtualMachines.byName("Ubuntu");
utm.inputKeystroke(vm, { text: "myuser" });
utm.inputScanCode(vm, { codes: [28] }); // Enter
utm.inputKeystroke(vm, { text: "secretpw" });
utm.inputScanCode(vm, { codes: [28] });Create a stripped-down ARM Linux VM
const utm = Application("UTM");
utm.make({
new: "virtual machine",
withProperties: {
backend: "qemu",
configuration: {
name: "tiny-arm",
architecture: "aarch64",
machine: "virt",
memory: 1024,
"cpu cores": 2,
uefi: true,
hypervisor: true,
},
},
});You then need to attach a disk and an installer ISO, which is much easier from the GUI wizard — see workflows.md.
Take a snapshot via guest-side fsfreeze (no UTM-native snapshot CLI)
UTM does not expose a "snapshot" verb in the dictionary. To create application-consistent backups, freeze the guest filesystem first, then cp -R the bundle:
osascript -l JavaScript <<'JS'
const utm = Application("UTM");
const vm = utm.virtualMachines.byName("Ubuntu");
utm.execute(vm, { at: "/usr/sbin/fsfreeze", withArguments: ["-f", "/"] });
JS
cp -Rc "$HOME/Library/Containers/com.utmapp.UTM/Data/Documents/Ubuntu.utm" /Volumes/backup/
osascript -l JavaScript -e '
const utm = Application("UTM");
utm.execute(utm.virtualMachines.byName("Ubuntu"),
{ at: "/usr/sbin/fsfreeze", withArguments: ["-u", "/"] });'(Use APFS cp -c to clone reflinks — the bundle can be tens of GB.)
UTM bundle and configuration reference
Read this when the user wants to hand-edit a .utm bundle outside UTM, scripts a custom QEMU argument, or needs to understand the on-disk schema for backup, migration, or templating.
Contents
- Bundle layout
- config.plist top level
- QEMU configuration schema
- Apple configuration schema
- Modifying a bundle safely
- Custom QEMU arguments
- Migrating a bundle between hosts
Bundle layout
MyVM.utm/ ← directory, opaque in Finder ("show package contents")
├── config.plist ← XML or binary plist; the canonical configuration
├── Data/
│ ├── <uuid>.qcow2 ← virtual disk(s); also seen: .img, .raw
│ ├── efi_vars.fd ← UEFI NVRAM (QEMU UEFI guests)
│ ├── AuxiliaryStorage ← Apple-backend NVRAM blob (binary)
│ └── HardwareModel ← Apple-backend hardware-model blob (binary)
├── Images/ ← removable-media files (older bundles)
├── view.plist ← per-host display state (window size, scaling); safe to delete
└── screenshot.png ← last frame, used as VM tile in UTM's main viewA QEMU VM only needs config.plist plus its disk images. Apple VMs additionally need AuxiliaryStorage and HardwareModel (those tie the VM to a specific Apple machine identity — they cannot be regenerated for a macOS guest without a fresh restore).
config.plist top level
config.plist is a property list. Use plutil to convert formats:
plutil -convert xml1 -o - config.plist | less # human-readable
plutil -convert binary1 config.plist # back to binary
plutil -p config.plist # pretty-printThe top-level keys you will see in current (ConfigurationVersion = 4) bundles, taken directly from the CodingKeys enums in Configuration/UTMQemuConfiguration.swift and Configuration/UTMAppleConfiguration.swift:
Backend string "QEMU" or "Apple"
ConfigurationVersion int 4 in UTM 4.x
Information dict Name, Icon, IconCustom, Notes, UUID
System dict Architecture, Target, MemorySize, CPUCount, …
Display array one dict per virtual monitor
Drive array one dict per disk / CD / BIOS / kernel
Network array one dict per NIC
Serial array one dict per serial device
Sound array one dict per audio device
Sharing dict QEMU only: DirectoryShareMode, DirectoryShareReadOnly, ClipboardSharing
Input dict QEMU only: USB bus settings
QEMU dict QEMU only: UEFIBoot, Hypervisor, AdditionalArguments, DebugLog, …
Virtualization dict Apple only: pointer device, audio, balloon, entropy, keyboard, rosettaExternal-file references (drive sources outside the bundle, shared-directory paths) are stored as security-scoped bookmarks in UTM's UserDefaults under the Registry key — i.e. ~/Library/Containers/com.utmapp.UTM/Data/Library/Preferences/com.utmapp.UTM.plist, not inline in config.plist. Hand-edits to config.plist cannot create new external references — use UTM's GUI or AppleScript update registry for that.
QEMU configuration schema
Key fields (cross-referenced with Configuration/UTMQemuConfigurationSystem.swift etc. in the source):
System
Architecture(x86_64,aarch64,arm,i386,riscv64,ppc64le,mips64,s390x, …)Target(QEMU machine type, e.g.q35,virt,pc-i440fx-8.0)MemorySize(MiB)CPUCount(0 = match host)ForceMulticore(bool — keep multiple cores even when the guest expects single-core)CPU(e.g.default,host,cortex-a72)CPUFlagsAdd,CPUFlagsRemove(arrays of strings)JITCacheSize(MiB; 0 = default; iOS only)
QEMU sub-dict
On disk these keys do not carry the Has prefix that the Swift property names use — the prefix is stripped in CodingKeys. Confirmed against UTMQemuConfigurationQEMU.swift:
UEFIBoot(bool)Hypervisor(bool — HVF when host arch matches)RTCLocalTime(bool)RNGDevice(bool)BalloonDevice(bool)TPMDevice(bool — Windows 11 needs this)TSO(bool — Apple Silicon nested virtualization tweak)MachinePropertyOverride(string — extra-machine ...properties)AdditionalArguments(array ofqemu argumentdicts) — see Custom QEMU argumentsDebugLog(bool)
Drive
CodingKeys per UTMQemuConfigurationDrive.swift:
{
"Identifier": "<uuid>",
"ImageType": "Disk" | "CD" | "BIOS" | "LinuxKernel" | "LinuxInitrd" | "LinuxDTB" | "None",
"Interface": "IDE" | "SCSI" | "SD" | "MTD" | "Floppy" | "PFlash" | "VirtIO" | "NVMe" | "USB" | "None",
"InterfaceVersion": 1,
"ImageName": "<file>.qcow2", // present only for bundle-internal drives
"ReadOnly": false
}Notes:
- The plist key is
Interface, notInterfaceType. Values are capitalized exactly as shown —IDEnotide,VirtIOnotvirtio. - `SATA` is not a valid value in this enum. UTM exposes IDE/SCSI/VirtIO/NVMe/USB for typical disks.
- Drive size is not stored on disk — it is computed from the qcow2/raw file's actual size at load time.
- For drives that point at a host file outside the bundle (external ISO, etc.) the
ImageNamekey is omitted; the file path is reconstructed from a bookmark in UTM's registry, not fromconfig.plist.
Network
CodingKeys per UTMQemuConfigurationNetwork.swift:
{
"Mode": "Emulated" | "Shared" | "Host" | "Bridged",
"Hardware": "virtio-net-pci" | "rtl8139" | "e1000" | …,
"MacAddress": "52:54:00:…",
"IsolateFromHost": false,
"BridgeInterface": "en0", // Bridged mode
"VlanGuestAddress": "10.0.2.0/24", // Emulated mode (optional)
"VlanHostAddress": "10.0.2.2",
"VlanDhcpStartAddress": "10.0.2.15",
"VlanDhcpEndAddress": "10.0.2.30",
"VlanDhcpDomain": "internal",
"VlanDnsServerAddress": "10.0.2.3",
"VlanDnsSearchDomain": "internal",
"HostNetUuid": "<uuid>", // Host mode (links VMs into one virtual network)
"PortForward": [
{ "Protocol": "TCP",
"GuestAddress": "",
"GuestPort": 22,
"HostAddress": "127.0.0.1",
"HostPort": 2222 }
]
}Protocol values are uppercase "TCP" and "UDP". Mode values are capitalized "Emulated"/"Shared"/"Host"/"Bridged".
Display
CodingKeys per UTMQemuConfigurationDisplay.swift:
{
"Hardware": "virtio-gpu-pci" | "virtio-gpu-gl-pci" | "qxl-vga" | "ramfb" | "vmware-svga" | …,
"DynamicResolution": true,
"NativeResolution": false,
"UpscalingFilter": "Linear" | "Nearest",
"DownscalingFilter": "Linear" | "Nearest",
"VgaRamMib": 16 // VGA RAM in MiB; on disk this key is VgaRamMib (not VgaRamSize)
}Sharing
CodingKeys per UTMQemuConfigurationSharing.swift:
{
"DirectoryShareMode": "None" | "WebDAV" | "VirtFS",
"DirectoryShareReadOnly": false,
"ClipboardSharing": true
}There is no DirectoryShareBookmark key — the bookmark to the host directory is stored in the registry, not the bundle.
Apple configuration schema
The Apple backend records are simpler because Virtualization.framework hides QEMU-style detail. CodingKeys per UTMAppleConfiguration*.swift:
`System.Boot` (UTMAppleConfigurationBoot.swift):
OperatingSystem—Linux|macOSUEFIBoot(bool — required for some Linux distros that boot via EFI rather than direct kernel)LinuxKernelPath(relative path insideData/; the URL is reconstructed at load)LinuxCommandLine(kernel cmdline)LinuxInitialRamdiskPath(initrd, same path convention)EfiVariableStoragePath(NVRAM blob path)
The macOS recovery IPSW URL is not persisted (it's only used during install).
Drives (UTMAppleConfigurationDrive.swift, CodingKeys lines 39-44): Identifier, ImageName (bundle-internal drives), Bookmark (legacy field, kept for migration of older bundles), ReadOnly, Nvme (boolean — true exposes as NVMe, false as VirtIO Block). Drive size is not persisted as a plist key — it is read from the image file itself.
Network (UTMAppleConfigurationNetwork.swift, CodingKeys lines 46-48): Mode (Shared | Bridged), MacAddress, BridgeInterface. The Apple backend has no Hardware key — Virtualization.framework picks the device class itself.
SharedDirectory (UTMAppleConfigurationSharedDirectory.swift): Bookmark (security-scoped), ReadOnly. The host directory path itself is not stored as a string — it must be resolved through the bookmark.
`Virtualization` sub-dict (UTMAppleConfigurationVirtualization.swift, CodingKeys lines 67-74):
Audio,Balloon,Entropy(booleans)Keyboard— enum string"Disabled"|"Generic"|"Mac"(capitalized exactly as shown)Pointer— enum string"Disabled"|"Mouse"|"Trackpad"(capitalized)Trackpad(legacy boolean kept for migration of pre-Pointer-enum configs)Rosetta(bool — macOS 13+ Apple Silicon only)ClipboardSharing(bool — host↔guest clipboard)
Rosetta mounts the host's x86_64 translator into a Linux guest under the virtiofs tag rosetta (not share). The guest mounts it with mount -t virtiofs rosetta /mnt/rosetta and registers it as a binfmt handler — see UTMAppleConfigurationVirtualization.swift for the VZ wiring.
Modifying a bundle safely
Rules:
1. UTM must not be running while you edit config.plist. UTM rewrites the file on quit. 2. Always cp -Rc the bundle first, edit the copy, and re-import on success. 3. Round-trip through XML: plutil -convert xml1 -o config.xml config.plist, edit, then plutil -convert binary1 -o config.plist config.xml (UTM accepts either, but keeps it as XML by default). 4. Do not change Backend after a VM is created — the device arrays use different schemas and UTM will fail to load. 5. Do not change Information.uuid unless you also remove the bundle from UTM and re-import (UTM tracks VMs by UUID inside the registry).
A safer alternative for most fields: read the configuration via AppleScript, mutate, write back via update configuration. See applescript.md.
Custom QEMU arguments
UTM exposes a free-form QEMU Arguments tab. Each entry is appended verbatim to the QEMU command line, after UTM's generated arguments. Useful examples:
-cpu host— pass through full host CPU features (Intel host, Linux x86_64 guest).-machine smm=off,vmport=off— quiet certain Windows boot warnings.-bios path/to/edk2.fd— replace UTM's bundled UEFI firmware.-monitor unix:/tmp/utm-mon,server,nowait— expose a QMP/HMP socket so external tools can drive snapshots, hot-plug, or take screenshots.-device usb-host,vendorid=0x046d,productid=0xc016— pin a USB device by vendor/product (alternative to UTM's runtime connect/disconnect).
Order matters when arguments shadow each other. UTM does not validate the strings — a typo will cause QEMU to fail to launch with a console error in Settings → QEMU → Debug Log.
In the on-disk format these live under QEMU.AdditionalArguments as an array of plain strings (per QEMUArgument.swift — init(from:) decodes a String directly, not a keyed container). Example: <array><string>-cpu host</string><string>-machine smm=off</string></array>.
Migrating a bundle between hosts
QEMU bundles are portable across Macs as-is — copy the bundle, double-click. Some pitfalls:
- External drive bookmarks. If a drive's
Bookmarkpoints outside the bundle (e.g. an ISO on the original Mac's Desktop), it will fail to resolve on the new host. Either move the file to the same path or re-attach the drive in UTM. - Bridged interface.
Network[].BridgeInterfaceis host-specific (en0may not exist on the other Mac). Edit before first run or switch to Shared. - Shared directory bookmarks. Same problem as drives — re-add the share on the new host.
- macOS guests are tied to the Apple machine identity stored in
Data/HardwareModel. They generally restore on another Apple Silicon Mac but Apple ID services may flag the move; consider this when migrating.
For a clean cross-host export, use UTM → File → Export Selected (utmctl has no export command — osascript -e 'tell app "UTM" to export …' does the job from CLI). Export resolves bookmarks into bundle-local copies where possible.
UTM troubleshooting and edge cases
Read this when something does not work. Topics are grouped; each entry leads with the symptom.
Contents
- Scripting / utmctl errors
- Guest agent (exec/file/ip-address) failures
- Networking issues
- Performance problems
- Display, GPU, and resolution
- USB pass-through
- Snapshots and save state
- iOS / iPadOS specifics
- Build-from-source pitfalls
Scripting / utmctl errors
Symptom: `Application can't be found.` or `Not authorized to send Apple events to UTM.` The caller does not have automation permission. Open System Settings → Privacy & Security → Automation, find your terminal / script runner, and tick UTM. There is no programmatic way to grant this.
Symptom: Commands hang or return generic errors when run via SSH. AppleScript needs a logged-in graphical session. Solutions in order of preference:
1. Run in Terminal/Tmux inside Screen Sharing. 2. Use a launchd LaunchAgent (loaded at login). 3. caffeinate -d -u keeps the session active without sleep. 4. Last resort: configure auto-login on the host.
Symptom: `utmctl: command not found`. The CLI is not on PATH. Either call it by full path:
/Applications/UTM.app/Contents/MacOS/utmctl …or symlink it: sudo ln -sf /Applications/UTM.app/Contents/MacOS/utmctl /usr/local/bin/utmctl. The App Store build is at the same path.
Symptom: `utmctl attach` says "not yet implemented". Correct — the flag exists but the implementation is incomplete. Read the serial port's address/port via AppleScript and connect with screen or nc. See applescript.md.
Symptom: `utmctl exec` returns immediately with no output. Likely capturing was off in the underlying call (utmctl sets it on by default, but if you build a custom AppleScript skip this), or the guest command exited before output flushed. Add explicit redirection: utmctl exec "$vm" -- /bin/sh -c 'cmd 2>&1'.
Symptom: `utmctl start` prints `OSStatus error -2700 / Operation not available` against an Apple-backend VM, but `utmctl status` says `started`. Cosmetic. OSStatus -2700 is the generic AppleScript "event failed" envelope; the real meaning is in the trailing message and the resulting VM state. UTMScriptingVirtualMachineImpl.start first attaches a window controller (data.run(vm:startImmediately:false)) and then re-reads vm.state — on Apple-backend VMs the state has often already left .stopped, so the bridge throws operationNotAvailable even though the start succeeded.
Do not retry the start, delete the VM, or treat the non-zero exit code as authoritative. Verify with utmctl status "<vm>"; if it returns starting or started, continue. Same pattern when scripting start via AppleScript directly.
Symptom: `utmctl exec` / `utmctl file` / `utmctl ip-address` prints `OSStatus error -2700 / Operation not supported by the backend`. Real failure — and it will keep failing. This is the other -2700 variant: the Apple Virtualization backend has no QEMU guest agent, so these commands have nothing to talk to. Pivot to SSH (exec, file) or ARP/mDNS on bridge100 (ip-address). See SKILL.md → Finding a guest's IP. The two -2700 cases are distinguished only by the trailing message line — always read it.
Guest agent (exec/file/ip-address) failures
Symptom: `query ip` returns empty list, or exec hangs. The QEMU guest agent is not running. Check inside the guest:
- Linux:
systemctl status qemu-guest-agent. Install withapt install qemu-guest-agentor distro equivalent. The agent talks over a virtio-serial port that UTM auto-creates; you do not need to add anything in the host config. - Windows: install virtio-win Guest Tools. Verify the
QEMU Guest Agentservice is running.
Symptom: agent runs but `exec` reports "operation not supported". The VM uses the Apple Virtualization backend, which has no QEMU guest agent. Use SSH instead, or convert the workflow to AppleScript-only operations.
Symptom: `file pull` corrupts binary files. You forgot --binary … there is no such flag. utmctl file pull already streams binary data through base64 internally, so this should not happen — but make sure no shell interprets the bytes (redirect to a file with > rather than piping through read/xargs).
Networking issues
Symptom: Bridged guest never gets an IP on macOS Sequoia. macOS 15 added a "Local Network" privacy permission that UTM needs to bridge. System Settings → Privacy & Security → Local Network → UTM ✓. After the first refusal you may need to remove and re-add UTM in the list.
Symptom: Port forwarding works, but only `127.0.0.1`-bound services on the guest are reachable. The QEMU SLIRP user network's port forward terminates inside the guest's NIC. If the guest service binds to 127.0.0.1 it will not see the forwarded packets — bind to 0.0.0.0 or to the guest's NIC IP.
Symptom: Two QEMU VMs in "Host" mode cannot ping each other. Both VMs need to be on the same host-only network. UTM creates one shared host-only subnet, so this should work — verify both VMs have Network.Mode = "Host" in config.plist and not Emulated.
Symptom: Apple-backend Linux guest has no IPv6 / no internet. Apple's shared mode (vmnet-shared) does NAT44 only. For IPv6 you need Bridged mode plus Local Network permission.
Symptom: Slow DNS / failed lookups on QEMU SLIRP networks. SLIRP forwards DNS to the host via the synthetic 10.0.2.3 resolver. If the host's DNS is going through a VPN that disallows split tunneling, lookups can fail. Switch to Bridged or set explicit DNS in the guest (8.8.8.8, 1.1.1.1).
Performance problems
Symptom: x86_64 Linux/Windows on Apple Silicon is unbearably slow. That's QEMU TCG translating x86 instructions on the fly. Expectations: 10–30 % of native. Mitigations:
- Use ARM64 Linux / Windows 11 ARM where possible.
- Inside Windows 11 ARM, run x86 apps under Microsoft's built-in x86 emulator (faster than QEMU TCG nested in QEMU).
- For Linux x86_64 specifically, mount Rosetta in an ARM Linux guest under the Apple backend (Settings → Sharing → "Run x86 binaries through Rosetta"). Single ARM kernel, ARM and x86_64 user-space binaries both run.
- Reduce the working set: fewer cores often runs faster than more, because TCG does not parallelize per-vCPU efficiently.
Symptom: Apple-backend macOS guest sluggish in graphical apps. No GPU acceleration is exposed by Virtualization.framework — even simple animations are CPU-rendered. There is no fix; for graphics-heavy macOS work, run on the host.
Symptom: QEMU guest pegs CPU even when idle. Common with old Linux kernels lacking PV interrupt drivers, or with the cirrus display in graphical mode. Switch the display to virtio-gpu-pci, install qemu-guest-agent (it implements idle hints), and ensure tickless is on in the guest kernel.
Symptom: HVF is supposedly enabled but performance feels emulated. HVF only kicks in when host arch == guest arch. Check Architecture in config.plist: it must match arm64/aarch64 on Apple Silicon, or x86_64 on Intel. Also verify QEMU.HasHypervisor = true.
Display, GPU, and resolution
Symptom: Resolution does not change when I resize the window. Dynamic resolution requires the SPICE guest agent (spice-vdagent) on Linux, or the SPICE Tools installer on Windows. Without it, the resolution is whatever the OS chose at boot.
Symptom: Linux guest has black screen after install. Most often the bootloader is still pointing at a virtual console only. Switch the display device to virtio-gpu-pci (or virtio-gpu-gl-pci for VirGL) in Settings → Display, or boot with console=tty0 console=ttyS0 so output also goes to the serial port and you can debug there.
Symptom: Tiny / blurry text on Retina display. Tick Settings → Display → Native Resolution for sharp 2× rendering, then bump the guest's DPI / scale factor. Without "Native Resolution", UTM hands the guest the logical (low-DPI) size.
USB pass-through
Symptom: `utmctl usb connect` says "no such device".
- Check
utmctl usb listfirst; the host may not yet see the device if it's still enumerating. - Some devices require Input Monitoring permission for UTM (mice/keyboards in particular).
- USB pass-through is QEMU only; the Apple backend never lists devices.
Symptom: Device connects, then disconnects after a few seconds. Power-management timeouts in QEMU EHCI/XHCI. Add a -device qemu-xhci (USB 3) explicitly via custom QEMU arguments and connect again — often more reliable than the default USB 2 hub UTM uses.
Symptom: Apple Silicon kernel panic when connecting USB to a Linux guest. Known interaction between certain USB 3 hubs and the macOS USB stack. Plug the device into a different port (preferably the Mac's built-in port, not a hub), or use a USB-IP server on the host instead.
Snapshots and save state
Symptom: Suspending a VM with `--save-state` fails with "device does not support live save". QEMU live state save requires every device in the VM to be migration-aware. The usual culprit is a USB host-pass-through device: disconnect first (utmctl usb disconnect …), then suspend.
Symptom: After resume, guest network is dead. DHCP leases time out across long suspends. Inside the guest, dhclient -r && dhclient (Linux) or ipconfig /release && ipconfig /renew (Windows). Apple-backend macOS guests handle this transparently.
Symptom: I want a real "snapshot tree" like VirtualBox. UTM does not expose savevm/loadvm in the GUI. Workarounds:
1. Stop the VM and cp -Rc bundle.utm bundle-state-1.utm for an APFS reflink copy. Restoration = swap the bundle back. 2. Use a custom QEMU monitor argument (-monitor unix:…) and drive savevm / loadvm from a script. 3. Maintain qcow2 backing-chain images outside UTM (advanced; UTM does not always preserve the chain).
iOS / iPadOS specifics
UTM on iOS comes in two flavors:
- UTM (full) — uses JIT for QEMU TCG. Requires either a jailbroken iOS (Palera1n, Dopamine), an enterprise certificate (rare), or a runtime JIT enabler (AltStore / SideStore + JitStreamer / StikJIT) which works only on specific iOS versions.
- UTM SE ("Slow Edition") — uses a threaded interpreter instead of JIT, no special privilege required. Available on the App Store. ~3× slower than JIT for compute-heavy workloads but fine for terminal-only use.
Common questions:
- "Can I sideload UTM with regular AltStore?" — Yes, but JIT will not work without a JIT enabler app on the same device. Without JIT, performance is uselessly slow; install UTM SE instead.
- "How do I refresh the 7-day cert?" — Open AltStore weekly while connected to the same Wi-Fi as your AltServer. SideStore can refresh fully on-device (no Mac needed) using a wireguard-based proxy.
- "Why does my VM die when iOS backgrounds?" — iOS reclaims memory aggressively. Pin UTM to the foreground or accept that long suspends will kill the VM.
There is no utmctl, no AppleScript, and no shell on iOS — automation is not supported. The skill's CLI/AppleScript content does not apply on iOS.
Build-from-source pitfalls
These come up when users try to build UTM themselves to get debug logging or experiment with the source.
- Xcode signing. The
Build.xcconfigtemplate has placeholders; copyCodeSigning.xcconfig.sampletoCodeSigning.xcconfigand fill in your team ID before opening the project. - Submodule depth.
git clone --recursiveis required; QEMU and SPICE patch trees are submodules. - Build time on M1 Air. Around 30 minutes for the first full build (mainly QEMU and SPICE-related libraries). Subsequent builds are minutes.
- Tethered launch (jailbroken iOS). The app must be re-launched via a paired Mac after every reboot; see
Documentation/TetheredLaunch.mdin the source tree.
For deeper development questions, point users at Documentation/MacDevelopment.md and Documentation/iOSDevelopment.md in the upstream repo (<https://github.com/utmapp/UTM>) rather than answering inline.
utmctl reference
utmctl is the CLI bundled with UTM.app. It is implemented in Swift on top of the AppleScript bridge, so every operation here also has an AppleScript equivalent — but the CLI is shorter for shell scripting.
Contents
- Locating and invoking utmctl
- Global behavior
- Identifiers
- version
- list
- status
- start
- suspend
- stop
- clone
- delete
- attach
- ip-address
- exec
- file pull / file push
- usb list / connect / disconnect
- Common patterns
Locating and invoking utmctl
utmctl lives inside the app bundle:
/Applications/UTM.app/Contents/MacOS/utmctlEither call it by full path or symlink it once:
sudo ln -sf /Applications/UTM.app/Contents/MacOS/utmctl /usr/local/bin/utmctlUTM does not need to be running — utmctl will launch it on demand. When invoked from the App Store build with a tightened sandbox, the first call may produce a permission prompt asking the user to allow scripting of UTM; this must be approved interactively.
utmctl is unusable from a plain SSH session because the AppleScript send fails outside an Aqua login. From a SSH session you can osascript -e 'tell application "UTM" to launch' after running caffeinate or after using screen sharing to log in.
Global behavior
utmctl [--debug] [--hide] <subcommand> ...-d,--debug— print debug output to stderr.--hide— keep UTM windows hidden after the command runs.- Exit code 0 on success, non-zero on error. Most failures print a localized
NSErrordescription to stderr. - All commands operate over the AppleScript bridge; they are synchronous from the CLI's perspective but the underlying VM action may continue (e.g.
startreturns once UTM has dispatched the start, not once the guest has booted).
Identifiers
Wherever an <identifier> is required, you can pass either:
- The VM's exact
name(case-sensitive). Quote names containing spaces. - The VM's
idUUID as printed byutmctl list.
Names are convenient for humans; UUIDs are stable across rename. Prefer UUIDs in long-lived scripts.
---
version
utmctl versionPrints UTM's version string (e.g. 4.6.5). Useful as a probe in CI to confirm UTM is installed and scriptable.
list
utmctl listPrints a header followed by one row per registered VM:
UUID Status Name
3f1b2c0a-…-… stopped Ubuntu 24.04
9e8a7d54-…-… started Windows 11 ARMStatus is one of: stopped, starting, started, pausing, paused, resuming, stopping. Parse this with awk on whitespace, or grep by UUID.
status
utmctl status <identifier>Prints one of the status enumerators above. Exits non-zero if the VM cannot be found.
start
utmctl start [-a | --attach] [--disposable] [--recovery] <identifier>--disposable— equivalent to QEMU's-snapshot: writes go to a scratch overlay and are discarded when the VM stops. Excellent for CI runs.--recovery— boot the VM into recovery mode. Required to reinstall macOS guests; ignored on most Linux configurations.-a,--attach— accepted but the post-start serial attach is unimplemented. The VM still starts normally; utmctl printsWARNING: attach command is not implemented yet!to stdout via plainprint(). Treat the flag as a no-op.
start resumes a suspended VM as well as cold-starting a stopped one.
Cosmetic `OSStatus error -2700 / Operation not available` on Apple-backend VMs. On Apple Virtualization VMs (especially freshly duplicated macOS guests) utmctl start frequently exits non-zero with:
Error from event: The operation couldn't be completed. (OSStatus error -2700.)
Operation not available.The VM still transitions to started. The cause is in UTMScriptingVirtualMachineImpl.start (UTM source Scripting/UTMScriptingVirtualMachineImpl.swift:112): the scripting bridge calls data.run(vm:startImmediately:false) to attach a window controller, then re-reads vm.state and throws operationNotAvailable whenever the state has already left .stopped / .paused. The race is reliable enough to look like a hard failure but the underlying data.run already kicked off the start.
Handle it by checking the VM state, not the utmctl exit code:
utmctl start "macOS-test" || true
case "$(utmctl status "macOS-test")" in
starting|started) ;; # actually fine, continue
stopped|paused) echo "real start failure" >&2; exit 1 ;;
*) echo "unexpected state" >&2; exit 1 ;;
esacDo NOT retry the start (the VM is already starting), recreate the VM, or treat the stderr text as authoritative. The same -2700 code with the different message Operation not supported by the backend. IS a real failure — see ip-address, exec, and the troubleshooting reference. Disambiguate by the trailing message line.
suspend
utmctl suspend [--save-state] <identifier>- Without
--save-state, the VM is paused in memory only. Quitting UTM (or rebooting the host) loses the state. - With
--save-state, UTM writes a snapshot to disk inside the.utmbundle, so the VM can be cold-resumed later.
stop
utmctl stop [--force | --kill | --request] <identifier>Mutually exclusive flags; default is --force:
--force(default) — sends a stop request to the QEMU/VZ backend. Equivalent to "Stop" in the menu. Fast, may corrupt unflushed guest writes the same way pulling power would.--request— issues an ACPI/QMP power-down request. The guest OS sees this as the user pressing the power button and shuts down cleanly. Some guests (or guests with no power-button handler) may ignore it indefinitely.--kill— terminates the QEMU process. Last resort for hung VMs.
For automated test runs prefer --request followed by polling utmctl status until stopped, with a fallback to --force.
clone
utmctl clone [--name <new-name>] <identifier>Duplicates the VM bundle (deep copy of disks). If --name is omitted, UTM derives a name like <original> Clone. The clone's UUID is freshly generated.
delete
utmctl delete <identifier>No confirmation. Deletes the bundle from disk and removes it from the registry. There is no undo. Always print and confirm utmctl list output before scripting delete.
attach
utmctl attach [--index <N>] <identifier>Intended to redirect a serial port to the calling terminal. The terminal-emulation half is unimplemented, but the command does print the serial endpoint before returning, so it is usable as a discovery tool. Output looks like:
WARNING: attach command is not implemented yet!
PTTY: /dev/ttys009or, for a TCP serial port:
WARNING: attach command is not implemented yet!
TCP: 127.0.0.1:4001--index N selects the serial-port index (defaults to the first one with an available interface). Pipe the output through grep -E '^(PTTY|TCP):' to extract the address, then connect with screen <ptty> or nc <host> <port>. For an AppleScript-based alternative that does not print a warning, see applescript.md.
ip-address
utmctl ip-address <identifier>Prints one IP per line. IPv4 addresses appear before IPv6. Loopback addresses are excluded. Requires the QEMU guest agent to be running in the guest.
Do not call this on Apple-backend VMs. It always fails with Operation not supported by the backend and does nothing useful — there is no guest agent on Apple Virtualization. Detect the backend first (osascript -e 'tell application "UTM" to get backend of virtual machine named "<vm>" as text'); on apple, use ARP on bridge100 or mDNS instead. See SKILL.md → Finding a guest's IP.
Useful pattern (poll until ready):
until ip=$(utmctl ip-address "Ubuntu" 2>/dev/null | head -n1) && [ -n "$ip" ]; do
sleep 2
done
echo "Guest IP: $ip"exec
utmctl exec [--input] [--env NAME=VALUE]... <identifier> -- <command> [args...]Executes command inside the guest via the QEMU guest agent.
--input— read host stdin and forward to the guest process's stdin.--env NAME=VALUE— repeat for additional env vars. UTM passes these as a flat list.- Use
--before the guest command so utmctl's argument parser stops consuming flags — without it, a guest flag like-cis parsed as a utmctl flag and rejected.
Output behavior:
- Guest stdout streams to host stdout, guest stderr to host stderr.
- Exit status of the guest process becomes
utmctl's exit status. - Output is captured on the UTM side until the guest process exits, then flushed; you do not get true real-time streaming. For long-running processes prefer SSH.
Examples:
# Capture command output
out=$(utmctl exec "Ubuntu" -- /usr/bin/uname -srm)
# Forward stdin
echo 'print("hi")' | utmctl exec --input "Ubuntu" -- /usr/bin/python3 -
# Set env
utmctl exec --env LC_ALL=C --env DEBIAN_FRONTEND=noninteractive \
"Ubuntu" -- /usr/bin/apt-get -y updateFailure modes:
- Returns "operation not supported" on Apple-backend VMs (no QEMU guest agent).
- Hangs if the guest agent service isn't running. Inside Linux:
systemctl status qemu-guest-agent. Inside Windows: ensure theQEMU Guest Agent VSS Providerservice is running. - The path passed to
at/the executable must exist or be reachable through the guest'sPATH. The agent does not run a shell unless you call one explicitly (/bin/sh -c '...').
<a name="file"></a>
file pull / file push
utmctl file pull <identifier> <guest-path>
utmctl file push <identifier> <guest-path>pull writes the guest file's contents to host stdout. push reads host stdin and writes it to <guest-path> on the guest. Both transfer in 4096-byte base64-encoded chunks via the guest agent, so they handle binary data correctly but are slow for files larger than a few MB — for big transfers, use SSH/SCP/rsync over the guest's IP instead.
# Save guest log
utmctl file pull "Ubuntu" /var/log/syslog > syslog.txt
# Upload a file
utmctl file push "Ubuntu" /root/setup.sh < ./setup.sh
utmctl exec "Ubuntu" -- /bin/sh -c 'chmod +x /root/setup.sh && /root/setup.sh'The guest path must be writable by the user the guest agent runs as (typically root on Linux, SYSTEM on Windows).
<a name="usb"></a>
usb list / connect / disconnect
utmctl usb list
utmctl usb connect <vm-identifier> <device-identifier>
utmctl usb disconnect <device-identifier>usb list prints currently-attached host USB devices that UTM can see, with columns Name, VID :PID (note the space before the colon — that is the literal column header), and Location. The device identifier passed to connect/disconnect is either:
VID:PIDin hex (e.g.046D:C016), or- the integer Location id from
usb list.
USB pass-through is QEMU-only. The Apple Virtualization backend cannot attach host USB devices. Some hosts also require granting UTM the "Input Monitoring" or "USB" entitlement before the device shows up.
# Move a YubiKey into a Windows VM, work, then return it to the host
utmctl usb connect "Windows 11" 1050:0407
# … VM uses the device …
utmctl usb disconnect 1050:0407---
Common patterns
Wait for a state change
wait_for() {
local vm=$1 want=$2 timeout=${3:-120}
for ((i=0; i<timeout; i++)); do
[ "$(utmctl status "$vm")" = "$want" ] && return 0
sleep 1
done
return 1
}
utmctl start "Ubuntu"
wait_for "Ubuntu" startedGraceful stop with fallback
graceful_stop() {
local vm=$1
utmctl stop --request "$vm"
if ! wait_for "$vm" stopped 60; then
echo "Guest ignored power-down, forcing." >&2
utmctl stop --force "$vm"
fi
}CI: run a command in a disposable clone
utmctl clone "Ubuntu Base" --name "ci-$BUILD_ID"
utmctl start --disposable "ci-$BUILD_ID"
trap 'utmctl stop --force "ci-$BUILD_ID"; utmctl delete "ci-$BUILD_ID"' EXIT
# wait for guest agent
until utmctl ip-address "ci-$BUILD_ID" >/dev/null 2>&1; do sleep 2; done
utmctl exec "ci-$BUILD_ID" -- /bin/bash -lc './run-tests.sh'Discover all VMs in a script
utmctl list | tail -n +2 | awk '{ uuid=$1; status=$2; $1=$2=""; sub(/^ /,""); print uuid"\t"status"\t"$0 }'The first column is always a UUID, second is the status, the rest is the name (which can contain spaces).
Detect backend before sending input
backend=$(osascript -e 'tell application "UTM" to get backend of virtual machine named "MyVM" as text')
[ "$backend" = "qemu" ] || { echo "Input automation requires QEMU backend" >&2; exit 1; }For input itself, see applescript.md — utmctl has no input subcommand.
Things utmctl does NOT do
These require AppleScript:
- Create a new VM (
make new virtual machine). - Send keyboard input or mouse clicks (
input keystroke,input mouse click,input scan code). - Read or change configuration (RAM, CPU, disks, network mode, port forwards, displays).
- Open or read/write guest files at arbitrary offsets —
utmctl filealways streams from offset 0 and closes after. - Mount/unmount removable media or update directory-share bookmarks (
update registry).
For all of those, see applescript.md.
UTM end-user workflows
Recipes for the work users actually do in UTM: install a guest OS, set up file sharing, configure networking, manage snapshots/backups. Every workflow here assumes UTM 4.x on macOS. iOS is covered briefly in troubleshooting.md.
Contents
- Choosing a backend
- Where VMs live on disk
- Importing a VM from the gallery
- Installing Linux (Apple backend)
- Installing Linux (QEMU backend)
- Installing Windows 11 ARM
- Installing Windows on Intel via QEMU
- Installing macOS as a guest
- File sharing
- Networking
- Snapshots and backups
- Adding a drive or ISO at runtime
Choosing a backend
| Use case | Backend |
|---|---|
| Run macOS guest on Apple Silicon | Apple (only option) |
| Run modern Linux fast on Apple Silicon | Apple preferred (virtio drivers, near-native speed) |
| Run Windows 11 ARM on Apple Silicon | QEMU (Apple Virtualization does not support Windows guests) |
| Run x86 Windows / Linux on Apple Silicon | QEMU with TCG emulation (slow) |
| Run x86 Windows / Linux on Intel Mac | QEMU with HVF acceleration (fast) |
| Need USB pass-through | QEMU |
| Need scripted keyboard / mouse input | QEMU |
| Need cross-arch (ARM, RISC-V, PPC, MIPS, …) | QEMU |
| Need fastest macOS-on-macOS | Apple |
The backend is permanent for a VM — you cannot convert in place. duplicate always preserves the backend.
Where VMs live on disk
Default storage:
~/Library/Containers/com.utmapp.UTM/Data/Documents/<Name>.utm<Name>.utm is a directory bundle (Finder hides this and shows a single icon — right-click → Show Package Contents). Inside:
<Name>.utm/
├── config.plist # XML or binary plist; full config
├── Data/ # disk images and auxiliary storage
│ ├── <uuid>.qcow2 # virtual drives (or .img / .raw)
│ └── efi_vars.fd # firmware NVRAM
├── view.plist # last window size/position (per host)
└── screenshot.png # last frame (used as VM tile)The bundle is fully self-contained. Backup = cp -R (or cp -Rc on APFS for clone-reflinks). Move between Macs by copying the bundle and double-clicking it; UTM will register it.
To put VMs on an external drive, change the storage location in UTM → Settings → General → Default location, or move bundles by hand and double-click them on the new path.
Importing a VM from the gallery
The official gallery is at <https://mac.getutm.app/gallery/>. Each entry links to a .utm or .zip containing one. To import:
1. Download and (if needed) unzip — it expands the qcow2 image, requires several GB free. 2. Double-click the .utm bundle, or drag onto UTM's main window, or utmctl import (no such command — use AppleScript: tell application "UTM" to import new virtual machine from file …). 3. First boot may take a minute as UTM expands sparse images.
Most gallery entries use the QEMU backend so they work on both Apple Silicon and Intel.
Installing Linux (Apple backend)
Best path for fast, clean Linux on Apple Silicon. Limited to distros whose installer ships a Linux kernel + initrd UTM can launch directly (most modern x86_64 ISOs do not work; ARM64 ISOs do).
1. Download an ARM64 ISO (Ubuntu, Debian, Fedora — pick the aarch64/arm64 server or live ISO). 2. File → New → Virtualize → Linux. 3. Pick the ISO. For Ubuntu/Debian server installers UTM autodetects the kernel and initrd from the ISO; if not, point it at them manually (mount the ISO and look in casper/, install/, or boot/). 4. Allocate RAM (4096+ MiB) and CPU (4 cores typical) and a disk (32+ GiB). 5. Optionally add a Shared Directory. 6. Boot, install as you would on bare metal. Keep the ISO attached only on first boot; remove afterward (Settings → Drives → CD/DVD). 7. After install, install GUI drivers if you want sharper graphics: apt install spice-vdagent spice-webdavd (or distro equivalent).
GPU acceleration via VirGL/Venus (virtio-gpu-gl-pci) works on recent macOS versions but is off by default; turn on Display → GPU Supported if your guest drivers are recent enough.
Installing Linux (QEMU backend)
Use this when you need x86_64 Linux on Apple Silicon, or a non-mainstream architecture, or USB pass-through.
1. File → New → Emulate → Linux (or Other for exotic archs). 2. Pick architecture and machine. Defaults: q35 for x86_64, virt for aarch64. 3. Tick UEFI Boot for modern distros. 4. Attach the ISO as a CD/DVD drive on first boot. 5. Allocate RAM (2048–4096 MiB), CPU (host cores), disk (32+ GiB). 6. Sharing: pick VirtFS (Linux-only, low overhead, mount -t 9p -o trans=virtio share /mnt/share) or WebDAV / SPICE (works for any guest with spice-webdavd). 7. Boot, install. Then install qemu-guest-agent so utmctl exec/ip-address/file work:
sudo apt install -y qemu-guest-agent
sudo systemctl enable --now qemu-guest-agent8. Optionally install spice-vdagent for clipboard sharing and dynamic resolution.
x86_64 Linux on Apple Silicon runs at ~10–30 % of native through QEMU TCG. Acceptable for compiling/testing; not for desktop use.
Installing Windows 11 ARM
Apple Silicon only. Install through the QEMU backend (the Apple backend cannot run Windows).
1. Generate a Windows 11 ARM ISO with Crystalfetch (UTM's sibling app, on the App Store) or download directly from Microsoft. 2. File → New → Virtualize → Windows (UTM detects the ARM architecture automatically). 3. Tick Install drivers and SPICE tools so virtio drivers and SPICE Guest Tools are mounted on first boot. 4. Allocate ≥ 4096 MiB RAM, ≥ 4 cores, ≥ 64 GiB disk. 5. Boot, run setup. When the partition step shows "no drives", click Load driver and pick viostor from the virtio CD. 6. After install, run the SPICE tools installer from the same CD for clipboard, dynamic resolution, and folder sharing. 7. Activate Windows or accept the eval period.
Common pitfalls:
- Setup freezes on "Just a moment". Hit Shift+F10 to open
cmd, runOOBE\BYPASSNRO, then continue. Lets you complete setup without a Microsoft account / network. - No mouse cursor in installer. Some virtio versions miss the input driver — the SPICE tools CD includes it, install after first boot.
- TPM/Secure Boot are simulated by the QEMU machine; do not disable in config.
Installing Windows on Intel via QEMU
On an Intel Mac the same x86_64 ISO works, with HVF acceleration enabled by default. Use the Windows preset, attach the ISO, install. Performance is near-native.
On Apple Silicon you can still run x86_64 Windows via QEMU TCG — installation alone may take ≥ 1 hour and runtime is slow. Prefer Windows 11 ARM unless you specifically need x86 Windows software (in which case Microsoft's x86 emulation inside Windows 11 ARM is usually faster than QEMU TCG).
Installing macOS as a guest
Apple Silicon host only, macOS 12+ host, Apple backend only. Cannot run on Intel Macs.
1. File → New → Virtualize → macOS 12+. 2. UTM offers to fetch the latest IPSW restore image automatically, or accept a path you specify. 3. Allocate RAM (4096+ MiB) and disk (≥ 64 GiB; 128 GiB+ if installing Xcode). 4. UTM creates an auxiliary storage and machine-identity blob inside the bundle automatically. 5. Boot — first launch installs macOS into the disk image, takes 10–20 minutes. 6. Complete the macOS Setup Assistant inside the guest.
Limitations of macOS guests:
- No GPU acceleration (no Metal). 3D apps run on CPU.
- iCloud account sign-in works but Apple ID activation may fail; some users keep guests local-only.
- USB pass-through is not available (Apple backend limitation).
- Cloning a macOS guest produces two VMs with the same machine identity — Apple's services may flag the second one. Use
duplicate(which regenerates the auxiliary blob) rather than copying the bundle.
File sharing
| Backend | Recommended share method | Mount inside guest |
|---|---|---|
| Apple, Linux/macOS guest | Apple-native shared directory | Auto-mounts as /Volumes/My Shared Files (macOS guest) or via mount -t virtiofs share /mnt/share (modern Linux) |
| QEMU, Linux guest | VirtFS (9P) | mount -t 9p -o trans=virtio,version=9p2000.L share /mnt/share |
| QEMU, Windows or other | WebDAV via SPICE Guest Tools | Tools install a Spice client folder mapping; or \\spice-host\Shared |
| Either, big files | SMB/SSH from host network | Standard SMB/SSH client in guest |
WebDAV requires spice-webdavd in the guest (Linux) or the SPICE Tools installer (Windows). VirtFS requires the host to expose a directory in Settings → Sharing.
Apple Virtualization shared directories are read/write by default. To share read-only, tick "Read Only" when adding the share.
Networking
| Mode | Effect |
|---|---|
| Shared (NAT) | Default. Guest gets a private IP from UTM's DHCP, can reach the internet, host can reach guest. Works without privacy prompts. |
| Bridged | Guest is a peer on the host's LAN. Pick a host interface (en0/Wi-Fi). On macOS Sequoia+, requires granting UTM "Local Network" permission. |
| Host-Only | Guest is isolated to a virtual subnet shared with other UTM guests on this host. No external connectivity. |
| Emulated VLAN | QEMU only; the SLIRP user network with full DHCP/DNS configurable. Useful for offline labs. |
Port forwarding (Shared mode, QEMU only)
Per-VM, Settings → Network → New Port Forward. UTM stores them in additionalArguments as QEMU -netdev hostfwd=… entries. Host port → Guest IP : Guest port.
host TCP 2222 → 192.168.64.x:22 # SSH from host: ssh -p 2222 user@localhost
host TCP 8080 → 192.168.64.x:80Apple-backend VMs do not support port forwarding — use Bridged mode and connect to the guest's LAN IP, or set up a reverse SSH tunnel from inside the guest.
Finding the guest's IP
UTM's default Shared (NAT) network lives on host interface bridge100, subnet 192.168.64.0/24 (gateway .1). Both Apple-backend and QEMU "Shared" guests show up there; bridged-mode guests appear on the host's primary LAN (en0 etc.) instead.
- QEMU + guest agent installed →
utmctl ip-address "<vm>". Only path that returns the IP directly. - Apple backend (always) and QEMU without guest agent →
utmctl ip-addresswill fail (Apple:Operation not supported by the backend; QEMU without agent: timeout). Skip it. Read the host ARP table onbridge100:
arp -a -n -i bridge100 # raw table
arp -a -n -i bridge100 \
| awk '$2 != "(192.168.64.1)" && $4 != "incomplete" && $2 ~ /^\(/ \
{ gsub(/[()]/, "", $2); print $2 }' # one IP per running guest (excludes the bridge gateway)- mDNS (most Linux distros and macOS guests advertise by default):
dscacheutil -q host -a name myhost.local # resolve a known hostname
dns-sd -B _ssh._tcp local. # browse advertising hosts- GUI fallback: hover over the network icon in UTM's status bar.
Snapshots and backups
UTM exposes only a primitive snapshot model:
- Suspend with save state (
utmctl suspend --save-state) writes the running RAM/CPU snapshot into the bundle. Resume withutmctl start. - Disposable mode (
utmctl start --disposable) runs from a scratch overlay; changes are discarded. - Manual snapshots are not in the CLI/AppleScript dictionary. The QEMU monitor command
savevm/loadvmworks if you build your own QEMU monitor connection (advanced).
For real backups, stop the VM and copy the bundle. APFS clones make this near-instant:
cd ~/Library/Containers/com.utmapp.UTM/Data/Documents
cp -Rc "Ubuntu.utm" /Volumes/Backups/Ubuntu.utmUse fsfreeze (Linux) or vssadmin Create Shadow (Windows) before copying a running guest's bundle if you do not want to stop the VM. See the snapshot recipe in applescript.md.
Adding a drive or ISO at runtime
A drive must be added while the VM is stopped:
1. Stop the VM. 2. Settings → Drives → New → choose Disk Image, ISO, or NVMe/IDE/SCSI/VirtIO; pick a host file. 3. Save and restart.
Removable drives (CD/DVD) can be swapped at runtime via the toolbar disk icon — UTM updates the registry bookmark; the underlying QEMU sees a media-change event. Use AppleScript `update registry` to do the same from a script.
For per-VM customization beyond the GUI, edit config.plist while UTM is closed. See configuration.md.