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.
tailscaled which user or tags that device has.| Your machine | Machine being controlled | |
|---|---|---|
| Command | rdc mcp --target studio-mac or rdc -t studio-mac … | rdc serve |
| Job | Speaks MCP to the agent, converts screenshot pixels to desktop points | Identifies callers, captures the screen, sends input |
| Runs as | A process the agent starts | LaunchAgent (macOS), systemd user service (Linux), scheduled task (Windows) |
| Holds secrets | No | No |
A click, end to end
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.
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.
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.
| Platform | Capture | Input | Windows and focus | Runs as | What shaped it |
|---|---|---|---|---|---|
| Linux, Wayland | portal Screenshot, then wlr-screencopy | wlr virtual pointer and keyboard | hyprctl on Hyprland | systemd user unit | GNOME and KDE lack the wlr input protocols: capture works there, input does not yet |
| Linux, X11 | xcb | XTEST | window list only | systemd user unit | xcap reports geometry divided by DPI scale; input wants raw pixels, so rdc multiplies back |
| macOS | screencapture (about 0.3 s); CoreGraphics fallback is slow on recent macOS | CGEvent via enigo | xcap list, NSRunningApplication | LaunchAgent inside a signed rdc.app | Screen Recording and Accessibility grants are keyed to the code signature; unsigned builds lose them on every rebuild |
| Windows | GDI / Graphics Capture | SendInput normalised over the virtual desktop | xcap list, SetForegroundWindow | Task 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
inputgrant controls the keyboard. A desktop session is enough to open a shell, so rdc does not offer one separately. Grantviewbroadly andinputnarrowly. - 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 doctorreports the missing permission.
Full threat model: Security.