macos

Tart VM on Apple Silicon: Run macOS and Linux VMs (OpenAI Tart)

A Tart VM gives you macOS or Linux on an Apple Silicon Mac through Apple’s Virtualization.framework, and the project now lives at openai/tart on GitHub. The tart.run quick-start still says brew install cirruslabs/cli/tart, but that tap is frozen at Tart 2.32.1 and its formula no longer even loads on current Homebrew.

Original content from computingforgeeks.com - post 179858

You’ll get the install that works today, macOS and Linux guests, shared folders, nested KVM, a registry round trip and a disposable VM per AI agent task, plus the headless keychain errors that stop most first attempts. Every command and output below was built and run in October 2026 on an AWS EC2 mac-m4.metal host (Apple M4, 24 GB, macOS 26.7) with Tart 2.41.1.

Check your Mac against each feature

Any Apple Silicon Mac on macOS 13 Ventura or newer runs the Tart binary, because Package.swift targets .macOS(.v13) and the formula itself says depends_on :macos => :ventura. Homebrew is stricter, though. The OpenAI formula pulls in softnet, and the softnet formula declares depends_on :macos => :tahoe, so on macOS 13 to 15 use the release tarball. Intel Macs cannot run it at all. The newer features each raise that floor, so check this table before you plan a CI host around one of them.

FeatureHost needsWhere it’s stated
Run macOS or Linux guests (tarball install)Apple Silicon, macOS 13 VenturaREADME, Package.swift
brew install openai/tools/tartmacOS 26 Tahoe (softnet dependency), plus brew trust on Homebrew 6openai/homebrew-tools softnet.rb, lab run
Shared folders with --dirmacOS 13 host; macOS guests 13+tart run --help
tart execmacOS 14 Sonoma, guest agent in the VMExec.swift error message
Nested virtualization (--nested)M3 or newer chip, macOS 15 Sequoia, Linux guest onlyRun.swift validation, lab run
Headless VMs without a GUI loginAn unlocked login.keychain for macOS gueststart.run FAQ, lab run
Running macOS guests at onceTwo per Mac; Linux guests don’t countApple SLA section 2B(iii), lab run
Softnet isolation (--net-softnet)macOS 26, SUID bit or passwordless sudo for softnetopenai/softnet README

The nested row is the one people misread. An M1 or M2 Mac can’t run KVM inside a guest no matter what you pass, and macOS guests never get it. The macOS-guest limit trips CI plans next: Apple’s macOS licence allows “up to two (2) additional copies or instances” in virtual machines per Mac, and Tart refuses a third one outright, as the agent section shows. Budget for disk as well, because the macOS base image is a 26.4 GB download that becomes a 50 GB disk.

Read the Tart licence before you build on it

Tart ships under FSL-1.1-ALv2, the Functional Source License with an Apache 2.0 future grant, and the copyright line now reads “Copyright 2022-2026 OpenAI”. The Homebrew formula carries the same license "FSL-1.1-ALv2" field. The part that decides whether you can use it is the Permitted Purpose clause, quoted from the LICENSE file:

A Permitted Purpose is any purpose other than a Competing Use. A Competing Use means making the Software available to others in a commercial product or service that: 1. substitutes for the Software; 2. substitutes for any other product or service we offer using the Software that exists as of the date we make the Software available; or 3. offers the same or substantially similar functionality as the Software. Permitted Purposes specifically include using the Software: 1. for your internal use and access; 2. for non-commercial education; 3. for non-commercial research; and 4. in connection with professional services that you provide to a licensee using the Software in accordance with these Terms and Conditions.

Each release also converts on a timer: “We hereby irrevocably grant you an additional license to use the Software under the Apache License, Version 2.0 that is effective on the second anniversary of the date we make the Software available.” There’s no core count anywhere in that text. The tart.run licensing page still shows the older Fair Source terms with a 100-core free tier and paid plans, so trust the LICENSE in the repo, not that page. The old terms still cover older builds, including the 2.32.1 the Cirrus Labs tap serves.

