I’ve always wanted a Docker-like CLI for virtual machines. Pull an image, clone it, run it, throw it away. I’ve had throwaway scripts for this forever, and in 2020 I called Multipass “the Docker of creating virtual machines”. For Linux I used Lima for a long time. tart is the first one that really clicked for me: OCI images, instant clones, macOS and Linux guests, and on Apple silicon it’s fast. Really fast.

What it didn’t run was Windows, which is exactly the OS I want throwaway machines for, because it’s the hardest one to automate.

There’s a one-minute video of it on X: real boots, and a .NET app built and run over tart exec.

Some history

In 2014 I was high on Go and Docker, and I wrote my own wrappers around QEMU and KVM, because libvirt felt like too much for what I needed. That’s when it clicked for me: virt-manager and friends are mostly generating enough arguments for qemu and qemu-img. Once you see that, VMs stop being magic.

I even implemented live migration once. It’s mostly copying CPU state and RAM, re-sending the pages that got dirty in the meantime, and a brief pause at the end. With a virtualized network layer and a shared filesystem like GlusterFS underneath, we moved servers with 16 GB of memory over a gigabit network with only a few seconds of pause. Fun times.

Around 2015, early in my career, I worked on a platform that put VMware, Hyper-V, KVM, LXC and Docker behind one API, to tame the sprawl. I was mostly an API user of those hypervisors, and the project was eventually cancelled, but it taught me a lot. I like virt-manager, and I like KubeVirt, but they abstract too much away from me. I’ve also tried shipping VMs as Docker images, with QEMU and the disk image inside. Docker’s own copy-on-write layers are a bad fit for multi-gigabyte disk images, so I ended up using QEMU’s own layering (qcow2 backing files) instead.

Why Windows didn’t run on tart

tart is a CLI around Apple’s Virtualization.framework, made by Cirrus Labs, which joined OpenAI this year. Apple’s framework officially runs two kinds of guests: macOS and Linux.

When someone asked for Windows in tart issue #1123, the answer was:

Since Tart is using Apple’s Virtualization.Framework under the hood, we’re limited to the devices already implemented by that framework. As a result, support for Windows can only happen when either (1) Virtualization.Framework implements/emulates devices needed for proper Windows functioning, or (2) Windows implements drivers needed to run in virtualized Apple Silicon environments.

Fair. UTM does run Windows on Apple silicon, but not through Apple’s framework. UTM has two backends: Apple’s Virtualization.framework, the only way to run macOS guests, and QEMU, which uses Apple’s lower-level Hypervisor.framework (HVF) so the CPU still runs at native speed. Windows goes through QEMU. QEMU then emulates whatever devices Windows wants: a simple framebuffer, a TPM through swtpm, and so on. Apple’s framework doesn’t let you add devices. You get its virtual hardware, and Windows has to live with it.

It turns out neither Apple nor Microsoft had to change anything. It took four pieces.

1. A hidden switch

Out of the box, Windows doesn’t boot on Apple’s framework: the VM sits with one vCPU at 100% forever.

The culprit is the PMU, the performance monitoring unit: the hardware counters in the CPU (cycles, instructions, cache misses) that profilers and the OS itself use. Apple’s framework doesn’t give Linux guests one by default, and Linux doesn’t care: it checks whether a PMU is there and boots fine without one. Windows doesn’t get that far.

Apple’s framework has private settings that aren’t in its documentation, and anyone can list them: the Objective-C runtime will tell you every method a class has. On macOS 27 there are 85 private setters across 25 classes, most of them clearly internal (_setTestIgnoreEntitlementChecks: is a fun one). The generic platform, the one Linux guests use, has four. Claude found the right ones on its own, by listing them, reasoning about which ones Windows could plausibly need, and trying them until Windows booted. The branch turns on two:

func platform(nvramURL: URL, needsNestedVirtualization: Bool) throws -> VZPlatformConfiguration {

let result = VZGenericPlatformConfiguration()

...

+ try Self.checkHostSupport()

+ Dynamic(result)._setPerformanceMonitoringUnitEmulationEnabled(true)

+ Dynamic(result)._setFineGrainedTrapsEmulationEnabled(true)

return result

}Only the first one is actually needed. I tested each one turned off. Without PMU emulation, Windows never boots. Without fine-grained traps, a full unattended install still finishes and Windows boots to the desktop normally, at least on an M4 Pro.

