Run in a VM, over SSH, or in a sandbox
Run Cua Driver inside a macOS Lume VM, drive a Windows desktop over SSH, or test a driver build in a disposable Linux sandbox.
Cua Driver always runs on the machine whose desktop it drives. To reach a VM or a remote computer, run the driver there and carry MCP over SSH. To test a driver build, use a disposable Linux sandbox.
macOS Lume VM#
On an Apple Silicon host with Lume and jq, pull the
published Tahoe image (150 GB sparse disk; login and password lume):
IMAGE=macos-tahoe-cua:26.5.2
VM=cua-driver-dev-26.5.2
lume pull "$IMAGE" "$VM"
lume run "$VM"The image has SIP disabled to match the maintainer test base; that grants no permissions, and Cua Driver works with SIP on too. In Terminal inside the VM display, install and grant consent:
/bin/bash -c "$(curl -fsSL https://cua.ai/driver/install.sh)"/Users/lume/.local/bin/cua-driver permissions grant
/Users/lume/.local/bin/cua-driver list_apps '{}'
/Users/lume/.local/bin/cua-driver call get_desktop_state '{}' > /tmp/desktop.jsonApprove three kinds of prompt: Accessibility and Screen Recording for
CuaDriver, CuaDriver controlling System Events (from list_apps), and Tahoe's
separate direct screen capture (from get_desktop_state). Rerun the last two
commands; they must finish without prompts. Then confirm the daemon's grants:
/Users/lume/.local/bin/cua-driver permissions status --jsonExpect accessibility: true, screen_recording: true, and
source.attribution: "driver-daemon". unknown means no app-owned daemon
answered: run cua-driver permissions grant, which starts it.
Run an agent inside the VM with the usual
mcp-config, or from the host over
SSH. Copy only your public key into the guest, then register the SSH command
as a stdio server:
VM_IP="$(lume get "$VM" --format json | jq -r '.[0].ipAddress')"
cat ~/.ssh/id_ed25519.pub | ssh "lume@${VM_IP}" \
'umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys'
codex mcp add cua-driver-vm -- \
ssh -T -o BatchMode=yes "lume@${VM_IP}" /Users/lume/.local/bin/cua-driver mcpThe remote cua-driver mcp proxies to the daemon in the logged-in guest
session, so screenshots and input keep the guest app's permission identity.
Do initial consent and GUI checks in the VM display, not over SSH.
Save a reusable copy while stopped. Keep it local, with no tokens, private keys, or personal files inside:
lume stop "$VM"
lume clone "$VM" "${VM}-backup"
lume lsA clone keeps its grants only while /Applications/CuaDriver.app keeps the
release signature; an ad-hoc source build gets a new permission identity.
Maintainers running the driver's macOS E2E suite in Lume follow
libs/cua-driver/tests/runners/macos-lume/README.md in the repository.
Windows over SSH#
Windows OpenSSH runs in Session 0, which has no desktop, so window tools
return empty results there. cua-driver doctor reports
running in Session 0 (services). Run the daemon in your interactive session
and let the SSH side proxy to it over the named pipe:
Session 1+ (RDP or console) cua-driver serve (autostart task)
▲ \\.\pipe\cua-driver
Session 0 (SSH) cua-driver call … / cua-driver mcp --socket \\.\pipe\cua-driver1. From RDP or the local console, register and start the task:
cua-driver autostart enable
cua-driver autostart kickquery session must show your user as Active or Disc. If you have never
signed in with RDP, do it once.
Sign in through SSH with the same Windows account that owns the interactive daemon. The named
pipe is private to that account; another account can see the pipe but gets Access is denied.
Run whoami /user in both sessions and compare the SIDs, which must match exactly.
2. From SSH, check the daemon and call tools:
cua-driver status
# Cua Driver daemon is running
# socket: \\.\pipe\cua-driver
cua-driver call list_appsIf the endpoint exists but belongs to another Windows account, status reports
it as not reachable and tells you to compare the two account SIDs.
3. Register the agent with an explicit socket:
claude mcp add --transport stdio cua-driver -- cua-driver.exe mcp --socket \\.\pipe\cua-driverBare cua-driver mcp owns its own runtime on Windows and fails in Session 0. Always pass
--socket. If the interactive daemon is gone, MCP startup fails instead of acting from the
wrong session.
Still empty? Check that whoami /user shows the same SID over SSH and on the
desktop, that cua-driver --version matches over SSH, that
cua-driver autostart status shows the task, and that cua-driver doctor from
RDP reports session N has an attached interactive desktop.
Test a driver build in a sandbox#
A Cua sandbox gives a driver build a real Linux desktop (Xvfb, XFCE,
AT-SPI) that you can throw away, on your machine or on a CI runner. Put your
build into it with --overlay, then check that it is the one running. This is
for testing Cua Driver itself. Inside a sandbox you do not install the driver
to use it: the sandbox's guest service bundles its tools.
On your machine#
Build the driver for Linux, then create a sandbox with it:
cua sb create linux --name drv --wait desktop \
--overlay cua-driver=./target/release/cua-driverThe overlay replaces /usr/local/bin/cua-driver in the guest, records its
sha256 and stops any older copy still running. Check it:
cua doctor local:drv --no-host --only build --expect cua-driver=git:<commit sha>
cua sb exec local:drv cua-driver doctor --jsoncua-driver doctor --json and the health_report tool report the running
build under build:
git_sha: the commit it was built from;exe_sha256: the executable that actually runs.
A process started before the file was replaced still reports its old hash.
Run your tests against the desktop, then delete the sandbox:
cua sb cp ./tests/smoke.sh drv:/tmp/smoke.sh
cua sb shell drv "set -a; . /run/cua-desktop/desktop.env; set +a; bash /tmp/smoke.sh"
cua sb rm local:drv --forceThe driver inside cua-spacesd#
cua-spacesd links the driver core and serves its tools on /mcp. To test
driver changes through spacesd, inject a cua-spacesd built from the same
commit. The overlay restarts the daemon and waits until the new process runs
it:
cua sb overlay drv cua-spacesd=./target/release/cua-spacesd
cua doctor local:drv --no-host --only build \
--expect cua-spacesd=git:<commit sha> --expect cua-driver=git:<commit sha>With both injected, cua-driver=git: must hold for the linked driver and the
standalone binary.
In GitHub Actions#
- uses: trycua/cua/.github/actions/cua-sandbox@main
with:
image: linux
overlay: cua-driver=libs/cua-driver/rust/target/release/cua-driver
expect: cua-driver=git:${{ github.sha }}
guest-setup: apt-get update -qq && apt-get install -y -qq jq ffmpeg
run: |
cua sb cp scripts/ci/linux/preflight-rust-e2e.sh "$CUA_SANDBOX:/tmp/preflight.sh"
cua sb shell "$CUA_SANDBOX" "set -a; . /run/cua-desktop/desktop.env; set +a;
export CUA_E2E_SOURCE_SHA=${{ github.sha }} CUA_TEST_DRIVER_BIN=/usr/local/bin/cua-driver;
bash /tmp/preflight.sh"This is how the repository's own Linux driver preflight runs. The preflight
fails with stale driver when the driver's embedded commit differs from
CUA_E2E_SOURCE_SHA.
On the runner, the action puts a PATH first where cua-driver is the
injected build, and cua-spacesd is a stub that fails. A copy installed on
the runner is never picked up.
See Test in CI with Cua sandboxes for every input and output.