Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

How rdc works

A coding agent on your machine takes a screenshot of another machine, decides where to click, and clicks. This page follows that click through each step, shows who is allowed to send it, and explains how a pixel in the screenshot maps to the right place on a screen with a different resolution.

Two roles, one binary

The same rdc executable runs on both ends. On the machine being controlled it is a daemon, rdc serve. On your machine it is either an MCP server that Claude Code talks to, rdc mcp, or a command-line client you use directly. The network between them is your Tailscale tailnet.

your machine Claude Code (the agent) rdc mcp pixels → points MCP, stdio HTTP · JSON · PNG inside the Tailscale tunnel (WireGuard) machine being controlled rdc serve auth · audit the desktop screen · mouse · keys OS APIs tailscaled whois?
One connection crosses between the machines, and it carries no password. The Tailscale tunnel identifies the sending device, and the daemon asks its local tailscaled which user or tags that device has.
Your machineMachine being controlled
Commandrdc mcp --target studio-mac or rdc -t studio-mac …rdc serve
JobSpeaks MCP to the agent, converts screenshot pixels to desktop pointsIdentifies callers, captures the screen, sends input
Runs asA process the agent startsLaunchAgent (macOS), systemd user service (Linux), scheduled task (Windows)
Holds secretsNoNo

A click, end to end

agent rdc mcp rdc serve 1 · screenshot() 2 · GET /v1/screenshot 3 · PNG 2560×1440 + rect header 4 · image 1568×882 · "covers x=0 y=0 w=2560 h=1440" 5 · click(x=780, y=480) ← pixels in that image 6 · POST /v1/act click 1273,784 ← desktop points 7 · {ok} after validate → move → press → release 8 · GET /v1/screenshot (350 ms later) 9 · fresh image so the agent can check its work 780 × 2560/1568 = 1273
The agent gives coordinates as pixels of the last screenshot. rdc mcp stores the desktop region that screenshot covered and converts, so the model does not need to know about display scaling or multiple monitors.

The daemon returns the full-resolution capture and the client downscales it, so the image size sent to the agent can be changed without touching the remote machine. Every action returns a new screenshot by default, which keeps the mapping current and shows the agent the result.

Who gets in

rdc has no passwords, tokens or certificates. The Tailscale tunnel identifies the sending device, tailscaled reports who that device is, and a list in the config says what they may do. Every request goes through these checks in order.

0 · at startup: bind the Tailscale IP only anything else is refused, except 127.0.0.1 with --dev-loopback 1 · Host header names this machine? its Tailscale IPs, MagicDNS name, hostname, [serve].hosts 2 · peer IP is in 100.64/10 or fd7a:115c:a1e0::/48? taken from the TCP socket, never from a header 3 · ask tailscaled: whois(peer IP) → login alice@example.com node studio-laptop tags [] tagged devices: the creator's login is dropped; only tags and node name count 4 · match grants, union their capabilities "alice@example.com" → all { who = "monitor-bot", can = "view" } cached 30 s per IP 5 · does this route's capability match? screenshot needs view · click needs input · clipboard needs clipboard 6 · do it, then write the audit line 421 misdirected request 403 not a Tailscale address 403 not a tailnet peer 403 not in the allowlist 403 forbidden: may not use `input` audit.jsonl every outcome, one line
Every rejection and every success is written to the same audit file. Tailscale reports the user who created a tagged device; rdc drops that login, so tagged devices match only by tag or node name.

Grants live in the config file. A plain string grants everything; an inline table limits it. Several matching grants add up.

[serve]
allow = [
  "alice@example.com",                                     # full control
  { who = "monitor-bot", can = "view" },                   # screenshots only
  { who = ["tag:ops", "bob@example.com"], can = ["view", "clipboard"] },
]

Capabilities: view (displays, windows, screenshots), input (mouse, keyboard, focus), clipboard (read and write). See Grants and Tailscale policy for worked examples and the matching Tailscale rules.

