arc runner agent-rt
winget install --id=agent-rt.arc-runner -e Windows runner for arc - drive this machine from an AI agent over an encrypted link.
winget install --id=agent-rt.arc-runner -e Windows runner for arc - drive this machine from an AI agent over an encrypted link.
Agent remote control. Let an AI agent on one machine (e.g. Claude Code on macOS) drive another machine (e.g. Windows) over an encrypted link — build and run apps, operate GUIs, move files — through semantic, structured tools, not a pixel stream.
It is not a remote desktop: there's no screen-sharing for humans. The agent
calls tools — run a command, list/click UI elements, type, screenshot, transfer
files — over an end-to-end-encrypted channel. Part of
agent-rt (rt = runtime).
Controller (macOS / Linux) — the arc CLI, which is also the MCP server:
brew install agent-rt/tap/arc
Runner (Windows) — the machine being driven:
winget install agent-rt.arc-runner
Over Tailscale (recommended — no relay, no pairing code to copy):
On the Windows runner, register it (autostarts at logon). On Tailscale
it's one flag — --tailscale auto-detects the tailnet IP and restricts
access to this machine's Tailscale owner:
arc-runner install --tailscale
(Equivalent to --listen :8787 --trust-tailnet --allow ,
still passable explicitly; --allow-any permits any tailnet peer, --port
changes the port, --dry-run previews.) It mints credentials and prints a
ready-to-paste [targets.win] block.
On the controller, drop that block into ~/.config/arc/config.toml:
default = "win"
[targets.win]
direct = ":8787"
trust_tailnet = true
Drive it:
arc -t win shell --cmd 'ver'
arc-runner uninstall removes the task; arc-runner upgrade self-updates it to
the latest release (download → validate → swap → restart). For relay mode
instead of Tailscale, use arc-runner install --relay .
> Coming from ssh + scp? See docs/from-ssh.md for a
> side-by-side migration of the common workflow.
arc shell --cmd 'dotnet build' # run a command (streams live; --timeout )
arc run ./fix.ps1 -Port 8788 # ship & run a local script (.ps1/.bat); args pass through
arc push ./app C:/work/app # send a tree up (incremental, .gitignore-aware)
arc watch ./app C:/work/app --on-change 'cargo build' # auto-push + rebuild on every save
arc pull C:/work/app/bin ./bin # fetch a tree (or file) back
arc cat C:/work/app/log.txt # print a remote file
arc tail C:/work/app/log.txt -f # follow a remote log (streams new lines)
arc shot ui.png --launch notepad # launch → wait for render → screenshot, one shot
arc screencap shot.png --window N # screenshot to a file (.png/.webp by extension)
arc screencap now.png --window N --baseline before.png --diff diff.png # regression diff
arc windows --filter notepad # list top-level windows (--json for structured)
arc elements --json # list a window's UI Automation elements
arc find --type Button --name Save # query elements by attribute (no full dump)
arc wait --name Done --timeout 30 # block until a matching element appears
arc open notepad # launch an app
arc activate # restore + foreground a window (before capture/input)
arc ps notepad # list remote processes; arc kill
arc click # click a UI element (from `elements`)
arc read # read one control's text (verify without a screenshot)
arc set 'text' # set a control's value directly
arc type 'hello' --into # focus an element, then type into it
arc type "$(cat big.txt)" --into --paste # paste long text via clipboard (fast)
arc key ctrl+a delete --into # key chords in sequence (--into focuses an element first)
arc mouse drag 40 80 300 400 # move / click / down / up / scroll / drag
arc clip get # read the remote clipboard; arc clip set 'text'
arc agents-md # full Markdown command reference for an AI agent
arc finds a runner via a saved named target (-t , or the config's
default), explicit flags (--direct / --relay / --session / --pairing),
or env vars (ARC_DIRECT / ARC_RELAY_URL / ARC_SESSION / ARC_PAIRING).
Config lives in ~/.config/arc/config.toml:
default = "win"
[targets.win] # Tailscale-direct, identity-authenticated
direct = "100.x.y.z:8787"
trust_tailnet = true
[targets.public] # over a relay
relay = "wss://relay.example/v1/relay"
session = "0011…ffff"
pairing = "XXXX-XXXX"
push/pull are direction-by-verb (controller→runner / runner→controller). A
single file is copied wholesale; a directory transfers incrementally —
content-hashed with one round-trip to skip unchanged files, never walking build
dirs (target/bin/obj/…). Flags: --delete (mirror the source),
--dry-run, --whole (skip the diff). arc watch keeps a tree mirrored live
(debounced, build-churn-ignoring); add --on-change '' to rebuild on the
box after every sync — a zero-ssh edit → build → run → screenshot loop. To run a
local script on the box,
arc run ./script.ps1 [args] ships its contents and executes it (interpreter by
extension; .ps1 runs with -ExecutionPolicy Bypass) — no push first, no
cross-shell quoting to escape, args passed straight through.
> The model is code lives on your Mac; the Windows box is a build/run > container. With Windows App SDK 2.x an unpackaged WinUI 3 app builds with > just the .NET SDK (no Visual Studio), so a fresh runner + .NET SDK builds WPF / > WinForms / console / WinUI 3.
The same arc binary runs as a stdio MCP server with arc --mcp, exposing every
capability as a tool (run_command, run_script, screenshot, list_windows,
list_elements, find_elements, click, type_text, set_value, read_file,
write_file, …). Register it with, e.g.,
mcpctl:
mcpctl server add arc --command arc --args --mcp \
--env ARC_DIRECT=:8787 --env ARC_PAIRING=XXXX-XXXX
It connects lazily on the first tool call and reconnects on drops. run_command
streams output (relayed as MCP progress when the client supplies a
progressToken), so an agent can watch a long build in real time.
The relay exists to solve NAT traversal; on a shared private network it's unnecessary.
arc-runner --listen :8787) and the controller dials it — no relay, no
extra process, lower latency.arc-relay, which matches them by SessionId
and forwards opaque ciphertext (it holds no key).trust_tailnet = true): the runner
authenticates each caller by verified Tailscale identity (tailscale whois, optionally restricted to allow_logins), so no pairing code is
exchanged — authentication is the WireGuard transport plus identity, with a
fixed public key for the (defense-in-depth) Noise channel. controller (macOS/Linux) relay (optional) runner (Windows)
┌────────────────────┐ wss ┌──────────────┐ wss ┌──────────────────────┐
│ arc / arc --mcp │──────►│ arc-relay │──────►│ arc-runner │
│ (CLI / MCP server) │◄──────│ opaque relay │◄──────│ shell·UIA·input· │
└────────────────────┘ │ (no key) │ │ screenshot·files │
└────────── L2: Noise channel, end-to-end encrypted ──────────┘
proto::relay): Hello + SessionId + Role, then opaque
Relay{data} frames forwarded between the session's two peers. Zero-knowledge.
Skipped entirely in direct mode.proto::wire): Request / Response / Event carrying
Commands, sealed in a Noise channel (proto::crypto) and chunked through the
32 MiB frame cap.Both transports share one Session (arc-net), used by the CLI, the MCP
server, and the runner.
| Capability | Mechanism | In a detached session? |
|---|---|---|
list_*, set, element click | UI Automation (COM) | ✅ works |
single-window screencap | GDI PrintWindow | ✅ works |
type, key, coordinate mouse, full-screen screencap | SendInput / DXGI | ⚠️ needs an active console/RDP session |
UIA pattern-based control works headlessly; raw input and full-screen capture
need a connected desktop. ElementId is ":", resolved by
RuntimeId identity — it keeps pointing at the same control across tree changes,
or returns NotFound.
The runner serves requests concurrently (a hung command never blocks the
link or other commands) and enforces a default 10-minute command deadline
(--timeout ; 0 to disable).
The relay sees only ciphertext. The low-entropy pairing code is never used as a
key directly: both peers run a symmetric SPAKE2 PAKE to derive a
high-entropy key that keys Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s. Recording
the handshake gives no offline dictionary attack; a wrong code makes the Noise
handshake fail. In trust_tailnet mode the boundary is Tailscale ACLs plus
verified identity.
The Windows binaries are code-signed — see the code signing policy (free certificate courtesy of the SignPath Foundation).
just build # release `arc` (CLI + MCP server)
just lint # clippy -D warnings (host crates)
just test
| Crate | Role |
|---|---|
arc-proto | wire protocol, CBOR framing, Noise channel (chunked), SPAKE2 |
arc-net | shared client transport — relay/direct Session |
arc-relay | zero-knowledge wss relay (axum) |
arc-runner | Windows runner: shell, screenshot, UI Automation (windows-rs), input (enigo) — Windows-only |
arc-cli | the arc binary: CLI and arc --mcp server |
arc-runner builds only on Windows (windows-rs, xcap); the controller
crates build everywhere. Release/packaging is in RELEASING.md.
Licensed under either of MIT or Apache-2.0 at your option.