Drive a Windows app over SSH
Use Cua Driver to drive GUI apps on a remote Windows machine from an SSH session.
The problem
Windows OpenSSH server runs in Session 0, the non-interactive services session. Sessions 1, 2, ... are interactive logons, one for each console or RDP user. A process inherits its parent's session, so shells created by sshd also run in Session 0.
The Win32 APIs used by Cua Driver window tools, including EnumWindows, GetForegroundWindow, PrintWindow, UIA, and BitBlt, are scoped to the caller's WindowStation and Desktop. Session 0 has no attached interactive desktop, so the tools cannot see the user's windows:
# Over SSH (Session 0), with no daemon helper:
cua-driver call list_windows
# [] ← empty, even though the user's RDP session has 12 windows openRun cua-driver doctor to confirm this state. The Windows session probe reports it directly:
[warn] interactive session: running in Session 0 (services); window-driving
tools (list_windows, click, type_text, screenshot, get_window_state)
will return empty results — these APIs need an attached interactive
desktop.
The solution
Run a cua-driver serve daemon in your interactive session, Session 1 or higher, through an autostart Scheduled Task. The CLI running over SSH detects that daemon and proxies tool calls through its named pipe. The SSH process only moves protocol messages; the daemon performs the actual GUI work from a session with a desktop attached.
┌───────────────────────────────────────────────────────────────┐
│ Session 1+ (RDP / console — has interactive desktop) │
│ │
│ cua-driver-serve (autostart Scheduled Task) │
│ ↑ │
│ │ named pipe: \\.\pipe\cua-driver │
│ │ │
└──────┼────────────────────────────────────────────────────────┘
│
┌──────┼────────────────────────────────────────────────────────┐
│ Session 0 (services / SSH — no desktop) │
│ │ │
│ cua-driver mcp │
│ cua-driver call list_windows │
│ Claude Code → MCP stdio → cua-driver mcp │
└───────────────────────────────────────────────────────────────┘
Set it up
1. From an interactive session, either RDP or the local console, run:
cua-driver autostart enable
cua-driver autostart kickenable registers a Scheduled Task with LogonType: Interactive. That setting is required because the alternatives would start the daemon in Session 0. kick starts the task immediately instead of waiting for the next logon.
You need an active interactive session for kick to place the daemon in Session 1+. Confirm that state with query session; your row should show Active or Disc:
query session
# SESSIONNAME USERNAME ID STATE TYPE
# rdp-tcp#23 you 2 ActiveIf you have never opened RDP on this machine, connect once with RDP. The autostart trigger fires automatically on the next logon.
2. From your SSH session, verify that the daemon is reachable:
cua-driver status
# Cua Driver daemon is running
# socket: \\.\pipe\cua-driver
# pid: 12345
# session: 2 ← daemon is in your interactive session3. Call tools from SSH:
cua-driver call list_apps # sees real GUI appsConnect Claude Code over SSH
After the daemon is running in the interactive session, register Claude Code the same way you would locally:
# From inside your SSH session:
claude mcp add --transport stdio cua-driver -- cua-driver.exe mcp
claudeEach MCP tool call starts cua-driver mcp on the SSH side. That process detects the daemon and proxies through it. Claude Code sees a normal stdio MCP server, while the daemon receives tool calls from the Session 1+ desktop context.
Diagnose empty results
Check these items before opening an issue:
- Confirm that
cua-driver --versionon the SSH side reports0.2.7or later. Earlier Windows builds do not proxymcp. Upgrade withirm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.ps1 | iex. - Run
cua-driver statusfrom SSH and confirm it reports a running daemon. If it does not, usecua-driver autostart statusto see whether the Scheduled Task is registered. - Run
query sessionand confirm your user has a row inActiveorDiscstate. - Run
cua-driver doctorfrom RDP and confirm it reports[ok] interactive session: session N has an attached interactive desktop. - Confirm that you did not pass
--no-daemon-relaunchand thatCUA_DRIVER_RS_MCP_NO_RELAUNCHis unset.
Opt out of proxying
To keep cua-driver mcp in the current process, such as in a CI runner that already owns an interactive session, use either opt-out:
cua-driver mcp --no-daemon-relaunch # per-invocation flag
$env:CUA_DRIVER_RS_MCP_NO_RELAUNCH = "1" # per-shell env varWith either form, tool calls run directly against the current session.