In plain terms, running Tart on your own Macs for your own CI, testing or agent sandboxes is internal use. Selling hosted macOS VMs built on it is the case the Competing Use clause targets, and that is a question for your lawyer, not a blog post.

Install Tart with Homebrew

Use the tap the README names. The openai/homebrew-tools tap serves Tart 2.41.1 and pulls in softnet 0.24.1 as a dependency. On Homebrew 6, a plain brew install openai/tools/tart trusts the tart formula but not its dependency, and stops with this:

==> Tapping openai/tools
Cloning into '/opt/homebrew/Library/Taps/openai/homebrew-tools'...
Tapped 1 cask and 5 formulae (29 files, 266.7KB).
==> Trusted formula openai/tools/tart
Error: Refusing to load formula openai/tools/softnet from untrusted tap openai/tools.
Run `brew trust --formula openai/tools/softnet` or `brew trust openai/tools` to trust it.

Homebrew 6.0.22, the version on the test host, wants you to trust the whole tap once. Then install:

brew trust openai/tools
brew install openai/tools/tart

Homebrew installs softnet first and then Tart, and the install itself took seconds. The deprecation warnings the EC2 image’s own aws tap printed are trimmed from this capture:

Trusted tap: openai/tools
==> Would install 1 formula:
openai/tools/tart 2.41.1
==> Would install 1 dependency for tart:
openai/tools/softnet 0.24.1
==> Fetching downloads for: tart
✔︎ Formula softnet (0.24.1)
✔︎ Formula tart (2.41.1)
==> Installing tart from openai/tools
==> Installing openai/tools/tart dependency: openai/tools/softnet (0.24.1)
🍺  /opt/homebrew/Cellar/softnet/0.24.1: 6 files, 14.6MB, built in 1 second
==> Installing openai/tools/tart
🍺  /opt/homebrew/Cellar/tart/2.41.1: 14 files, 78.8MB, built in 2 seconds

Confirm the binary on your PATH is the new one:

tart --version

It prints 2.41.1, and brew list --versions tart softnet shows tart 2.41.1 and softnet 0.24.1. The formula’s caveat is worth acting on if you’ll spin up many short-lived VMs a day. macOS hands out DHCP leases for 86,400 seconds, so a busy CI host can run out of addresses on its /24. Drop the lease to 600 seconds:

sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.InternetSharing.default.plist bootpd -dict DHCPLeaseTimeSecs -int 600

Reading the plist back with defaults read /Library/Preferences/SystemConfiguration/com.apple.InternetSharing.default.plist bootpd should show DHCPLeaseTimeSecs = 600;, which it did on the test host.

Skip the cirruslabs/cli tap

The cirruslabs/cli/tart command on tart.run and in older tutorials is a dead end now. Its tart.rb is pinned at version 2.32.1 with license "Fair Source", last touched on 11 April 2026. After tapping it and running brew trust --formula cirruslabs/cli/tart, Homebrew 6.0.22 still refused to load the formula at all:

