Provision a macOS cua sandbox VM with lume
Turns a lume-managed macOS guest on Apple Silicon into a working
cua computer-use sandbox: cua-driver running under a LaunchAgent, Accessibility
and Screen Recording granted, and a verified screen capture at the resolution you asked for.
On macos-tahoe-cua the run is unattended — Accessibility, Screen Recording, and full-display
capture all come up without anyone at the VM's screen. That hinges entirely on the image; see the
decision rule below. One leftover needs a single click: Tahoe raises a separate "direct capture"
consent that does not live in TCC.db and so cannot be scripted — see
REFERENCE.md §3. Capture works without answering it; the dialog just sits on the
desktop, where it will get in the way of UI automation until dismissed.
Validated end to end on a Mac Studio M3 Ultra / macOS 26.6.1 host, guest macOS 26.4 (25E246), lume 0.5.3, cua-driver 0.20.0. Every trap the scripts work around is documented in REFERENCE.md.
Prerequisites
- Apple Silicon Mac;
lume0.5.3+ andjqonPATH - ~50 GB free per sandbox: lume's cache keeps the ~21 GB image in addition to the ~26 GB VM
- Only 2 macOS guests may run at once (XNU limit), counting any VM already running
Decision rule: which image
This is the only choice that matters, because it decides whether provisioning can be unattended.
macos-tahoe-cua:latest |
macos-tahoe-vanilla:latest |
|
|---|---|---|
| SIP | disabled | enabled |
| Autologin (real Aqua session) | yes, first boot | no — boots to the login window |
| Xcode Command Line Tools | yes | no |
CuaDriver.app / cua-computer-server |
no — you still install them | no |
| TCC grants pre-applied | no | no |
| Default disk | 150 GB | 100 GB |
| TCC grants scriptable? | yes (SIP off ⇒ TCC.db writable) | no — one manual sitting |
Use macos-tahoe-cua:latest unless you have a specific reason not to. "Pre-provisioned" means
the environment, not the cua software — the driver install is still yours to run. What it actually
buys you is SIP-off and autologin, and SIP-off is what removes the manual step.
Quick start
VM=my-sandbox SHARED_DIR="$HOME/work/sandbox" bash scripts/provision.sh
VM is required. Everything else is optional:
| Variable | Default | Notes |
|---|---|---|
VM |
— | required; the lume VM name |
IMAGE |
macos-tahoe-cua:latest |
ignored when GOLDEN is set |
GOLDEN |
— | clone this VM instead of pulling (APFS CoW, ~2 s) |
SHARED_DIR |
— | host dir to share :rw; omitted entirely when unset |
CPU / MEMORY |
8 / 16GB |
|
DISK_SIZE |
— | unset means don't resize; increase-only |
DISPLAY_RES |
1920x1080 |
applied inside the guest, see traps |
INSTALL_COMPUTER_SERVER |
0 |
1 also installs Path B (HTTP API) |
INSTALL_TERMINFO |
1 |
export the host's terminfo into the guest user's ~/.terminfo |
TERMINFO_TERMS |
$TERM |
space-separated entries to export; skips ones the guest has |
--recreate destroys an existing VM of that name first.
What provision.sh does
- Acquire — clone from
GOLDENif set, else pullIMAGE. Skipped if the VM already exists. - Size —
lume setwith the VM stopped; resizes only when it's a genuine increase. - Boot, wait for IP and SSH.
- Check
/dev/console— the one precondition everything rests on. If it's owned byrootthere is no GUI session; the script applieslume setup --unattended tahoeand reboots. - Install
cua-driverand a LaunchAgent pointing at the in-bundle binary. - Grant TCC directly in
TCC.dbwhencsrutilreports SIP disabled; otherwise print the manual instructions. - Export terminfo — compiles the host's
$TERMentry into the guest user's~/.terminfo, which ncurses already searches, so SSHing in from Ghostty/kitty/WezTerm doesn't hit "unknown terminal type". - Set guest resolution with
displayplacer. - Verify — SIP,
doctor, permissions booleans, and a realget_desktop_statecapture.
It is idempotent: re-running is also the health check.
Verify
lume ssh "$VM" "stat -f '%Su' /dev/console" # -> lume (root = no GUI session)
lume ssh "$VM" "/Applications/CuaDriver.app/Contents/MacOS/cua-driver permissions status --json"
# -> "accessibility": true, "screen_recording": true
lume ssh "$VM" "/Applications/CuaDriver.app/Contents/MacOS/cua-driver call get_desktop_state '{}'" \
| grep -E 'screen_width|screenshot_png_b64'
Booleans flipping is not proof — only a returned screenshot_png_b64 is.
Snapshot a golden image
lume clone is APFS copy-on-write: ~2 seconds, ~0 bytes. Do this once and never re-download:
lume stop "$VM" && lume clone "$VM" "${VM}-golden"
GOLDEN="${VM}-golden" VM="$VM" bash scripts/provision.sh --recreate # future rebuilds
lume's cache keeps only the current manifest per image, so a :latest that moves upstream costs
a full re-download even with caching on. A golden clone is the only durable protection.
Register the MCP server
Never bake the VM's IP into the registration — a NAT guest's IP drifts across reboots and the MCP server then fails silently. Install the wrapper, which re-resolves at connect time:
install -m 755 scripts/cua-mcp-wrapper.sh ~/.local/bin/cua-${VM}-mcp
claude mcp add --scope user cua-driver-vm -- ~/.local/bin/cua-${VM}-mcp
The wrapper reads CUA_VM (default: the name baked in at install time). stdout is the JSON-RPC
channel — any diagnostic printed there breaks the transport.
Traps worth knowing up front
- SSH working is not evidence of a GUI session. A vanilla image has working SSH at the login
window.
stat -f '%Su' /dev/consoleis the only reliable test —rootmeans no Aqua session, and thencua-drivercannot launch, LaunchAgents never load, and permissions readunknown. lume set --displaynever reaches the guest. It only offers the mode to the virtual GPU; macOS keeps rendering at its previous resolution and cua-driver screenshots at that.lume lswill confidently report a resolution the guest is not using.lume stopis unreliable — it can no-op silently or fail withCannot modify <vm>: the VM is running. Stop it first.fromstopitself. The script falls back to terminating the process.take_screenshotdoes not exist in cua-driver 0.20.0; capture isget_desktop_state. The old name fails withno reviewed risk classification, which reads like a TCC error but is the driver's own tool-manifest gate.- Don't rename the
lumeguest account. cua hardcodes/Users/lume, and every lume convenience command defaults to it. Change the password instead.
Full diagnosis of each, plus the Path B HTTP API and the version history, is in REFERENCE.md.