Increase disk size of a UTM macOS VM and expand its APFS container

Increasing a UTM virtual disk's size does not automatically expand the guest's APFS container. If a Recovery partition lies between the container's partition and the added free space, it blocks direct expansion.

This Codex/Astra-crafted script uses Python 3.9+ standard library and macOS diskutil, cp, and lsof; no pip packages, Go, Homebrew, or QEMU are required. It targets macOS 26+ with diskutil image support and an APFS host filesystem for copy-on-write backups:

Usage

Shut down the VM and do not start it while the script runs. Pass either the .utm bundle or its writable disk image. Inspection is the default:

python3 resize_utm.py '/path/macOS.utm' --size-gib 200

Apply the change, creating a new mandatory image backup first:

python3 resize_utm.py '/path/macOS.utm' --size-gib 200 --apply --backup '/path/macOS-before.img'

The size is GiB, so 200 means 214,748,364,800 bytes (Disk Utility displays about 214.7 GB). An initial 120 GiB image therefore gains 80 GiB. The main APFS container is smaller than the disk because ISC and outer Recovery retain their space.

What it does

  1. Resolves the single writable image from an Apple-backend UTM bundle, or accepts an image directly. Rejects physical devices, images currently open by another process, shrinking, and unexpected partition layouts.
  2. Attaches read-only for inspection. Validates both GPT headers and partition arrays, their CRCs, partition bounds/order, and Recovery's APFS superblock geometry.
  3. With --apply, hashes the entire Recovery partition and creates a native APFS clone backup of the image.
  4. Uses diskutil image resize --image-only to grow the image. On a tested macOS 27 build, this already relocates Recovery for ASIF images.
  5. If needed, relocates Recovery itself through the attached raw device. It derives usable space from actual device capacity and GPT array dimensions, preserves APFS block alignment, copies and SHA-256-verifies Recovery before updating GPT, and writes both GPT copies with new CRCs. Partition IDs, names, attributes, and unrelated entries are preserved.
  6. Checks Recovery against its original full SHA-256, detaches/reattaches to refresh the kernel's partition map, and expands the main APFS container using diskutil apfs resizeContainer ... 0.
  7. Verifies the partition map and checks the final GPT layout fills the space up to Recovery. APFS's resize operation also performs a filesystem consistency check.

Only exactly three outer partitions, in physical order ISC / APFS / Apple_APFS_Recovery, are supported. Matching uses the Recovery type GUID, not a guessed name. Raw and ASIF image interpretation is delegated to macOS. The relocation core supports 512- and 4096-byte logical sectors. Growth smaller than or equal to Recovery's size is deliberately refused to avoid an overlapping copy. Already-grown images can be completed by requesting their existing capacity.

Failure and rollback

This is not a crash-atomic disk transaction. A crash during GPT commit can leave its copies inconsistent; validation then fails closed. The script does not infer missing Recovery data from an NXSB signature, and does not attempt to repair a partially executed third-party resizer. The retained image backup is the rollback source. A copy interruption before GPT commit leaves the original Recovery and GPT intact, although unused destination space may have changed.

If anything fails, leave the VM stopped. Eject any attachment listed in the log before restoring. Replace the modified image with a copy of the retained backup at the exact original image path. Do not restore only the old GPT after APFS growth.

An encrypted/locked APFS guest may require unlocking its Data volume before APFS expansion; the script reports the native error rather than asking for or recording passwords. Do not launch UTM or attach the image in another app during the run; the initial open-file check is not an exclusive lock against other applications.

References

The workflow is based on the UTM discussion and yunyang088's Go resizer. The discussion documents the stale-GPT-end bug, fixed here by computing geometry from actual device capacity. This implementation takes a different interruption strategy: verified nonoverlapping copy before GPT publication, with a mandatory whole-image backup for rollback.

❧ 2026-09-18