Installation
Install Cua Driver on macOS, Windows, or the Linux pre-release backend
Install Cua Driver with the command for your platform. macOS and Windows are the official install targets; Linux is available as a pre-release backend while platform testing is still in progress.
| Platform | Command |
|---|---|
| macOS | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)" |
| Linux pre-release | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)" |
| Windows | irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.ps1 | iex |
macOS / Linux pre-release
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)"Windows (PowerShell)
irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.ps1 | iexOn macOS, the installer drops CuaDriver.app into /Applications and symlinks the binary at ~/.local/bin/cua-driver. The bundle is signed under com.trycua.driver, so TCC grants survive every release.
On Linux, the pre-release backend installer downloads the release into ~/.cua-driver/packages/releases/, retargets ~/.cua-driver/packages/current, and symlinks ~/.local/bin/cua-driver.
Install home is ~/.cua-driver on every platform. Earlier cua-driver-rs releases (before
v0.2.16) put the Linux/macOS package home at ~/.cua-driver-rs; the release installer now writes
to ~/.cua-driver like the local installer and the runtime already do. Re-running the installer
over an older release auto-sweeps a stale ~/.cua-driver-rs, and also cleans up any prior
install-local dev build (*-local-* release dirs + the local signing-identity marker) under the
shared home so the downloaded release is the single authoritative install. The CUA_DRIVER_RS_HOME
override is still honored for back-compat.
Linux is pre-release. Linux artifacts and install paths are published for early testing, but Linux has not completed the same official release and platform validation pass as macOS and Windows. Expect gaps, report issues, and avoid treating Linux as a supported production target until it is promoted out of pre-release.
On Windows, the installer downloads the release into %USERPROFILE%\.cua-driver\packages\releases\, retargets the current junction, and exposes cua-driver.exe through %LOCALAPPDATA%\Programs\Cua\cua-driver\bin.
The install runs without sudo/admin. On macOS and Linux the CLI symlink lives under $HOME; on Windows the script updates the User-scope Path. If ~/.local/bin is not on your PATH, the shell installer detects your shell (zsh / bash / fish) and appends an export PATH=... line to the right rc file. Reload with source ~/.zshrc (or restart your terminal).
Power users: override the install path with --bin-dir (or CUA_DRIVER_RS_INSTALL_DIR). Use --no-modify-path if you'd rather edit your shell rc yourself.
/bin/bash -c "$(curl -fsSL .../install.sh)" -- --bin-dir=/usr/local/bin # legacy system path (needs sudo)
/bin/bash -c "$(curl -fsSL .../install.sh)" -- --no-modify-path # don't touch shell rcUpgrading from the retired Swift driver? The Rust implementation uses the same
/Applications/CuaDriver.app bundle id (com.trycua.driver). Existing Accessibility / Screen
Recording grants usually transfer; macOS may ask for a one-time re-grant because the binary cdhash
changed.
Both canonical installers live under libs/cua-driver/scripts/. The shell installer covers macOS and the Linux pre-release backend; install.ps1 covers Windows.
The Windows installer auto-detects host architecture (x64 / arm64) via [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture and downloads the matching cua-driver-rs-<v>-windows-{x86_64,arm64}.zip from GitHub Releases.
It runs without admin and without Developer Mode.
It also appends %LOCALAPPDATA%\Programs\Cua\cua-driver\bin to your User-scope Path so cua-driver --version resolves in any new PowerShell window. (v0.2.13 and earlier used Programs\trycua\cua-driver-rs\bin; the v0.2.14 installer auto-migrates the legacy layout.) The write is idempotent (re-running the installer never duplicates the entry) and falls back to printing manual [Environment]::SetEnvironmentVariable(...) instructions if the registry write is blocked (group policy, locked-down account). Pass -NoPathUpdate to suppress the auto-add — useful when you manage Path out-of-band via chezmoi / dotfiles / GPO:
# Fetch + invoke explicitly so we can pass the switch:
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.ps1))) -NoPathUpdateThe install layout is wired with NTFS directory junctions (IO_REPARSE_TAG_MOUNT_POINT), which any unprivileged user can create — no elevated symlink privilege required.
Versioned-dirs install layout
Linux and Windows installs land in a three-tier layout that makes upgrades and rollbacks an atomic retarget of one link, never a file overwrite. The macOS install intentionally stays on a different layout (/Applications/CuaDriver.app) — see the macOS asymmetry note below.
Linux
$CUA_DRIVER_RS_HOME/ (default: ~/.cua-driver)
packages/
releases/
0.2.0-x86_64-unknown-linux-gnu/cua-driver (real binary, immutable)
0.2.1-x86_64-unknown-linux-gnu/cua-driver (real binary, immutable)
current -> releases/0.2.1-x86_64-unknown-linux-gnu (symlink — active version)
current/cua-driver (= the binary inside the target dir)
$CUA_DRIVER_RS_INSTALL_DIR/cua-driver -> $CUA_DRIVER_RS_HOME/packages/current/cua-driver
(default: ~/.local/bin/cua-driver)Windows
%USERPROFILE%\.cua-driver\ ($env:CUA_DRIVER_RS_HOME)
packages\
releases\
0.2.14-x86_64-pc-windows-msvc\cua-driver.exe (real binary, immutable)
0.2.15-x86_64-pc-windows-msvc\cua-driver.exe (real binary, immutable)
current\ (directory junction → releases\0.2.15-...)
%LOCALAPPDATA%\Programs\Cua\cua-driver\bin\ ($env:CUA_DRIVER_RS_INSTALL_DIR)
(directory junction → packages\current)v0.2.13 and earlier used %LOCALAPPDATA%\Programs\trycua\cua-driver-rs\bin\ and %USERPROFILE%\.cua-driver-rs\. The v0.2.14+ installer detects the legacy layout and migrates transparently — stop any running daemon, delete the legacy task / junctions / package home, prune the legacy PATH entry, then install at the new path.
Both junctions are NTFS reparse points (IO_REPARSE_TAG_MOUNT_POINT), so the whole PATH-resolution chain bin\ → current\ → releases\<v>\cua-driver.exe is two filesystem hops transparently served from whichever release the inner junction currently points at.
Atomic upgrades and rollback
Both platforms ship every release into its own per-version directory under packages/releases/. Older versions are kept on disk so you can roll back without re-downloading. The active version is whichever directory packages/current points at.
Linux — roll back to a specific version
# The symlink target is RELATIVE to ~/.cua-driver/packages/, so just
# "releases/<v>-<target>" (no leading "../") — matches what install.sh writes.
ln -sfn releases/0.2.0-x86_64-unknown-linux-gnu ~/.cua-driver/packages/.current.tmp
mv -Tf ~/.cua-driver/packages/.current.tmp ~/.cua-driver/packages/current
cua-driver --version # → 0.2.0The mv -Tf is the rename(2) idiom: a concurrent cua-driver lookup sees either the old or new target, never an absent path.
Windows — roll back to a specific version
Re-run the install one-liner with $env:CUA_DRIVER_RS_VERSION pinned to the version you want active. The installer notices the per-version dir is already on disk, skips the download, and just retargets the current junction — so the round-trip is a single junction retarget under the hood.
$env:CUA_DRIVER_RS_VERSION = "0.2.0"
irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.ps1 | iex
cua-driver --version # → 0.2.0Clear $env:CUA_DRIVER_RS_VERSION and re-run to roll forward to the newest release.
Env-var overrides
| Env var | Default (Linux/macOS) | Default (Windows) | What it controls |
|---|---|---|---|
CUA_DRIVER_RS_VERSION | unset → use baked version | unset → use baked version | Pin a specific release (e.g. 0.2.0). |
CUA_DRIVER_RS_INSTALL_DIR | ~/.local/bin | %LOCALAPPDATA%\Programs\Cua\cua-driver\bin | The visible PATH-entry directory. On Windows this is itself a junction. |
CUA_DRIVER_RS_HOME | ~/.cua-driver | %USERPROFILE%\.cua-driver | Package home — holds packages/releases/<v>/ and packages/current. (Renamed from ~/.cua-driver-rs in v0.2.16; a stale legacy dir is auto-swept on the next install.) |
CUA_DRIVER_RS_NO_MODIFY_PATH (Linux/macOS only) | 0 | use -NoPathUpdate switch on install.ps1 | Skip the PATH append. On Windows the installer appends %LOCALAPPDATA%\Programs\Cua\cua-driver\bin to the User-scope Path by default; use the fetch+invoke form with -NoPathUpdate to opt out (see the Windows install section above for the exact one-liner). |
CUA_DRIVER_RS_KEEP_VERSIONS (Linux/Windows only) | 5 | 5 | Keep the N most recent per-version release dirs after install (0 disables GC). See the Old-version cleanup callout below for per-target and active-install invariants. |
How the installer picks a version. Resolution order is:
$CUA_DRIVER_RS_VERSION(Linux/macOS) or$env:CUA_DRIVER_RS_VERSION(Windows) — explicit pin.- A baked-in default version carried in the install script itself, auto-updated by the release pipeline after every published
cua-driver-rs-v*release. This is the common-case default forcurl … | bash/irm … | iexagainstmain: the version resolves locally with zero network calls to GitHub's API. - GitHub Releases API — fallback only, hit when the env override is absent and the baked line hasn't been updated yet (e.g. installing from a dev branch before its first release).
So an API outage, an unauthenticated-rate-limit (60 req/hr per IP), or a transient network blip never breaks a default install from main. Power users who want the very latest tip (rather than the baked default) can set CUA_DRIVER_RS_VERSION=<x.y.z> explicitly.
Why macOS uses a different layout. On macOS the install still drops CuaDriver.app into /Applications and symlinks ~/.local/bin/cua-driver into the bundle. The .app placement is the anchor for both TCC attribution (cdhash + bundle id) and LaunchServices (open -a CuaDriver); symlinking the .app from /Applications to a versioned dir under $CUA_DRIVER_RS_HOME would break both. The asymmetry is intentional — rollback on macOS = reinstall an older release tag with CUA_DRIVER_RS_VERSION=<x.y.z>.
Old-version cleanup (Linux / Windows). Every install drops the binary into a fresh packages/releases/<version>-<target>/ directory and retargets the current symlink/junction at it. To keep disk usage bounded, the installer prunes old per-version dirs after each install — by default it keeps the 5 most recent per platform target, deleting the rest.
# Keep more (or fewer) versions on disk
CUA_DRIVER_RS_KEEP_VERSIONS=10 bash install.sh # keep 10 most recent
CUA_DRIVER_RS_KEEP_VERSIONS=0 bash install.sh # disable GC entirelyTwo invariants worth knowing:
- The active install is always preserved, even if it would otherwise fall outside the keep window (e.g. you rolled back to an older version). Worst-case on-disk count is
keep + 1. - Per-target filtering — a multi-arch dev with both
aarch64-apple-darwinandx86_64-unknown-linux-gnudirectories under the same$CUA_DRIVER_RS_HOME(rare, but possible with shared/NFS homes) gets each target's history GC'd independently of the other.
The macOS install path is unaffected — /Applications/CuaDriver.app is an in-place replacement with no per-version accumulation.
Concurrent-install lockfile. Running two installs at the same time — e.g. clicking the one-liner while a CI script is also installing, or two terminals racing — used to risk a partially-installed state if both hit the atomic current swap simultaneously. The installer now takes a process-level lock per $CUA_DRIVER_RS_HOME so the second install waits for the first to finish:
==> another cua-driver-rs install is already in progress (lock at
~/.cua-driver/packages/.install.lock.d); waiting...If you see this, the safe action is to let it block — it polls every 1 second and proceeds the moment the holding install finishes. The lock entry stamps the holder's pid, start time, and invocation args so you can confirm what's running:
cat ~/.cua-driver/packages/.install.lock.d/info
# pid=43210
# started=2026-05-17T09:14:22Z
# argv=install.shGet-Content $env:CUA_DRIVER_RS_HOME\install.lock
# pid=43210
# started=2026-05-17T09:14:22.123Z
# invocation=install.ps1 -Release latestStale-lock recovery. If the holding process died without releasing (kill -9, host reboot mid-install, OOM), the lock would otherwise wedge every subsequent install. After 600 seconds of waiting, the installer assumes the lock is stale, force-releases it with a loud lock appears stale (>600s), forcing release log, and proceeds. So even in the pathological case the worst recovery is a 10-minute wait, not a manual rm -rf on internal-looking paths.
Lock locations:
| Platform | Lock |
|---|---|
| Linux | $CUA_DRIVER_RS_HOME/packages/.install.lock.d/ (mkdir-based mutex) |
| Windows | $env:CUA_DRIVER_RS_HOME\install.lock (FileShare::None mutex) |
Both primitives are unprivileged — no sudo, no admin elevation, no Developer Mode. The lock is released on every script exit path (success, error, Ctrl-C, exit).
Verify it worked
cua-driver --version
# cua-driver 0.1.0
cua-driver --help
# OVERVIEW: macOS Accessibility-driven computer-use agent — MCP stdio server.For a structured environment + install report on any platform, run:
cua-driver doctor
# [ok ] binary: cua-driver 0.2.1 (aarch64-macos)
# [ok ] install dir: /Users/you/.local/bin/cua-driver
# [ok ] home dir: /Users/you/.cua-driver (3 release dirs cached)
# [ok ] telemetry: enabled (install-id present)
# ...doctor runs version + install-layout + telemetry checks on every platform, plus platform-specific probes (legacy LaunchAgent / updater cleanup on macOS, UI Automation + interactive desktop session on Windows, X11/Wayland + AT-SPI on Linux). For the detailed macOS TCC + cdhash report, run cua-driver diagnose — doctor only surfaces a pointer to it. Pass --json for a scripting-friendly shape. Exit code is 0 when every probe is [ok] / [warn], non-zero only on [err] (e.g. the binary cannot resolve its own install dir).
Windows: interactive-session requirements
Window-driving tools — list_windows, click, type_text, screenshot, get_window_state — all bottom out in Win32 APIs (EnumWindows, GetForegroundWindow, PrintWindow) that are scoped to the calling process's WindowStation + Desktop. A process without an attached interactive desktop sees zero windows even when the user's logged-in desktop has plenty.
Windows session model in one paragraph. Every Windows process runs inside a numbered session. Session 0 is reserved for services and has no interactive desktop attached. Sessions 1, 2, ... are interactive logons — one per console / RDP user. SSH-launched processes (OpenSSH server) default to Session 0 unless the SSH server is explicitly configured to spawn the shell inside the interactive session. The same applies to most Windows service hosts, scheduled tasks running under SYSTEM, and background invocations from CI agents.
If cua-driver lands in Session 0, the window tools will silently return empty arrays and "no windows found" — that's the APIs working as designed against a session with no desktop, not a Cua Driver bug.
Diagnose with cua-driver doctor. The Windows session probe surfaces this directly:
[warn] interactive session: running in Session 0 (services); window-driving
tools (list_windows, click, type_text, screenshot, get_window_state)
will return empty results — these APIs need an attached interactive
desktop.
re-run cua-driver from an interactive logon (RDP, console, or a
scheduled task in the user's session) for the GUI tools to function.A healthy interactive logon reports the inverse:
[ok ] interactive session: session 2 has an attached interactive desktop
(WinSta0 + foreground window)Common fixes for "all my tools return empty arrays on Windows".
- Run from RDP or the local console — the simplest fix; both put your shell in your own interactive session, not Session 0.
- Run from a Windows Terminal / PowerShell launched by your logged-in user — same effect.
- Register a logon Scheduled Task (recommended, equivalent to macOS LaunchAgent) — see Auto-start at logon below, or the Autostart concept page for the
cua-driver autostart enableone-liner. - Driving over SSH? Register the autostart task once from an interactive session, then
cua-driver mcpandcua-driver callover SSH proxy through the running daemon automatically. See Running Cua Driver under SSH on Windows for the full workflow. - Use a session-launcher utility — if you must invoke from a service / SSH context without the daemon-proxy path, run the binary through a launcher that spawns it inside an explicit interactive session id rather than inheriting Session 0.
Run cua-driver doctor after switching contexts to confirm the warning has cleared.
Auto-start at logon (Windows)
The Windows-native equivalent of macOS's LaunchAgent is a Scheduled Task triggered at logon, running as your user, with Run only when user is logged on (i.e. LogonType: Interactive). Once registered, cua-driver serve starts automatically every time you sign in via RDP or the console — no more pasting a startup one-liner.
One-liner: cua-driver autostart enable && cua-driver autostart kick does everything the
manual block below does. See the Autostart concept
page for the full enable / disable / kick / status
verb family, and Running Cua Driver under SSH on
Windows for the recommended headless workflow on
top of it.
install.ps1 prints the exact one-shot registration block on a successful install. To register it manually (substitute your install path if you built from source):
$exe = "$env:LOCALAPPDATA\Programs\Cua\cua-driver\bin\cua-driver.exe"
$user = "$env:COMPUTERNAME\$env:USERNAME"
$action = New-ScheduledTaskAction -Execute $exe -Argument 'serve' -WorkingDirectory $env:USERPROFILE
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $user
$principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit (New-TimeSpan -Hours 0)
Register-ScheduledTask -TaskName 'cua-driver-serve' -Action $action -Trigger $trigger `
-Principal $principal -Settings $settingsUseful follow-ups:
schtasks /Run /TN cua-driver-serve # start it now without re-logging
schtasks /Query /TN cua-driver-serve /V /FO LIST
schtasks /Delete /TN cua-driver-serve /F # removeNotes:
LogonType: Interactiveis load-bearing — it pins the task to a Session 1+ logon. The alternative (S4UorPassword) would land you in Session 0 again, undoing the point.-ExecutionTimeLimit (New-TimeSpan -Hours 0)means "no time limit"; without it Windows kills the task after 72h by default.- Set
CUA_DRIVER_RS_TELEMETRY_ENABLED=0in your User environment if you want to disable telemetry persistently:[Environment]::SetEnvironmentVariable('CUA_DRIVER_RS_TELEMETRY_ENABLED', '0', 'User').
Grant TCC permissions
Cua Driver needs two permissions:
- Accessibility — to walk AX trees and dispatch
AXUIElementPerformAction. - Screen Recording — to capture per-window screenshots via ScreenCaptureKit.
Start the daemon first so TCC attributes the subsequent requests to CuaDriver.app rather than to whatever shell parent launched the CLI:
open -n -g -a CuaDriver --args serveThen trigger the prompts:
cua-driver check_permissionsmacOS raises the Accessibility and Screen Recording system dialogs. Grant both, then re-run the command to confirm:
cua-driver check_permissions
# ✅ Accessibility: granted.
# ✅ Screen Recording: granted.check_permissions reports the TCC status of the calling process. Inside an IDE terminal
(Claude Code, Cursor, VS Code, Conductor) the shell inherits the IDE's TCC responsibility chain,
so results can read "NOT granted" even when you've granted both to CuaDriver.app. The
daemon-first recipe above sidesteps this because the CLI forwards through the daemon, which runs
under CuaDriver.app's bundle id.
Prefer the permissions verbs. cua-driver permissions grant launches CuaDriver via
LaunchServices so the dialog attributes to com.trycua.driver (not your terminal), then waits for
and confirms the grant — the one-step authoritative path. cua-driver permissions status reads
the driver's real grant state through the daemon; when no daemon is running it reports
❓ unknown rather than your terminal's grants, so it can never claim granted when the driver
has none.
If a grant still reads NOT granted after granting in the dialog, open System Settings → Privacy & Security, find CuaDriver.app under Accessibility and Screen Recording, and flip the toggle.
First-launch permissions gate (cua-driver serve). On the Rust port,
cua-driver serve runs an interactive permissions gate at startup. If
Accessibility or Screen Recording is missing it prints a clear terminal
banner, opens the matching System Settings pane(s), and polls TCC every
second until you grant the missing items. When both grants are already
active the gate is a transparent no-op.
Experimental native panel. A CuaDriver Permissions NSPanel with
live status rows + auto-dismiss is also available behind an opt-in env
var. The terminal flow is the safer default because the panel relies on
a chain of macOS behaviours that are easy to break in dev environments
(TCC responsible-process attribution, session-level TCC caches that
survive tccutil reset, ad-hoc codesign identity mismatches, AppKit
modal-run-loop modes). Opt in with:
CUA_DRIVER_RS_PERMISSIONS_PANEL=1 cua-driver serveAccepted "on" values (case-insensitive): 1, true, yes, on. Any
other value (or unset) keeps the terminal flow. The panel also requires
the daemon to be launched from the bundled .app (e.g.
open -n -g -a CuaDriver --args serve); bare-binary launches always
use the terminal flow regardless of the env var.
CI / headless runners should skip the entire gate so the daemon does not block waiting for a TTY-attached human:
# As a flag …
cua-driver serve --no-permissions-gate
# … or as an env-var.
CUA_DRIVER_RS_PERMISSIONS_GATE=0 cua-driver serveAccepted "off" values for the gate env-var (case-insensitive): 0,
false, no, off — so FALSE, Off, NO etc. all work. Any
other value (including unset) leaves the gate active.
Update-available banner (cua-driver mcp / serve / doctor). The
long-running interactive entry points kick off a non-blocking background
check against the GitHub releases API at startup and print a small
two-line banner to stderr when a newer cua-driver-rs-v* release exists:
✨ cua-driver v0.1.4 is available (you have v0.1.3).
Update with: cua-driver update
Release notes: https://github.com/trycua/cua/releases/tag/cua-driver-rs-v0.1.4The answer is cached at ~/.cua-driver/version_check.json for ~20
hours, so subsequent launches reuse the cached result without a network
call. Network failures are silent — the next launch retries.
Scripted / machine-readable entry points are NOT instrumented.
The banner only fires from the long-running entry points (mcp,
serve, doctor). One-shot and machine-readable subcommands —
--version, list-tools, describe, call, dump-docs,
mcp-config, update, stop, status, recording, config,
diagnose, and telemetry install-event — all skip it so piped
output (cua-driver list-tools | jq …) stays clean.
Disable per-invocation:
CUA_DRIVER_RS_UPDATE_CHECK=false cua-driver serveAccepted "off" values (case-insensitive): 0, false, no, off.
Disable permanently via the persisted config:
cua-driver config set update_check_enabled falseSource / dev builds (any CARGO_PKG_VERSION with a pre-release suffix
like -dev) auto-skip the check — there is no matching published
release for them to recommend.
Requirements
| Platform | Requirements |
|---|---|
| macOS | macOS 14 (Sonoma) or later; Apple Silicon or Intel; Accessibility and Screen Recording grants. |
| Linux pre-release | x86_64 Linux desktop session; X11 or XWayland; AT-SPI for accessibility-tree tools. |
| Windows | Windows 10/11 or Server with an interactive desktop session; PowerShell; UI Automation enabled by default. |
Plan for roughly 50 MB for the app bundle or package directory, plus any retained old versions on Linux / Windows.
Run the daemon
Most workflows benefit from a persistent daemon. Element-indexed workflows require one: the per-pid element cache lives in-process, so one-shot CLI invocations lose it between calls.
macOS
open -n -g -a CuaDriver --args serveLinux pre-release
cua-driver serve &Windows
cua-driver autostart enable
cua-driver autostart kickConfirm it is up:
cua-driver status
# Cua Driver daemon is running
# socket: /Users/you/Library/Caches/cua-driver/cua-driver.sock
# pid: 12345Stop it cleanly when done:
cua-driver stopOn macOS, open -n -g -a CuaDriver --args serve is the recommended form because LaunchServices attributes the process to CuaDriver.app's bundle id, which is what the user actually granted TCC against. cua-driver serve & also works and auto-relaunches itself via open when it detects the wrong TCC context.
Register with an MCP client (optional)
The MCP server is one of two ways to use Cua Driver — skip this section if you only want the CLI (cua-driver list_apps, etc.). You can register both later, MCP and CLI work side-by-side.
Cua Driver speaks MCP over stdio. Use cua-driver mcp-config --client <name> to print the right command for your client, or copy one of the snippets below directly:
Clients with a CLI add command
# Claude Code (add --scope project|global as needed)
claude mcp add --transport stdio cua-driver -- ~/.local/bin/cua-driver mcp
# Claude Code computer-use compatibility mode
claude mcp add --transport stdio cua-computer-use -- ~/.local/bin/cua-driver mcp --claude-code-computer-use-compat
# Codex (OpenAI)
codex mcp add cua-driver -- ~/.local/bin/cua-driver mcp
# OpenClaw
cua-driver mcp-config --client openclaw | shThe Claude Code compatibility mode keeps CuaDriver's normal MCP tools, but replaces screenshot with a window-only screenshot shim that requires pid and window_id. Use it when you want Claude Code's vision/computer-use-style flow to ground on CuaDriver window captures.
Use MCP for that Claude Code vision/computer-use-style path. Shelling out to cua-driver screenshot can capture a window, but it does not expose the mcp__cua-computer-use__screenshot tool name that Claude Code appears to use as the image-grounding cue.
Clients configured via a config file
Cursor, OpenCode, and Hermes all configure MCP servers via files. Use mcp-config to print the exact snippet, paste it into the right path:
# Cursor — paste into ~/.cursor/mcp.json (or .cursor/mcp.json for project scope)
cua-driver mcp-config --client cursor | pbcopy
# OpenCode (sst/opencode) — paste into opencode.json at the project root
cua-driver mcp-config --client opencode | pbcopy
# Hermes (NousResearch) — paste into ~/.hermes/config.yaml,
# then run `/reload-mcp` inside Hermes
cua-driver mcp-config --client hermes | pbcopyFor any other client that accepts the standard mcpServers JSON shape:
cua-driver mcp-config | pbcopyPi (badlogic/pi-mono) does not support MCP natively. Use cua-driver as a plain CLI from inside Pi instead — cua-driver list_apps, cua-driver click '{...}', etc. — which is exactly the shape Pi is built around. Run cua-driver mcp-config --client pi for the full guidance, or see the Quickstart for CLI usage.
The client spawns cua-driver mcp on demand once registered.
TCC auto-delegation. When MCP clients spawn cua-driver mcp from an IDE terminal (Claude
Code, Cursor, VS Code, Warp), macOS attributes the subprocess to the terminal — not
CuaDriver.app — so AX probes would silently fail against the wrong bundle id. mcp detects this
and auto-launches a cua-driver serve daemon via open -n -g -a CuaDriver --args serve, then
proxies every tool call through the daemon's Unix socket. The client sees an ordinary stdio MCP
server; the daemon runs under LaunchServices with the right TCC context. No Python bridge, no
manual serve step required. Pass --no-daemon-relaunch (or set CUA_DRIVER_MCP_NO_RELAUNCH=1)
to opt out, e.g. when MCP is launched from CuaDriver.app directly and already has the right TCC
grants.
Daemon-proxy on Windows / Linux (cua-driver-rs v0.2.7+). The same proxy mechanism applies on
Windows and Linux, addressing the Session 0 / interactive-session problem rather than TCC: when
cua-driver mcp starts up and detects a daemon already listening on the default socket, it
proxies every tool call through to it instead of running in-process. Crucial for SSH on Windows,
where the SSH session lands in Session 0 (no desktop) but the daemon — registered via cua-driver autostart enable — lives in the user's Session 1+ interactive logon. Same --no-daemon-relaunch
/ CUA_DRIVER_RS_MCP_NO_RELAUNCH=1 opt-out as macOS. See Running Cua Driver under SSH on
Windows for the full workflow.
Agent skills (optional)
The installer does not modify agent instruction directories by default. To fetch the matching Anthropic-format skill pack and link it into detected agents, run:
cua-driver skills install| Agent | Skills directory | Linked by skills install when present |
|---|---|---|
| Claude Code | ~/.claude/skills/cua-driver | yes |
| Codex | ~/.agents/skills/cua-driver | yes |
| OpenClaw | ~/.openclaw/skills/cua-driver | yes |
| OpenCode | ~/.config/opencode/skills/cua-driver on macOS / Linux, %APPDATA%\opencode\skills\cua-driver on Windows | yes |
| Antigravity | ~/.gemini/skills/cua-driver | yes |
skills install never overwrites an existing user-managed cua-driver link or directory. Use cua-driver skills status, cua-driver skills update, and cua-driver skills uninstall to inspect, refresh, or remove the pack.
Agents without a SKILL.md auto-loader
Cursor, Hermes, and Pi each use a single-file system prompt (different format) instead of a skills directory. We don't auto-wire these because:
- Cursor (
~/.cursor/rules/*.mdc): rules expect a different frontmatter shape (description/globs/alwaysApply), not Anthropic'sname/description. Symlinking the SKILL.md as.mdcworks but the frontmatter renders as a noisy comment block. - Hermes (
~/.hermes/SOUL.md) and Pi (~/.pi/agent/SYSTEM.md): replace the entire system prompt — symlinking would clobber any custom prompt you've set.
For these, install the skill pack and paste its content manually into whatever instructions file the agent uses:
cua-driver skills install
cat "$(cua-driver skills path)/SKILL.md" | pbcopyUninstall
One canonical uninstall URL per platform mirrors the install side:
| Platform | Command |
|---|---|
| macOS | bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/uninstall.sh)" |
| Linux | bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/uninstall.sh)" |
| Windows | irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/uninstall.ps1 | iex |
uninstall.sh removes the Rust implementation by default on macOS and Linux. A legacy --backend=swift path remains for users explicitly removing an old Swift install.
uninstall.ps1 self-elevates via UAC when it detects an -AutoStart install — the cua-driver-serve Scheduled Task is registered at RunLevel=Highest (see Autostart), so the daemon spawned by it runs at High IL and a non-elevated process (even the same user that installed it) can't terminate it or delete the task. The script detects either condition upfront and re-spawns itself elevated; the in-place path runs without UAC when no autostart artifacts are present.
What each uninstall removes
| Platform | Symlink / bin | App bundle / package home | Autostart entry | Skill links | Claude MCP registrations |
|---|---|---|---|---|---|
| macOS | ~/.local/bin/cua-driver (when it resolves into CuaDriver.app and a Rust marker exists) | /Applications/CuaDriver.app (current), /Applications/CuaDriverRs.app (legacy), ~/.cua-driver/ (legacy ~/.cua-driver-rs/ also swept) | ~/Library/LaunchAgents/com.trycua.cua-driver-rs.plist | cua-driver and legacy cua-driver-rs links under agent skill dirs | Scrubbed from ~/.claude.json |
| Linux | ~/.local/bin/cua-driver (when it resolves into ~/.cua-driver/) | ~/.cua-driver/ (legacy ~/.cua-driver-rs/ also swept) | ~/.config/systemd/user/cua-driver-rs.service (stop + disable + remove) | Same as macOS | Scrubbed from ~/.claude.json |
| Windows | %LOCALAPPDATA%\Programs\Cua\cua-driver\bin (directory junction; legacy Programs\trycua\cua-driver-rs\bin also swept) | %USERPROFILE%\.cua-driver\ (entire tree, including packages\current junction; legacy ~\.cua-driver-rs\ also swept) | Scheduled Task cua-driver-serve (schtasks /Delete; script self-elevates when needed) | Junctions under %USERPROFILE%\.claude\skills, .agents\skills, .openclaw\skills, %APPDATA%\opencode\skills, .gemini\skills | Not auto-edited — closing message prints the manual claude mcp remove command |
Safety invariants — every uninstall script (uninstall.sh, uninstall.ps1) refuses to
clobber a real directory at a path where the matching installer could have only created a symlink
/ junction (Linux/macOS use [[ -L ]]; Windows checks IO_REPARSE_TAG_MOUNT_POINT). A user who
hand-managed any of these dirs keeps theirs untouched. Re-running an uninstall on an already-clean
system prints nothing to remove per item — never errors.
TCC grants persist on macOS (Accessibility + Screen Recording). The uninstaller leaves them in System Settings → Privacy & Security so a re-install doesn't have to re-prompt. To reset them explicitly:
# Both Swift and Rust drivers share bundle id `com.trycua.driver`
# (the Rust port took over Swift's identity in cua-driver-rs ≥ 0.2.4).
tccutil reset Accessibility com.trycua.driver
tccutil reset ScreenCapture com.trycua.driverPre-rename Rust installs (cua-driver-rs ≤ 0.2.3) used a separate bundle id com.trycua.cuadriverrs — clean those up if you had one of those installs:
tccutil reset Accessibility com.trycua.cuadriverrs 2>/dev/null
tccutil reset ScreenCapture com.trycua.cuadriverrs 2>/dev/nullWindows is interactive by default. uninstall.ps1 prompts before each major delete so a stray paste doesn't wipe a working install. For non-interactive / scripted teardowns, pipe through a -Force arg:
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/uninstall.ps1))) -ForceIt also doesn't auto-edit %USERPROFILE%\.claude.json — the closing message prints the manual claude mcp remove command instead. The macOS / Linux uninstaller does auto-edit ~/.claude.json when python3 is on PATH; the Windows uninstaller skips the auto-edit unconditionally to keep the script dependency-free.
Troubleshooting
cua-driver: command not found — ~/.local/bin isn't on your PATH. Add it and reload (see the first-time install callout above).
Permissions dialogs reappear after every launch — macOS is attributing the process to a different bundle id than the one you granted. Run cua-driver diagnose and paste the output when filing an issue; it reports cdhash, team id, and which bundle TCC is checking against.
Daemon won't start — another daemon may already be bound to the socket. Check with cua-driver status and stop it with cua-driver stop. For stale lock files after a crash, the daemon's own probe detects those and proceeds.
Ready to drive an app? Head to the Quickstart.
After install, see Updating for how to check for and apply new releases.
Was this page helpful?