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, 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 SSH-side CLI or MCP adapter
must use that daemon. MCP selects it explicitly with
--socket \\.\pipe\cua-driver; cua-driver call already resolves that default
pipe and accepts --socket when you want to make the endpoint explicit. 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
# Equivalent explicit form:
cua-driver call list_apps --socket \\.\pipe\cua-driverConnect 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 --socket \\.\pipe\cua-driver
claudeClaude Code starts cua-driver mcp on the SSH side. The explicit socket keeps
that process as a protocol proxy to the interactive daemon. Bare cua-driver mcp owns a direct runtime on Windows and therefore fails closed in Session 0;
it never silently falls back to another session.
Diagnose empty results#
Check these items before opening an issue:
- Confirm that
cua-driver --versionon the SSH side reports the same current install you expect. Upgrade if needed withirm https://cua.ai/driver/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 the MCP command includes
--socket \\.\pipe\cua-driver, or the exact endpoint reported bycua-driver status.
If the explicitly selected interactive-session daemon is unavailable, MCP startup fails instead of attempting GUI work from the SSH session.