That’s also the scary part: a private API can disappear in any macOS update.

2. A 7 KB UEFI driver

Apple’s firmware can draw to the virtual GPU only by copying blocks of pixels. It doesn’t give the OS a framebuffer, a chunk of memory to draw into, which is what Windows Boot Manager expects.

I checked what happens without one, by hiding the driver from the firmware. Windows doesn’t boot blind, and it doesn’t hang either: the whole VM dies about 3.5 seconds in, right when the Windows logo would appear, with “The virtual machine stopped unexpectedly”. My best guess is that Boot Manager writes to a framebuffer address that doesn’t exist, and the hypervisor kills the VM for it.

So there’s a tiny UEFI driver (about 7 KB) that reserves a framebuffer in RAM, hands it to Windows, and copies changed rows to the real screen every 40 ms. It’s basically QEMU’s ramfb, rebuilt as software inside the firmware. When Windows’ own display driver takes over, it stops.

3. Skipping Windows Setup

Windows Setup can’t install on this VM, and its own logs say why:

- Windows 11 Setup hard-blocks on the missing TPM (TpmVersion: Hardin its compatibility scan). Everything else passes, even Secure Boot.

- With that check bypassed, it fails at the disk step: DiskLayoutMakeSystem: Failed to create system partition (0x8007000e). It tries to create its own 200 MB system partition even when the disk already has one, and there’s no room left.

(You also couldn’t watch it fail: WinPE has no driver for Apple’s virtual GPU, so the screen stays frozen on the boot logo.)

So tart create --from-iso doesn’t use Setup’s installer. It builds its own install media, and a script in WinPE does the install: partition the disk with diskpart, apply the image with DISM, add the drivers. Two boots, no clicks, no TPM needed.

tart create win11 --from-iso Win11_25H2_English_Arm64_v2.isoYou can get the ISO from Microsoft’s Windows 11 for Arm download page.

4. Drivers and the guest agent

Windows ships no drivers for virtio devices, and virtio devices are all Apple’s framework offers for disk, network, display and file sharing. The virtio-win project fills that gap. It’s the Windows driver set that Red Hat ships with Fedora and RHEL for KVM guests, and it has had ARM64 builds since version 0.1.164 in February 2019 (experimental at first). Shared folders (tart run --dir) show up as Z: through virtio-fs and WinFsp. You can share a single folder, or the whole Mac with tart run win11 --dir=/:

For tart exec and clipboard sharing, the tart guest agent needed a Windows port: vsock through virtio-win’s viosock driver, ConPTY for terminals, and job objects instead of Unix process groups.

~ % tart exec win11 cmd /c ver

Microsoft Windows [Version 10.0.26200.8037]That’s enough to drive Windows from a Mac terminal or a CI script. Install the .NET SDK with winget, then build and run a console app that prints where it’s running:

~ % tart exec win11 winget install --id Microsoft.DotNet.SDK.9 -e

Found Microsoft .NET SDK 9.0 [Microsoft.DotNet.SDK.9] Version 9.0.318

...

Successfully installed

~ % tart exec win11 dotnet new console -o hello

The template "Console App" was created successfully.

~ % tart exec win11 powershell cat hello\\Program.cs

using System.Runtime.InteropServices;

Console.WriteLine($"Hello from {RuntimeInformation.OSDescription} on {RuntimeInformation.OSArchitecture}");

~ % tart exec win11 dotnet run --project hello

Hello from Microsoft Windows 10.0.26200 on Arm64And inside, Windows knows exactly where it is:

Windows Server

Windows Server works too, unattended in under four minutes, with Microsoft’s public KMS client key so setup doesn’t stop at the product key page (it doesn’t activate Windows).

The hard part is getting an image. There’s no official ARM64 Windows Server ISO, and the UUP dump links I found had expired. I ended up with two prerelease ISOs from archive.org, which I can only verify against the uploader’s checksums. Microsoft keeps these behind closed doors.

Numbers

These are basic benchmarks, not a lab study: 7-Zip’s built-in benchmark (the same 7-Zip 26.03, native ARM64 on both sides) and a small Go program that writes 2 GiB and flushes it to disk. Everything ran on a Mac mini M4 Pro with macOS 27, with the VMs at 8 vCPUs and 16 GB.

