diff --git a/docs/content/docs/cua-driver/guide/getting-started/installation.mdx b/docs/content/docs/cua-driver/guide/getting-started/installation.mdx
index acc716cf25..28fd7203c7 100644
--- a/docs/content/docs/cua-driver/guide/getting-started/installation.mdx
+++ b/docs/content/docs/cua-driver/guide/getting-started/installation.mdx
@@ -27,7 +27,17 @@ irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/in
On 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-rs/packages/releases/`, retargets `~/.cua-driver-rs/packages/current`, and symlinks `~/.local/bin/cua-driver`.
+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
@@ -79,7 +89,7 @@ Linux and Windows installs land in a three-tier layout that makes upgrades and r
**Linux**
```
-$CUA_DRIVER_RS_HOME/ (default: ~/.cua-driver-rs)
+$CUA_DRIVER_RS_HOME/ (default: ~/.cua-driver)
packages/
releases/
0.2.0-x86_64-unknown-linux-gnu/cua-driver (real binary, immutable)
@@ -114,10 +124,10 @@ Both platforms ship every release into its own per-version directory under `pack
**Linux — roll back to a specific version**
```bash
-# The symlink target is RELATIVE to ~/.cua-driver-rs/packages/, so just
+# The symlink target is RELATIVE to ~/.cua-driver/packages/, so just
# "releases/-" (no leading "../") — matches what install.sh writes.
-ln -sfn releases/0.2.0-x86_64-unknown-linux-gnu ~/.cua-driver-rs/packages/.current.tmp
-mv -Tf ~/.cua-driver-rs/packages/.current.tmp ~/.cua-driver-rs/packages/current
+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.0
```
@@ -141,7 +151,7 @@ Clear `$env:CUA_DRIVER_RS_VERSION` and re-run to roll forward to the newest rele
| ---------------------------------------------------- | ------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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-rs` | `%USERPROFILE%\.cua-driver` | Package home — holds `packages/releases//` and `packages/current`. |
+| `CUA_DRIVER_RS_HOME` | `~/.cua-driver` | `%USERPROFILE%\.cua-driver` | Package home — holds `packages/releases//` 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. |
@@ -183,13 +193,13 @@ The macOS install path is unaffected — `/Applications/CuaDriver.app` is an in-
```
==> another cua-driver-rs install is already in progress (lock at
- ~/.cua-driver-rs/packages/.install.lock.d); waiting...
+ ~/.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:
```bash
-cat ~/.cua-driver-rs/packages/.install.lock.d/info
+cat ~/.cua-driver/packages/.install.lock.d/info
# pid=43210
# started=2026-05-17T09:14:22Z
# argv=install.sh
@@ -231,7 +241,7 @@ 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-rs (3 release dirs cached)
+# [ok ] home dir: /Users/you/.cua-driver (3 release dirs cached)
# [ok ] telemetry: enabled (install-id present)
# ...
```
@@ -415,7 +425,7 @@ other value (including unset) leaves the gate active.
Release notes: https://github.com/trycua/cua/releases/tag/cua-driver-rs-v0.1.4
```
-The answer is cached at `~/.cua-driver-rs/version_check.json` for ~20
+The 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.
@@ -624,8 +634,8 @@ One canonical uninstall URL per platform mirrors the install side:
| 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 `~/.cua-driver-rs/` exists)_ | `/Applications/CuaDriver.app` (current), `/Applications/CuaDriverRs.app` (legacy), `~/.cua-driver-rs/` | `~/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-rs/`)_ | `~/.cua-driver-rs/` | `~/.config/systemd/user/cua-driver-rs.service` (stop + disable + remove) | Same as macOS | Scrubbed from `~/.claude.json` |
+| 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 |
diff --git a/docs/content/docs/cua-driver/guide/getting-started/linux.mdx b/docs/content/docs/cua-driver/guide/getting-started/linux.mdx
index 47ffbf8ae6..668e3e0beb 100644
--- a/docs/content/docs/cua-driver/guide/getting-started/linux.mdx
+++ b/docs/content/docs/cua-driver/guide/getting-started/linux.mdx
@@ -148,7 +148,7 @@ This is a useful recipe for local CI experiments with the Linux pre-release back
## Distro-specific notes
-The canonical install script (`/bin/bash -c "$(curl -fsSL …/install.sh)"`) downloads a Linux x86_64 binary tarball from GitHub Releases, drops it into `~/.cua-driver-rs/packages/releases/-x86_64-unknown-linux-gnu/`, and symlinks `~/.local/bin/cua-driver`. Because Linux support is pre-release, expect distro-specific gaps and prerequisites.
+The canonical install script (`/bin/bash -c "$(curl -fsSL …/install.sh)"`) downloads a Linux x86_64 binary tarball from GitHub Releases, drops it into `~/.cua-driver/packages/releases/-x86_64-unknown-linux-gnu/`, and symlinks `~/.local/bin/cua-driver`. Because Linux support is pre-release, expect distro-specific gaps and prerequisites.
The bit that varies is **what accessibility / display tooling is pre-installed**:
diff --git a/docs/content/docs/cua-driver/reference/changelog.mdx b/docs/content/docs/cua-driver/reference/changelog.mdx
index d0e9c38395..2bdbfba3cf 100644
--- a/docs/content/docs/cua-driver/reference/changelog.mdx
+++ b/docs/content/docs/cua-driver/reference/changelog.mdx
@@ -18,6 +18,24 @@ The canonical, always-current source is the
release body is auto-generated per release from the commits touching
`libs/cua-driver/rust`, including SHA256 checksums and install instructions.
+## Unreleased
+
+- **Install home unified on `~/.cua-driver` + collision fix.** The release
+ installer (`install.sh` → `_install-rust.sh`) was still defaulting its package
+ home to the legacy `~/.cua-driver-rs`, while the local installer
+ (`install-local.sh`) and the runtime already used `~/.cua-driver` (renamed in
+ v0.2.16). That mismatch meant a machine could end up with two homes and two
+ conflicting installs. The release installer now defaults to `~/.cua-driver`
+ (still honoring the `CUA_DRIVER_RS_HOME` override), and on every install it (a)
+ cleans up a prior `install-local` dev build under the shared home — stops the
+ daemon, removes the `*-local-*` release dirs and the local signing-identity
+ marker — and (b) sweeps a stale `~/.cua-driver-rs` left by an older release.
+ Both steps are best-effort, idempotent, and conservative (marker-gated; they
+ never touch a real release dir, the `current` symlink, or unrelated user
+ state). TCC grants are preserved — the `/Applications/CuaDriver.app` bundle is
+ replaced in place, not `tccutil reset`. The Windows installer (`install.ps1`)
+ already used `~/.cua-driver` and migrated the legacy home, so it is unchanged.
+
## 0.5.0 (2026-06-01)
- **Windows: per-session agent cursors** — the per-session cursor model (macOS
diff --git a/libs/cua-driver/scripts/_install-rust.sh b/libs/cua-driver/scripts/_install-rust.sh
index 3d58edfc39..350c4c0952 100644
--- a/libs/cua-driver/scripts/_install-rust.sh
+++ b/libs/cua-driver/scripts/_install-rust.sh
@@ -28,9 +28,14 @@
# binary location
# CUA_DRIVER_RS_BIN_DIR=PATH legacy alias for INSTALL_DIR
# CUA_DRIVER_RS_HOME=PATH package home for versioned installs
-# (default ~/.cua-driver-rs). Holds
+# (default ~/.cua-driver). Holds
# packages/releases/-/ and
# packages/current/ on Linux/Windows.
+# Renamed from ~/.cua-driver-rs in
+# v0.2.16 / PR #1644 — this release
+# installer was missed in that rename
+# and is reconciled here; a stale
+# ~/.cua-driver-rs is swept post-install.
# CUA_DRIVER_RS_NO_MODIFY_PATH=1 same as --no-modify-path
# CUA_DRIVER_RS_KEEP_VERSIONS=N keep the N most recent per-version
# release dirs after install; older
@@ -110,7 +115,17 @@ TAG_PREFIX="cua-driver-rs-v"
# CUA_DRIVER_RS_INSTALL_DIR is the documented name; CUA_DRIVER_RS_BIN_DIR is
# the legacy alias kept for users with the old env in their shell rc.
BIN_DIR="${CUA_DRIVER_RS_INSTALL_DIR:-${CUA_DRIVER_RS_BIN_DIR:-$HOME/.local/bin}}"
-HOME_DIR="${CUA_DRIVER_RS_HOME:-$HOME/.cua-driver-rs}"
+# Canonical home is ~/.cua-driver (renamed from ~/.cua-driver-rs in v0.2.16 /
+# PR #1644). The local installer (_install-local-rust.sh) and the runtime
+# already default here; this release installer was missed in that rename and
+# kept writing to the legacy ~/.cua-driver-rs, which is the root cause of the
+# install collision (release wrote one home, install-local + runtime used the
+# other). Reconcile the default here, keep accepting the CUA_DRIVER_RS_HOME
+# override for back-compat, and sweep the stale legacy dir post-install below.
+HOME_DIR="${CUA_DRIVER_RS_HOME:-$HOME/.cua-driver}"
+# Pre-v0.2.16 home this installer used to write to. Swept after the new
+# install is staged so a single rooted home (~/.cua-driver) is left behind.
+LEGACY_HOME_DIR="$HOME/.cua-driver-rs"
NO_MODIFY_PATH="${CUA_DRIVER_RS_NO_MODIFY_PATH:-0}"
# Post-install GC: how many per-version release dirs to retain. Validated
# below as a non-negative integer; 0 means "never GC". The dir that
@@ -352,6 +367,84 @@ prune_old_releases() {
printf '%s\0' "${to_prune[@]}" | xargs -0 rm -rf
}
+# --- Clean up a pre-existing LOCAL (install-local) install --------------
+#
+# `install-local.sh` (`_install-local-rust.sh`) installs a dev build into the
+# SAME canonical home this release installer now writes to (~/.cua-driver, see
+# the HOME_DIR reconciliation above), under a `*-local-*` versioned release dir
+# (VERSION_TAG="0.0.0-local-"). On macOS it also cert-signs the shared
+# /Applications/CuaDriver.app with a self-signed identity recorded at
+# `~/.cua-driver/.tcc-signing-identity`.
+#
+# A user who ran install-local and then runs this release installer would
+# otherwise end up with the local artifacts lingering alongside the fresh
+# release: the `*-local-*` release dir(s) sit in `packages/releases/` (the
+# release `current` swap re-points away from them, but they're never removed
+# explicitly here), and the stale `.tcc-signing-identity` marker survives even
+# though the release bundle is CI-signed, not locally cert-signed. Follow the
+# same logic install-local / uninstall.sh use: stop the daemon, then remove
+# ONLY the unambiguously-local artifacts so the release install is the single
+# authoritative one.
+#
+# Conservative by construction: we only ever remove `*-local-*` release dirs
+# and the local signing-identity marker — never a real release dir, never the
+# `current` symlink (the release branch owns that), never unrelated user state
+# under the home. Every step is best-effort + idempotent; a machine with no
+# prior local install is a clean no-op.
+#
+# TCC is preserved deliberately: we do NOT `tccutil reset` here. The bundle at
+# /Applications/CuaDriver.app is shared (bundle id com.trycua.driver) and the
+# subsequent release `ditto` re-points the binary in place; grants keyed on the
+# bundle id survive (macOS may re-prompt once on the cdhash change, same as any
+# upgrade). Churning the signing identity would gratuitously invalidate
+# cert-pinned grants, so we leave it alone.
+cleanup_prior_local_install() {
+ local releases_dir="$HOME_DIR/packages/releases"
+ local tcc_marker="$HOME_DIR/.tcc-signing-identity"
+
+ # Collect the local-build release dirs (the unambiguous install-local
+ # signature — a release install never creates a `*-local-*` dir).
+ local local_dirs=()
+ local d
+ if [[ -d "$releases_dir" ]]; then
+ for d in "$releases_dir"/*-local-*/; do
+ [[ -d "$d" ]] && local_dirs+=("${d%/}")
+ done
+ fi
+
+ # Nothing local on disk → clean no-op (no marker, no local dirs).
+ if [[ ${#local_dirs[@]} -eq 0 && ! -f "$tcc_marker" ]]; then
+ return 0
+ fi
+
+ log "detected a prior install-local build under $HOME_DIR — cleaning it up so this release install is authoritative"
+
+ # Stop the local daemon BEFORE we yank its binary out from under it,
+ # mirroring the post-swap stop both installers already do. Best-effort.
+ stop_cua_driver_daemons
+
+ # Remove the `*-local-*` release dirs. The release install stages into its
+ # own `-` dir and swaps `current` to it, so deleting the
+ # local dirs can't strand the active install. If `current` somehow still
+ # points into a local dir (e.g. a partial prior run), the release branch
+ # below re-creates `current` immediately after, so a transient dangling
+ # link is harmless.
+ if [[ ${#local_dirs[@]} -gt 0 ]]; then
+ for d in "${local_dirs[@]}"; do
+ rm -rf "$d" 2>/dev/null || true
+ log " removed local build dir ${d##*/}"
+ done
+ fi
+
+ # Remove the local signing-identity marker — it describes the locally
+ # cert-signed bundle, which the release `ditto` is about to replace with
+ # the CI-signed one. Leaving it would misreport the bundle's identity.
+ if [[ -f "$tcc_marker" ]]; then
+ rm -f "$tcc_marker" 2>/dev/null || true
+ log " removed local signing-identity marker $tcc_marker"
+ fi
+}
+
# --- Resolve OS/arch ----------------------------------------------------
OS=$(uname -s)
@@ -506,6 +599,11 @@ fi
# --- Install ------------------------------------------------------------
+# Before staging the new release, sweep any prior install-local build that
+# shares this home so the release install ends up authoritative (see the
+# function definition above for the conservative marker-gated logic).
+cleanup_prior_local_install
+
mkdir -p "$BIN_DIR"
# macOS: install the .app to /Applications first, then symlink the
@@ -641,6 +739,23 @@ else
prune_old_releases "$RELEASES_DIR" "$CURRENT_LINK" "$TARGET" "$KEEP_VERSIONS"
fi
+# --- Sweep the legacy ~/.cua-driver-rs home -----------------------------
+#
+# This release installer used to default HOME_DIR to ~/.cua-driver-rs (the
+# pre-v0.2.16 name). Now that it writes to ~/.cua-driver like install-local
+# and the runtime, a prior RELEASE install can have left a stale
+# ~/.cua-driver-rs behind — the source of the two-homes collision this PR
+# fixes. Sweep it now that the new install is fully staged under the canonical
+# home, mirroring the same belt-and-braces sweep _install-local-rust.sh does.
+# Runs AFTER staging so we never delete state before the replacement exists;
+# skipped when the user pinned CUA_DRIVER_RS_HOME to the legacy path on
+# purpose. Best-effort + idempotent.
+if [[ -d "$LEGACY_HOME_DIR" && "$HOME_DIR" != "$LEGACY_HOME_DIR" ]]; then
+ rm -rf "$LEGACY_HOME_DIR" 2>/dev/null \
+ && log "swept legacy package home $LEGACY_HOME_DIR (reconciled onto $HOME_DIR)" \
+ || log "note: could not fully remove legacy package home $LEGACY_HOME_DIR (best-effort)"
+fi
+
# --- Stop any pre-swap cua-driver daemons -------------------------------
#
# Mirror of install.ps1's `Stop-CuaDriverDaemons` call sequence. The
@@ -663,7 +778,7 @@ show_cua_driver_daemon_survivors
# --- Fire the one-shot install telemetry ping ---------------------------
#
# Anonymous adoption signal — sends `cua_driver_install` to PostHog
-# exactly once per install (guarded by ~/.cua-driver-rs/.installation_recorded
+# exactly once per install (guarded by ~/.cua-driver/.installation_recorded
# on the binary side). The Rust port keeps its install signal independent
# of the Swift `cua-driver` install (separate marker dir + separate env var)
# so users can opt out of one without affecting the other.