Where the click lands

Machines report coordinates differently. A laptop with a 2880×1920 panel at 2× scale has a 1440×960 point desktop. A Mac mini at 2560×1440 is 1×. A Windows laptop at 2560×1600 and 150 % reports physical pixels. rdc uses one rule for all of them: every coordinate on the wire is a logical desktop point, a position in the virtual desktop spanning all monitors in the units the OS uses to place windows. A screenshot carries the rectangle of points it covers.

screenshot 1568×1045 pixel (784, 522) what the agent sees × 1440/1568 desktop 1440×960 points point (720, 480) what goes over the wire Wayland · fraction of first output's mode 720/1440 × 2880 = 1440, 480/960 × 1920 = 960 X11 · multiply by Xft.dpi scale 720 × 2 = 1440, 480 × 2 = 960 macOS · points already 720, 480 Windows · physical px already SendInput over the whole virtual desktop
One conversion on the client, then one per backend on the daemon. The numbers are the laptop's; on the Windows machine the same arithmetic maps a 1400-pixel-wide image onto its 2560×1600 screen.

Before moving anything, the daemon checks that the point is inside the combined display area, limits scrolling to 100 wheel steps, and rejects keys the platform does not have. A drag always releases the button, even if a move in the middle fails.

Per platform

The daemon uses different operating-system APIs on each platform. The table lists what it uses and the platform behaviour that determined how it is installed.

PlatformCaptureInputWindows and focusRuns asWhat shaped it
Linux, Waylandportal Screenshot, then wlr-screencopywlr virtual pointer and keyboardhyprctl on Hyprlandsystemd user unitGNOME and KDE lack the wlr input protocols: capture works there, input does not yet
Linux, X11xcbXTESTwindow list onlysystemd user unitxcap reports geometry divided by DPI scale; input wants raw pixels, so rdc multiplies back
macOSscreencapture (about 0.3 s); CoreGraphics fallback is slow on recent macOSCGEvent via enigoxcap list, NSRunningApplicationLaunchAgent inside a signed rdc.appScreen Recording and Accessibility grants are keyed to the code signature; unsigned builds lose them on every rebuild
WindowsGDI / Graphics CaptureSendInput normalised over the virtual desktopxcap list, SetForegroundWindowTask Scheduler logon task at standard integrity (--elevated opts into the highest run level)A service or SSH session is session 0 with no display; a non-elevated daemon cannot send input to elevated windows, which is the documented trade-off

What gets recorded

One JSON object per request or rejection, appended to audit.jsonl in the platform state directory, mode 0600, rotated by size. Typed text is recorded as a character count only. Key chords, window selectors and error messages are recorded with control characters replaced.

{"ts":"2026-09-09T16:08:55.979Z","peer":"100.64.0.7","login":"alice@example.com","node":"laptop",
 "method":"POST","path":"/v1/act","action":"input.click 100,100 Left x1",
 "outcome":"denied","status":403,"detail":"alice@example.com may not use `input` on this machine","ms":0}

Read it on the daemon machine with rdc audit -n 50, or --json for the raw lines.

Security notes

  • Anyone with an input grant controls the keyboard. A desktop session is enough to open a shell, so rdc does not offer one separately. Grant view broadly and input narrowly.
  • The tailnet is the security boundary. A stolen device that is on the allowlist, or a Tailscale policy that lets the wrong nodes reach port 7770, gives access to the desktop.
  • The Host check protects against browsers. A web page on an allowed machine could point its hostname at the daemon’s address and use that machine’s identity. The daemon answers 421 to any Host that is not one of its own names.
  • The agent never handles a password or a tailnet key. It receives images and returns pixel coordinates.
  • macOS asks again for Screen Recording about once a month. Until someone approves the prompt, screenshots show only the wallpaper. rdc doctor reports the missing permission.

Full threat model: Security.