To compare with UTM, I booted the same Windows disk under QEMU with Hypervisor.framework, which is the engine UTM uses for Windows.

CPU is close to native, and the same on both, since both run on Apple’s hypervisor underneath.

Disk is the weak spot, and it’s Windows-specific. Ubuntu on the same tart setup writes at 3.4–5.5 GB/s, close to the host. Windows tops out lower because of its storage driver path (virtio-win’s viostor plus NTFS). tart also flushes every write all the way to the SSD by default, which costs about 30%; with caching on and sync=none, tart’s Windows writes reach about 2 GB/s, the same as QEMU’s defaults.

And the timings, with tart:

tart clone is instant either way (APFS copy-on-write).

What doesn’t work (yet)

- Suspend and resume. Apple’s framework refuses to save the VM.

- Sound. Windows sees the virtio-sound device, but virtio-win has no driver for it.

- Nested virtualization. --nestedboots, but once Windows tries to start its own hypervisor (WSL2, Hyper-V), it hangs.

- GPU acceleration. The display driver is display-only. For everyday Windows or games, Parallels is the better fit.

- Agent setup is manual for now.

- The private API. See above.

What it’s for

- Throwaway Windows VMs for end-to-end tests: tart clone,tart run,tart delete.

- Testing Windows-only software: installers, apps, upgrade paths, on real Windows.

- A Windows Server when you need one, say Active Directory for a test domain.

- Windows CI on your own Macs, next to your macOS and Linux runners.

That last one is more interesting than it sounds. Apple’s M-series single-core performance beats a lot of x86 hardware, and probably the shared runners on cheap CI plans. If you ever build CI on Mac minis, Windows jobs on them could end up faster and cheaper.

How it was built

All of the code was written by Claude Opus 5.5. I started with a bit of a pep talk: people already have Linux drawing its first triangle on the M6 GPU, so getting Windows into a hypervisor should be nothing. It needed that nudge, because “make an OS run where the vendor says it can’t” looks a lot like a reverse engineering task at first glance, and frontier models are careful with anything that smells like security work unless you’re enrolled in their programs. I was ready to try Qwen and GLM 5.3 variants instead, but it wasn’t needed. I also set guardrails: never disable SIP, nothing shady, only what a normal signed app can do. Then I tested it on a second machine, which is where the Windows Server fix, the benchmarks and most of the “what doesn’t work” list came from.

Overall, I like it a lot.

What I actually want

As great as this is, what I still long for is a true Docker for VMs that depends only on qemu and qemu-img. tart is great, but it isn’t portable: it’s macOS only, by design.

libvirt is great, very generic software: the same XML describes a VM for QEMU/KVM, Xen, LXC, VirtualBox, VMware, Hyper-V, bhyve and more. That’s exactly why its XML is a bit bloated for my taste. virt-manager on Linux is great too. But a Docker-style CLI doesn’t need any of that. I just want to wrap QEMU commands, nothing fancy, and sometimes QEMU’s man pages are easier to read than libvirt’s XML translation of them.

I mostly care about CI automation, not desktop virtualization or virtual desktop infrastructure (VDI). Those are easy enough with the well-known platforms anyway.

I’ll find time to build it, hopefully: OCI images like tart, running on Kubernetes, a Docker for VMs in the cloud, and the same images on Mac and Windows dev machines.

Try it

It’s an unofficial preview build of my fork, not an OpenAI or Cirrus Labs release. I’ve also linked this post on tart issue #1123.

curl -LO https://github.com/mustafaakin/tart/releases/download/windows-preview-0.1.0/tart-windows-preview.tar.gz

tar xzf tart-windows-preview.tar.gz

xattr -dr com.apple.quarantine tart.app # only if you downloaded it with a browser

tart.app/Contents/MacOS/tart create win11 --from-iso Win11_Arm64.isoA few warnings:

- Bring your own Windows 11 ARM64 ISO from Microsoft. Activation and licensing are up to you; Microsoft only authorizes Parallels for Windows on Apple silicon.

- The VM has a known login (admin/admin) with SSH open on tart’s NAT network. Change the password.

- tart createdownloads virtio-win and WinFsp during install, both pinned by checksum.

- It needs Apple silicon, and it’s tested on macOS 26.6 and 27 only.

The code is on the windows-guest-extras branch of my fork (30 files, about 1,900 lines), and the agent is on the windows branch.