Utmapp
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).From its SKILL.md
npx -y skills add ljagiello/agent-skills --skill utmappAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
16.2 KB, ~3.9k tokens by cl100k_base, as published. Nobody here has run it
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 asutmctlin 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.plistinside a.utmbundle 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". deletehas no confirmation. Always check withutmctl listfirst.stopdefaults 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 callutmctl ip-addresson 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, andip-addressneed the QEMU guest agent. Installqemu-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 clonea 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 -2700from utmctl is overloaded — disambiguate by command and trailing line, then checkutmctl 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). Treatutmctl 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 version
Guest-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 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") {
// 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
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-agentinstalled →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.
What ships with it: 5 files
67.6 KB alongside SKILL.md
references/
- applescript.md16.4 KB
- configuration.md12.6 KB
- troubleshooting.md12.2 KB
- utmctl.md13.4 KB
- workflows.md12.9 KB