Error: cirruslabs/cli/tart: Calling `depends_on :macos` with `depends_on macos:` is disabled! Use `depends_on :macos` with `depends_on macos:` inside an `on_macos` block instead.
Please report this issue to the cirruslabs/homebrew-cli tap (not Homebrew/* repositories), or even better, submit a PR to fix it:
  /opt/homebrew/Library/Taps/cirruslabs/homebrew-cli/tart.rb:22

Even on an older Homebrew where it still loads, it hands you a release nine minors behind under the old licence. Both taps name the formula tart, so if your older Mac still has the Cirrus Labs build, remove it before you install from the OpenAI tap:

  1. Remove the old binary with brew uninstall tart.
  2. Check for its network helper with brew list softnet, and if it’s there, run brew uninstall softnet.
  3. Drop the tap with brew untap cirruslabs/cli.

On the test host only the untap step was run, because it started clean, and it printed Untapped 1 cask and 12 formulae. You can then trust the OpenAI tap and install as shown above.

Install from the release tarball instead

On macOS 13 to 15, or on a box without Homebrew, grab the signed app bundle from the GitHub release. It unpacks to tart.app plus the LICENSE:

curl -LO https://github.com/openai/tart/releases/latest/download/tart.tar.gz
tar -xzf tart.tar.gz
./tart.app/Contents/MacOS/tart --version

That prints 2.41.1 as well. Run it from inside the bundle, or symlink to that path, and never copy the binary out. A symlink keeps the bundle intact, and one in /usr/local/bin answered --version and list normally on the test host:

sudo mkdir -p /usr/local/bin
sudo ln -sf "$PWD/tart.app/Contents/MacOS/tart" /usr/local/bin/tart

Keep the bundle where it is after linking. The bundle carries an embedded.provisionprofile that grants the virtualization entitlement, and a copy of the bare binary on the test host died with exit code 137 (SIGKILL) on --version and on list without printing a thing.

Tart keeps everything under ~/.tart/: runnable VMs in ~/.tart/vms/ and pulled images in ~/.tart/cache/OCIs/. That’s the directory to put on a big volume, or to point elsewhere with TART_HOME.

Check image sizes before you pull

Tart images are OCI artifacts, so you can read their size from the registry before you commit 26 GB of bandwidth. This works from any machine with curl and jq, and the capture below came from an Ubuntu 26.04 box, not the Mac. Set the image, fetch an anonymous pull token from GHCR, then sum the manifest’s layers.

IMAGE=cirruslabs/macos-tahoe-base
TOKEN=$(curl -s "https://ghcr.io/token?scope=repository:${IMAGE}:pull" | jq -r .token)
curl -s -H "Authorization: Bearer ${TOKEN}" -H "Accept: application/vnd.oci.image.manifest.v1+json" "https://ghcr.io/v2/${IMAGE}/manifests/latest" | jq -c '{layers: (.layers|length), compressed_gb: ((([.layers[].size]|add)/1e9*100|round)/100), disk_gb: ((.annotations["org.cirruslabs.tart.uncompressed-disk-size"]|tonumber)/1e9), uploaded: .annotations["org.cirruslabs.tart.upload-time"]}'

Running that for six images on 29 October 2026 returned the numbers below. The disk_gb value comes from Tart’s own org.cirruslabs.tart.uncompressed-disk-size annotation, which is what lands in ~/.tart/vms/ after the clone:

== cirruslabs/macos-tahoe-base:latest
{"layers":96,"compressed_gb":26.39,"disk_gb":50,"uploaded":"2026-10-15T14:20:59Z"}
== cirruslabs/macos-sequoia-base:latest
{"layers":96,"compressed_gb":25.46,"disk_gb":50,"uploaded":"2026-10-03T10:07:25Z"}
== cirruslabs/macos-tahoe-xcode:latest
{"layers":263,"compressed_gb":69.09,"disk_gb":140,"uploaded":"2026-10-21T09:04:51Z"}
== cirruslabs/ubuntu:latest
{"layers":40,"compressed_gb":3.06,"disk_gb":20,"uploaded":"2026-10-24T08:39:26Z"}
== cirruslabs/debian:latest
{"layers":40,"compressed_gb":0.62,"disk_gb":20,"uploaded":"2026-10-24T08:40:23Z"}
== cirruslabs/fedora:latest
{"layers":40,"compressed_gb":0.8,"disk_gb":20,"uploaded":"2026-05-30T22:23:29Z"}

Watch the Xcode image if you plan to use it. It is 69 GB to download and presents a 140 GB disk to the guest, so check free space before you pull it onto a 256 GB Mac. Debian is the lightest guest at 0.62 GB, and the Fedora latest tag has not been rebuilt since May 2026. The terminal below shows the exact command run for the Tahoe base and Ubuntu images.

Tart VM image sizes read from the GHCR registry manifests for the macOS Tahoe base and Ubuntu images

Download speed is the other half of the budget. On the EC2 host in eu-west-1, cloning the 26.4 GB Tahoe base from GHCR took 4 minutes 59 seconds end to end, and the 3.1 GB Ubuntu image took 34 seconds. A home connection will take far longer, so pull the base image once and clone locally from then on.

Run a macOS VM from a base image

Cirrus Labs publishes three flavours per macOS release: vanilla (a clean install), base and xcode. Start with base, because tart exec only works where the Tart guest agent is installed, and the base image ships it. These are the steps:

  1. Clone the image into a local VM named tahoe-base. The first clone downloads it into the OCI cache.
  2. Size it with tart set while it’s stopped. Memory is in megabytes.
  3. Boot it. On a Mac with a screen, drop --no-graphics and a window opens with the desktop.
tart clone ghcr.io/cirruslabs/macos-tahoe-base:latest tahoe-base
tart set tahoe-base --cpu 4 --memory 8192
tart run --no-graphics tahoe-base &

Check the settings took with tart get tahoe-base. The image defaults to a 1024×768 display and a raw 50 GB disk, of which about 31 GB is actually used:

OS     CPU Memory Disk  DiskFormat Size  Display  MACAddress        State
darwin 4   8192   50 GB raw        31 GB 1024x768 72:76:f4:5e:d8:14 stopped

If that tart run dies at once with a “security error”, you’re on a host nobody has logged in to through the GUI, which covers most cloud Macs and fresh Mac minis. Jump to the keychain fix below, then come back. With the keychain sorted, the guest had an IP 8 seconds after boot. The login is admin with password admin on every Cirrus Labs image, for the GUI and for SSH, so change it before the VM touches anything you care about. Run three checks over SSH:

ssh admin@$(tart ip tahoe-base --wait 60) 'whoami; hostname; sw_vers -productVersion'

With --wait 60, tart ip keeps polling while the guest boots instead of failing on the first try. Guests land on the 192.168.64.0/24 NAT network, and the first one got 192.168.64.2. After the password prompt, the three commands confirm where you are:

([email protected]) Password:
admin
ip-192-168-64-2.eu-west-1.compute.internal
26.6.2

Those three lines are whoami, hostname and sw_vers -productVersion. The guest picked up the host’s DHCP search domain, and the image is macOS 26.6.2 even though the host ran 26.7, because the base image is rebuilt on its own schedule. Want the desktop on a headless host? Start the VM with tart run --vnc-experimental tahoe-base instead. It prints a vnc:// URL with a one-time password on 127.0.0.1, which you can reach through an SSH tunnel. That’s how this capture of the guest was taken:

macOS Tahoe guest running in a Tart VM, Terminal showing sw_vers 26.6.2 on arm64, captured over Tart's VNC server

The “App Background Activity” notice names Cirrus Labs software in the image, and the guest needed no answer to it. Stop the VM cleanly with tart stop tahoe-base, and check what’s on disk with tart list.

Fix the headless keychain errors

You’ll hit these on Mac minis and cloud Macs that nobody has logged in to. They come from the macOS keychain, not from Tart, which is why reinstalling Tart doesn’t help. On the test host, a Linux guest booted fine with the keychain locked, so only macOS guests are affected.

Error: “SecKeyCreateRandomKey_ios failed” or “Failed to create new HostKey”

From macOS 15 Sequoia, Virtualization.framework needs a login.keychain to start a macOS VM. Apple doesn’t document that, and the Tart FAQ records it with three messages you may see instead:

  • SecKeyCreateRandomKey_ios failed
  • Failed to generate keypair
  • Interaction is not allowed with the Security Server

On the macOS 26.7 host, where security default-keychain reported “A default keychain could not be found”, the failure read differently. You’ll find the only clue, “Failed to create new HostKey”, three errors deep:

Error Domain=VZErrorDomain Code=-9 "The virtual machine encountered a security error." UserInfo={NSLocalizedFailure=Unable to access security information., NSLocalizedFailureReason=The virtual machine encountered a security error., NSUnderlyingError=0x942c48f90 {Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument" UserInfo={NSLocalizedFailureReason=Failed to get current host key., NSUnderlyingError=0x942c48f60 {Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument" UserInfo={NSLocalizedFailureReason=Failed to create new HostKey., NSUnderlyingError=0x942c48f30 {Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument" UserInfo=0x9431fa840 (not displayed)}}}}}}

All of these mean the same thing, so one fix covers them. The durable route is logging in once over Screen Sharing, which creates the keychain, then turning on automatic login. On a host you only reach over SSH, create and unlock a keychain with an empty password, and use a real password with -p if other people can reach that account:

security create-keychain -p '' login.keychain
security unlock-keychain -p '' login.keychain

On macOS 26.7 you only need those two lines. Afterwards security default-keychain pointed at the new keychain, and the Tahoe guest booted and answered tart exec:

    "/Users/ec2-user/Library/Keychains/login.keychain-db"

The FAQ adds a third command, security login-keychain -s login.keychain. It exited 1 with no message on this host, and a repeat from a deleted keychain booted fine without it. The keychain doesn’t have to stay unlocked, either. A keychain made this way reports timeout=300s in security show-keychain-info, but guests kept booting after a forced 30 second auto-lock and after security lock-keychain. If a tart run ever sits silent, unlock the keychain in that session and try again before you dig further. The FAQ also warns that this route “might stop working at some point in the future”, so re-check it after each macOS upgrade on the host.

Error: “Keychain failed to add item: User interaction is not allowed”

This one hits tart login over SSH, because Tart stores registry credentials in the keychain and a locked keychain can’t take them. The FAQ lists it as status -25308, the code behind “User interaction is not allowed”. With the keychain locked, logging in to ECR failed like this:

Error: Failed(message: "Keychain failed to add item: User interaction is not allowed.")

Unlock the keychain in that session first, or skip it completely by passing credentials through TART_REGISTRY_USERNAME and TART_REGISTRY_PASSWORD, which is what the registry section below does.

Run a Linux VM with shared folders

Linux guests are the cheap ones: 0.6 to 3 GB to download and a 20 GB disk. That disk fills fast, so grow it before you boot the first time. The Ubuntu image ships cloud-initramfs-growroot, so the root filesystem grows to match on the next boot without any work inside the guest.

tart clone ghcr.io/cirruslabs/ubuntu:latest ubuntu
tart set ubuntu --disk-size 50

The --dir flag shares a host folder over virtio-fs, in the form [name:]path[:options]. Add :ro when the guest should only read it, which is what you want for source code you don’t trust a build to modify:

mkdir -p ~/src/project
tart run --no-graphics --dir=project:~/src/project:ro ubuntu &

A macOS guest mounts shares automatically under /Volumes/My Shared Files. Linux doesn’t, so SSH in with ssh admin@$(tart ip ubuntu --wait 60) (same admin/admin login) and mount the default com.apple.virtio-fs.automount tag yourself:

sudo mkdir -p /mnt/shared
sudo mount -t virtiofs com.apple.virtio-fs.automount /mnt/shared
df -h /
ls -la /mnt/shared/project

The root disk already reports 45G after the resize, and the share shows up as a subfolder named after its name: prefix:

Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        45G  4.2G   40G  10% /
total 4
drwxr-xr-x 3 admin admin 96 Oct 30 07:07 .
-rw-r--r-- 1 admin admin 20 Oct 30 07:07 README.md

The image is Ubuntu 24.04.5 LTS on a 7.0 kernel, not 26.04, so check /etc/os-release before you assume a package version. The :ro flag holds, too: touch /mnt/shared/project/x fails with “Operation not permitted” even for the file owner. To keep the mount across reboots, append it to the guest’s fstab, and run sync before you stop the VM:

echo "com.apple.virtio-fs.automount /mnt/shared virtiofs rw,relatime,nofail 0 0" | sudo tee -a /etc/fstab
sync

That sync came from a lost edit. tart stop ubuntu returned in 0.15 seconds on the Ubuntu guest, and the first fstab edit, made without it, left the share unmounted after the restart. With sync the line was still in fstab after the stop, and the share mounted by itself on the next boot. Keep the nofail option as well. Booting once without --dir, with that line in fstab, still gave the guest an IP and a working tart exec. systemctl is-system-running said degraded and the share was simply unmounted, which beats a guest that neither SSH nor tart ip can reach.

Pass --dir more than once for several shares, and give each a different name. If all you want is a disposable Ubuntu shell rather than a full VM toolchain, Multipass on macOS is the lighter option.

Turn on nested virtualization

Nested virtualization lets a Linux guest run KVM, which is what you need for Firecracker, minikube with the KVM driver, or Android emulators inside the VM. It’s off by default, and it only works on an M3 or newer Mac with macOS 15 or later, so restart the Ubuntu VM with --nested added to the same run line:

tart stop ubuntu
tart run --no-graphics --nested --dir=project:~/src/project:ro ubuntu &

Inside the guest, check with kvm-ok from the cpu-checker package:

sudo apt-get update
sudo apt-get install -y cpu-checker
sudo kvm-ok
sudo dmesg | grep -iE "kvm|el2" | head -4

On the M4 host, the guest kernel booted at EL2 and brought up KVM with nested-virt trap handlers, which is the line that proves the flag reached the guest:

INFO: /dev/kvm exists
KVM acceleration can be used
[    0.003530] CPU: All CPU(s) started at EL2
[    0.034571] kvm [1]: nv: 570 coarse grained trap handlers
[    0.034592] kvm [1]: nv: 664 fine grained trap handlers
[    0.034623] kvm [1]: IPA Size Limit: 40 bits

Boot the same VM without --nested and ls -l /dev/kvm answers “No such file or directory”, so the flag is per run, not a setting stored with the VM. Asking for it on a macOS guest fails before boot with macOS virtual machines do not support nested virtualization. On a host that doesn’t qualify, the Tart source has two more validation messages. “Nested virtualization is supported on hosts starting with macOS 15 (Sequoia), and later” means upgrade macOS. “Nested virtualization is available for Mac with the M3 chip, and later” means an M1 or M2, and no setting fixes that.

Push and pull VMs through a registry

Any OCI registry stores Tart VMs, including GHCR, Harbor, ECR or a plain registry:2. That’s how your team shares one golden image instead of rebuilding it on every Mac. The test pushed the Ubuntu VM to Amazon ECR, with the token in environment variables so no keychain was involved. Replace 123456789012 with your account ID and the lab’s cfg-lab-1332 prefix with your own, and create the repository first with aws ecr create-repository --repository-name cfg-lab-1332/tart-ubuntu:

export TART_REGISTRY_USERNAME=AWS
export TART_REGISTRY_PASSWORD=$(aws ecr get-login-password --region eu-west-1)
tart push ubuntu 123456789012.dkr.ecr.eu-west-1.amazonaws.com/cfg-lab-1332/tart-ubuntu:2026-10

Tart uploads the config, the disk and the NVRAM as separate parts. The 50 GB Ubuntu VM, 5 GB of it used, went up in 39 seconds:

pushing ubuntu to 123456789012.dkr.ecr.eu-west-1.amazonaws.com/cfg-lab-1332/tart-ubuntu:2026-10...
pushing config...
pushing disk... this will take a while...
pushing NVRAM...
pushing manifest for 2026-10...

Pull it on the next Mac with the same variables set, then clone it into a runnable VM:

tart pull 123456789012.dkr.ecr.eu-west-1.amazonaws.com/cfg-lab-1332/tart-ubuntu:2026-10
tart clone 123456789012.dkr.ecr.eu-west-1.amazonaws.com/cfg-lab-1332/tart-ubuntu:2026-10 ubuntu-golden

Inside the same region, the pull fetched 3.2 GB compressed in 14 seconds, the clone after it took 0.02 seconds, and tart list then shows the image twice under OCI, once by tag and once by sha256 digest. Tart only runs images that Tart pushed, so a Docker image under the same name can’t be pulled. Watch the cache as well. When the volume runs short, tart clone and tart pull evict the least recently used cached images, reclaiming up to 100 GB per run by default. Change that amount with --prune-limit, or run export TART_NO_AUTO_PRUNE=1 to turn automatic pruning off.

Sandbox AI agents with tart exec

Coding agents like Claude Code run shell commands on whatever machine they sit on, and a fresh VM per task is a strong wall around that. tart exec runs a command in a running VM through the guest agent, with no SSH keys, passwords or IP lookups. It needs macOS 14 on the host and the agent in the guest. Both the Tahoe base and the Ubuntu image answered it:

tart exec tahoe-base sw_vers
tart exec tahoe-base sh -c "exit 3"; echo "EXIT=$?"
tart exec ubuntu uname -a

The guest’s exit code comes back to the host unchanged, which is what lets a script branch on a failed task:

ProductName:		macOS
ProductVersion:		26.6.2
BuildVersion:		25G83
EXIT=3
Linux ubuntu 7.0.0-38-generic #38~24.04.4-Ubuntu SMP PREEMPT_DYNAMIC Mon Sep 14 17:12:37 UTC 2 aarch64 aarch64 aarch64 GNU/Linux

Add -i to pass your stdin through and -t for a PTY, so tart exec -i -t tahoe-base zsh gives you an interactive shell. Against a stopped VM it fails with VM "ubuntu" is not running and exit code 2. For an agent, wrap that in a per-task lifecycle:

  1. Clone a fresh VM per task from the cached base image, named after the task.
  2. Boot it headless with the repo shared read-only and a writable output folder.
  3. Wait until the guest agent answers and the share is mounted, and give up fast if the VM died.
  4. Copy the repo into the VM, run the task and keep its exit code.
  5. Stop and delete the VM, whatever the result.

The script below does those five steps for one task, and it’s the exact file that ran on the test host. Swap the make test line for your agent’s own command. Open a new file for it:

vim ~/agent-task.sh

Paste the script. The trap cleans the VM up on every exit path, and the wait loop checks whether the background tart run is still alive, so a VM that never boots ends the script in seconds instead of two minutes:

#!/bin/bash
set -euo pipefail
IMAGE="ghcr.io/cirruslabs/macos-tahoe-base:latest"
REPO="$(cd "${1:-.}" && pwd)"
TASK="agent-$(date +%s)"
OUT="${HOME}/agent-out/${TASK}"
SHARE="/Volumes/My Shared Files"

cleanup() {
  tart stop "${TASK}" >/dev/null 2>&1 || true
  tart delete "${TASK}" >/dev/null 2>&1 || true
}
trap cleanup EXIT

mkdir -p "${OUT}"
tart clone "${IMAGE}" "${TASK}"
tart run --no-graphics --dir=repo:"${REPO}":ro --dir=out:"${OUT}" "${TASK}" >"${OUT}/tart-run.log" 2>&1 &
RUN_PID=$!

ready=0
for i in $(seq 1 60); do
  if tart exec "${TASK}" test -d "${SHARE}/repo"; then ready=1; break; fi
  kill -0 "${RUN_PID}" 2>/dev/null || break
  sleep 2
done 2>/dev/null
if [ "${ready}" -ne 1 ]; then
  echo "${TASK}: guest agent or shared folder never came up, see ${OUT}/tart-run.log" >&2
  exit 1
fi

tart exec "${TASK}" cp -R "${SHARE}/repo" /Users/admin/work
set +e
tart exec "${TASK}" zsh -lc "cd /Users/admin/work && make test" >"${OUT}/result.log" 2>&1
rc=$?
set -e
echo "${TASK}: exit ${rc}, log in ${OUT}/result.log"
exit "${rc}"

Make it executable and run it from the repo you want tested. The test repo held a Makefile whose test target printed the guest’s macOS version and architecture, counted the files, and tried to write into the read-only share:

chmod +x ~/agent-task.sh
cd ~/demo-repo
time ~/agent-task.sh .

Clone to delete, the whole loop took 39 seconds. The clone itself took 0.02 seconds, because cloning from the local OCI cache is a copy-on-write APFS clone, not a 26 GB copy:

agent-1793344225: exit 0, log in /Users/ec2-user/agent-out/agent-1793344225/result.log
~/agent-task.sh .  0.12s user 0.11s system 0% cpu 39.203 total

The result.log on the host held 26.6.2, arm64, files: 2 and repo-read-only, so the agent ran in the guest and couldn’t touch your working tree. Afterwards tart list showed only tahoe-base and ubuntu, with no leftover task VM. Treat the VM’s network as the remaining hole. Default NAT networking doesn’t filter where the guest connects, while --net-softnet on a macOS 26 host limits it to globally routable addresses and the gateway.

Error: “The number of VMs exceeds the system limit”

Plan for two macOS tasks at a time per Mac. With tahoe-base and a second clone running, a third tart run stopped at once with this:

The number of VMs exceeds the system limit (other running VMs: t2, tahoe-base)

That matches section 2B(iii) of the macOS Tahoe licence, which allows two extra macOS instances in virtual machines per Mac, and only for software development, testing during development, macOS Server or personal non-commercial use. Running the agent script while two macOS VMs were up proved the fail-fast path: it exited 1 after 2.1 seconds with “guest agent or shared folder never came up”, the same error sat in its tart-run.log, and the trap deleted the half-made VM. Linux guests aren’t part of that count. An Ubuntu VM booted, reached running and got 192.168.64.4 next to the two running macOS guests, so a Linux image is how you go past two tasks on one Mac.

Keep this Tart command card

Keep these commands handy; this guide ran every one on Tart 2.41.1. Run tart <command> --help for the full flag list on your build:

TaskCommand
Install (macOS 26+)brew trust openai/tools, then brew install openai/tools/tart
Install (macOS 13 to 15)curl -LO https://github.com/openai/tart/releases/latest/download/tart.tar.gz, tar -xzf tart.tar.gz, then run ./tart.app/Contents/MacOS/tart
Headless keychain fixsecurity create-keychain -p '' login.keychain, security unlock-keychain -p '' login.keychain
Get a macOS VMtart clone ghcr.io/cirruslabs/macos-tahoe-base:latest tahoe-base
Get a Linux VMtart clone ghcr.io/cirruslabs/ubuntu:latest ubuntu
Resize CPU, RAM, disktart set ubuntu --cpu 4 --memory 8192 --disk-size 50
Boot headlesstart run --no-graphics ubuntu &
See the desktop on a headless hosttart run --vnc-experimental tahoe-base
Share a folder read-onlytart run --dir=project:~/src/project:ro ubuntu
Nested KVM (M3+, Linux)tart run --nested ubuntu
Guest IP and SSHssh admin@$(tart ip ubuntu --wait 60)
Command without SSHtart exec -i -t tahoe-base zsh
Registry without the keychainTART_REGISTRY_USERNAME and TART_REGISTRY_PASSWORD, then tart push or tart pull
Stop, list, deletetart stop ubuntu, tart list, tart delete ubuntu

If your host is a Linux or Proxmox box rather than a Mac, Tart won’t run there. Running macOS on KVM with QEMU or as a macOS guest on Proxmox VE covers that side. And if you’re still picking the Mac for this, the Macs that suit local AI work list sorts them by unified memory, which is what decides how many VMs fit at once.

Keep reading

Install KVM and Virt-Manager on Arch Linux Virtualization Install KVM and Virt-Manager on Arch Linux Install KVM on Debian 13 / Debian 12: Complete Guide KVM Install KVM on Debian 13 / Debian 12: Complete Guide Install VirtIO Drivers on Windows 11 and Server 2025: ISO and MSI KVM Install VirtIO Drivers on Windows 11 and Server 2025: ISO and MSI macOS Keyboard Shortcuts Cheat Sheet (Golden Gate and Tahoe) Cheat Sheets macOS Keyboard Shortcuts Cheat Sheet (Golden Gate and Tahoe) How to Install KVM on Fedora 45 / 44 with virt-manager Fedora How to Install KVM on Fedora 45 / 44 with virt-manager Build a Proxmox Server: Homelab Virtualization Host Proxmox Build a Proxmox Server: Homelab Virtualization Host

Leave a Comment

Press ESC to close