A CLI-driven RDP client designed for programmatic (e.g. LLM) consumption.
The ironrdp-agent binary is the CLI for the persistent daemon support:
- Daemon (
ironrdp-agent daemon-start): a long-lived, foreground process implemented byironrdp-daemonthat owns theironrdp-clientengine and one RDP session. It stays alive across many CLI invocations and serves requests over a local IPC transport (a Unix domain socket on Unix, a named pipe on Windows). - Gateway forward (
ironrdp-agent gw-forward): a foreground listener that relays TCP through an RD Gateway without an RDP session or the daemon.--socks5serves SOCKS5 CONNECT (no auth);--target HOST:PORTis an SSH-L-style fixed forward. Credentials come from--username/--passwordorRDG_USERNAME/RDG_PASSWORD, falling back toRDP_USERNAME/RDP_PASSWORD. The listener defaults to127.0.0.1; do not expose unauthenticated SOCKS5 to untrusted networks. - CLI (
ironrdp-agent <op> …): a short-lived invocation that opens the IPC endpoint, sends a single request, prints the response, and exits.
Run ironrdp-agent --help-agent for a structured, machine-readable description of every operation.
On Windows, an ActiveX host can expose its session through the same local RPC protocol. Start the
host with IRONRDP_ACTIVEX_RPC=1, then use --backend active-x for agent operations. The agent
uses the per-user ironrdp-activex endpoint by default and never attempts to start an ActiveX host.
Use --endpoint when the host selected IRONRDP_ACTIVEX_RPC_ENDPOINT.
The RAIL audit commands require the daemon backend.
connect accepts RDP_HOSTNAME, RDP_USERNAME, and RDP_PASSWORD as defaults for its named
connection flags. Explicit flags override those process-local values. The native MSTSC bridge uses
these only when it is explicitly enabled; RDP_AUTOLOGON is active only when its value is exactly
1, and requires nonempty username and password values.
On Windows, start the daemon with one or more --rdpdr-drive NAME=VOLUME_ROOT options to opt in to static filesystem redirection.
For example, ironrdp-agent daemon-start --rdpdr-drive System=C:\ --rdpdr-drive Data=D:\ exposes two local volumes to every session created by that daemon.
Each root must be a unique existing local volume root in the exact C:\ form, and each protocol-visible name must be unique case-insensitively and contain at most seven ASCII letters, numbers, spaces, underscores, hyphens, periods, or a trailing colon.
The configured drives are fixed for the daemon lifetime; hot-plug and rescan are not supported.
On Windows, enable WinSCard smartcard redirection with daemon-start --smartcard, overlay/connect property ironrdp_smartcard:i:1, or a sandbox config with SmartCardRedirection enabled.
Smartcard can be enabled without redirected drives (smartcard-only RDPDR).
Connect-time ironrdp_smartcard:i:0 disables it for that session even when the daemon was started with --smartcard.
The daemon performs strict certificate and hostname validation by default.
For an explicitly authorized test endpoint, start the daemon with --skip-certificate-check.
This startup-only flag accepts any certificate and server name, so it is vulnerable to on-path attacks and is unavailable through connect.
The daemon-backend rail commands expose client-validated RAIL handshake, launch, and window-order evidence without rendering a RemoteApp UI or accepting raw protocol PDUs.
Connect in RemoteApp mode with remoteapplicationmode:i:1 and a canonical remoteapplicationprogram:s:<program> property.
The client queues that program as the initial RAIL Execute request after the server handshake and keeps Client Info Alternate Shell and Working Directory empty.
An alternate shell value is only used as a compatibility fallback when remoteapplicationprogram is absent.
Start ironrdp-agent daemon-start in another terminal before connecting.
The target must publish and allow the requested RemoteApp.
ironrdp-agent connect --server rdp.example.test --prop remoteapplicationmode:i:1 --prop remoteapplicationprogram:s:notepad.exe
ironrdp-agent rail status
ironrdp-agent rail --format ndjson events
ironrdp-agent rail execute notepad.exe --arguments C:\Temp\audit.txtrail events retains 256 observations per connection generation.
Sequences remain monotonic across resize reconnects even though each new generation starts with fresh history.
When a caller resumes too far behind, the returned stream contains a gap event reporting the last sequence number no longer retained.
Set N to the latest sequence observed; rail wait --after-sequence N --timeout-ms 30000 returns retained later observations immediately or waits up to 30 seconds without polling.
If an accepted local launch fails before it is sent, the event stream reports execute_failed with a stable reason and no working-directory or argument data.
Use rail --format json for one deterministic document or rail --format ndjson for one event per line.
The agent advertises no local RAIL shell-integration flags because it does not implement move/size, taskbar, cloak, z-order, or display-power behavior.
type-unicode --text TEXT sends at most 96 Unicode characters in ordered FastPath input events.
The daemon reserves all queue slots before changing keyboard state, so queue backpressure sends none of a rejected request.
The ActiveX backend explicitly rejects this bulk input operation.
On Windows with the Windows Sandbox feature enabled, the agent can create and attach to a sandbox over the product's default named-pipe transport (\\.\pipe\{VMId}).
The connection uses standard RDP security with no encryption (PROTOCOL_RDP / ENCRYPTION_LEVEL_NONE).
:: create, inspect, and stop via WindowsSandboxServer gRPC
ironrdp-agent sandbox start
ironrdp-agent sandbox list
ironrdp-agent sandbox config <sandbox-id>
:: connect (daemon must already be running)
ironrdp-agent daemon-start
ironrdp-agent connect --sandbox-id <sandbox-id>
ironrdp-agent screenshot sandbox.png
:: shut down when finished
ironrdp-agent sandbox stop <sandbox-id>The agent speaks sandboxserver.SandboxCore in-process over the per-user named pipe (\\.\pipe\wsandbox\<md5(user SID)>) — no .NET helper is required.
WindowsSandboxServer must already be running; opening the Sandbox UI or invoking wsb starts it.
On retail builds that permit one active sandbox, stop that initial sandbox before using sandbox start; the agent reports server policy errors rather than bypassing them.
sandbox start accepts --id <GUID> and --config <FILE>, reads the same configuration XML accepted by wsb start --config, and prints the created sandbox Id.
Low-level escape hatch when you already have the pipe path and guest password:
ironrdp-agent connect --sandbox-pipe \\.\pipe\{VMId} -u WDAGUtilityAccount -p <password>Local (VMConnect :2179 + PCB) and guest TCP :3389 transports are not implemented as the
primary path; use the default NamedPipe recipe.
Prebuilt, checksummed archives are attached to each GitHub Release under the ironrdp-agent-v*
tags. See the Releases page for per-platform
download and verification instructions.
Messages are encoded with ironrdp-core's Encode/Decode traits, length-delimited with a
little-endian u32 byte-count prefix. There is no JSON anywhere. Both ends are the same binary at
the same version, so the format carries no version byte.
Connection configuration travels as a binary-encoded PropertySet inside a
strictly-typed Request::Connect. Runtime operations (mouse, keyboard, status, logs, …) are
strictly-typed messages. Request::Screenshot returns the most recent frame as PNG bytes (with the
mouse cursor composited in — the agent enables software pointer rendering), which the CLI writes to
disk.
The daemon never exposes secrets to the IPC reader. ConfigBuilder::build strips every
ironrdp_cfg::is_secret_key property (ClearTextPassword, GatewayPassword, the RDCleanPath
token, …) before producing the Config, and the daemon seeds its live property bag from that
post-build configuration. Secrets therefore never reach the live bag, so property dumps, status,
and logs cannot leak them — no separate redaction pass is needed.
An operator can preconfigure any settings — credentials in particular — without handing them to the
IPC caller. Pass an overlay PropertySet to daemon-start --overlay FILE;
the daemon layers it on top of every Request::Connect before building the configuration (overlay
wins). When the overlay carries a secret (password/token), Request::Status reports
credentials_loaded, so a caller should check the status first to learn whether it still needs to
supply a password.
connect and daemon-start both accept a repeatable --prop KEY:TYPE:VALUE flag, using the same
grammar as one .rdp file line (TYPE is i for integer or s for string, e.g.
--prop ironrdp_autologon:i:1 --prop username:s:admin). It lets a caller set any property without a
dedicated CLI flag existing for it. Final precedence, low to high:
.rdp file → --prop overrides → named flags (--server/--username/…) → daemon's overlay
On connect, --prop overrides win over an optional --rdp-file but lose to the named flags. On
daemon-start, --prop overrides win over an optional --overlay file, and the resulting overlay
still wins over everything a connect request supplies (unchanged).
Two logging concerns are kept separate:
- Daemon logging is the daemon's own operational logging (IPC handling, lifecycle). It is the
global
tracingsubscriber: a compact formatter writing to stderr, defaulting toinfoand tunable with theIRONRDP_LOGenvironment variable, mirroringironrdp-viewer. - RDP session logging is captured into a small, queryable in-memory ring buffer (read via
Request::QueryLogs) instead of the terminal. It is installed as a thread-local subscriber for the session thread only (tracing::dispatcher::with_default), so it never becomes the global subscriber. It defaults todebug; a per-Connectlog_directive(e.g.ironrdp_connector=trace) refines the filter to troubleshoot IronRDP itself.
After an RDP session connects, the daemon injects a per-session Devolutions::Now::Agent DVC
endpoint. The endpoint is local to the daemon and is not shared between RDP sessions. NOW protocol
framing, negotiation, heartbeats, capability gates, and command PDU handling are owned by the
now-client dependency; ironrdp-daemon owns only the endpoint/reconnect boundary and durable operation
state.
Use the supported commands below after connect:
ironrdp-agent now capabilities
ironrdp-agent now run "notepad.exe"
ironrdp-agent now powershell "Get-Process"
ironrdp-agent now pwsh "Get-Date"
ironrdp-agent now exec process cmd.exe --parameters "/c echo hello"
ironrdp-agent now exec batch "echo hello"now run is deliberately untracked and reports only local submission. Process, Batch, PowerShell,
and pwsh commands are daemon-tracked unless --detached is supplied. Tracked raw stdout and stderr
chunks are streamed without line buffering; a terminal nonzero exit code is returned by the CLI
(1–255 directly, wider values as 255). now cancel, now stdin, now attach, now list, and
now status operate on daemon-owned operation IDs. Initial stdin comes from --stdin FILE (or
-); live stdin is bounded to 1 MiB per request. Use --operation-id-file FILE to persist an
operation ID immediately after local submission for a later attach, cancellation, or stdin call.
PowerShell and pwsh use -NoProfile and -NonInteractive by default. --profile and
--interactive are explicit opt-outs. The agent retains at most 8 MiB of output per operation, 32
terminal records, and 32 MiB across retained records. now attach --after-sequence N replays
bounded output then follows a running operation. Live attachments are bounded and disconnect when
they cannot keep up; attach again with the last sequence number to resume from retained output. Use
now --format human|json|ndjson for raw human streaming, a JSON result, or JSON event lines; JSON
represents output bytes as arrays and is bounded to 8,192 events and 2 MiB of output. Use NDJSON
for unbounded streaming.
The local DVC endpoint is connected only on a NOW request. Its first readiness deadline is 30 seconds; a replacement after a worker/transport failure has a 10-second deadline. No Shell command, IPC request, capability, or execution mapping is exposed, even when a remote peer supports Shell.
The manual .github/workflows/agentic-rdp.yml workflow builds ironrdp-agent, enables localhost
RDP on a Windows runner, connects at the requested desktop size, drives the desktop through the
agent, and uploads logs and screenshots. Run the same scenario from an elevated Windows shell with:
cargo build -p ironrdp-agent --release
.\testing\agentic-rdp\Invoke-AgenticRdpTest.ps1 -DesktopSize 1920x1080The script temporarily changes local RDP settings and the current user's password. Use it only on a disposable test machine.
The ignored live test uses IRONRDP_AGENT_E2E_HOST, IRONRDP_AGENT_E2E_USERNAME,
IRONRDP_AGENT_E2E_PASSWORD, and optional IRONRDP_AGENT_E2E_DOMAIN:
$env:IRONRDP_AGENT_E2E = '1'
cargo test -p ironrdp-agent --test live_e2e -- --ignoredThe RemoteApp variant uses IRONRDP_AGENT_RAIL_E2E=1 plus the corresponding
IRONRDP_AGENT_RAIL_E2E_HOST, IRONRDP_AGENT_RAIL_E2E_USERNAME, IRONRDP_AGENT_RAIL_E2E_PASSWORD, and optional IRONRDP_AGENT_RAIL_E2E_DOMAIN variables.
It starts an isolated daemon with --skip-certificate-check, so run it only against an explicitly authorized test endpoint.