Skip to content

SSH device hosts don't work on Windows machines #27302

Closed

Description

@NorthPhoenix

What happened

I want to use the Android emulators on my Windows gaming PC from the T3 Code server on my Linux machine, through Settings → Devices → Machine over SSH. T3 Code can use devices on other computers, but a Windows PC can't be one of them. I'm surprised this isn't implemented; it looks like a bug.

Diagnosis

The docs' "SSH device hosts" section lists requirements (Node 22+, npm on the non-interactive PATH, ANDROID_HOME, ffmpeg with libx264) but no OS limit, and the local device host handles Windows deliberately (LocalDeviceHost.ts:131-140, AgentDeviceShim.ts:236). The SSH path assumes a Unix host throughout. I reproduced each failure on a Windows 11 host (references are to tag v0.1.6-nightly.20261029.3892; none of these files changed on main since):

  1. sh -c everywhere. The bootstrap (SshDeviceHost.ts:56-58), every remote command (:178) and the port-forward session (:286-288) run sh -c '<script>'. Windows' built-in OpenSSH runs commands through cmd.exe, which answers 'sh' is not recognized as an internal or external command, exit code 1.
  2. npm is spawned without a shell (sshDeviceScript.ts:122, :143). npm 11.20 is installed, but on Windows spawnSync('npm') fails with ENOENT, and spawnSync('npm.cmd') with EINVAL (Node refuses .cmd without shell: true since CVE-2024-27980). The probe reports "npm is missing from the non-interactive SSH PATH."
  3. ps -p <pid> -o command= (sshDeviceScript.ts:57, :74) doesn't exist on Windows. Git Bash's ps answers ps: unknown option -- o, so running versions aren't detected and the hub can't be stopped.
  4. Locks are symlinks (sshDeviceScript.ts:85-89). fs.symlinkSync needs Developer Mode or the "Create symbolic links" right. It worked on this PC only because Developer Mode is on.
  5. Background processes keep the SSH session open. The hub (and agent-device's daemon) start detached inside the bootstrap's SSH session. On Windows the child inherits the session's handles, so the session, and with it T3's start, doesn't end until the child exits. T3 waits its full 1300 s timeout, then reports "Device support failed during start", although the hub had been "ready" since the first minute. A detached child sleeping 20 s held a plain ssh command for 21 s; started through PowerShell Start-Process (no handle inheritance) it returned in 1 s and the child survived.
  6. The bundled agent-device can't manage its daemon on such a host. On Windows it reads process identities with powershell Get-CimInstance Win32_Process (agent-device 0.21.31, sdk-io.js), run synchronously with a 2 s timeout. Windows denies WMI to accounts signed in over the network (tasklist also says "Access denied"). So agent-device daemon status reports stopped while the daemon is running, and its event loop freezes for about 8 s during an open. T3's 2 s health check then restarts the daemon mid-request, the restart fails with "Could not stop the previous agent-device version.", and the client gets ECONNRESET socket hang up. Part of this may belong upstream in agent-device, but T3 users hit it through T3's supervision.

Steps to reproduce

  1. On a Windows 11 PC: enable the built-in OpenSSH Server (default shell cmd.exe) with key sign-in for a standard account, install Node 22+ and Git for all users, and install the Android SDK with an emulator.
  2. On a T3 Code server (Linux here): Settings → Devices → Add source → Machine over SSH → user@windows-pc with the identity file, then Test connection or open the device picker.
  3. Actual: the check fails at point 1. Each later point shows up once the one before it is worked around (see below).

Expected: Windows hosts work like Mac and Linux hosts, or T3 says plainly that they aren't supported.

Version

0.1.6-nightly.20261029.3892 (the server, running as the background service)

Environment

Server: Ubuntu, Linux 6.8.0 x86_64, T3 Code background service. Device host: Windows 11 Pro (build 26100), built-in OpenSSH Server, Node 26.13, Git for Windows, Developer Mode on, RTX 5070 Ti. Device tools as installed by T3: expo-device-hub 0.19.0-canary-20261023-1357114, agent-device 0.21.31.

Evidence

# Point 1, the probe run through cmd.exe as stock OpenSSH does
'sh' is not recognized as an internal or external command,
operable program or batch file.
cmd.exe exit code: 1

# Point 2, the probe in Git Bash, and Node's spawn errors on the same host
npm is missing from the non-interactive SSH PATH.
bash npm --version: 11.20.0
spawnSync('npm'): ENOENT
spawnSync('npm.cmd'): EINVAL

# Point 3
ps -p <pid> -o command=: status 1 "ps: unknown option -- o"

# Point 5, T3's trace: start timed out although the hub was ready within a minute
SshDeviceHost.connectOnce durationMs 1300017.6  DeviceHostError: Device host gaming-pc failed while start.
hub.log: Expo Device Hub ready  Local: http://localhost:56594

# Point 6, the daemon's /health polled every 0.5 s during an `open` (T3 allows 2 s)
2.5 health FAILED (>2s) TimeoutError   ... through 10.2 s
`agent-device daemon status`: {"state": "stopped"}   (daemon running and healthy)
T3 service log: stderr: 'Could not stop the previous agent-device version.\n'
agent-device client: Error (COMMAND_FAILED): Failed to communicate with daemon / ECONNRESET socket hang up

Related issues

#20373 (an SSH device host said "Could not connect" when Node was missing from the non-interactive PATH) is about error wording on a Unix host. No existing issue covers Windows SSH device hosts.

Fix applied or workaround

Nothing in T3 Code was changed. A configuration-only workaround on the Windows host makes the whole flow work: the emulator boots on the host GPU (Android Emulator OpenGL ES Translator (NVIDIA GeForce RTX 5070 Ti…)), shows live in the Device panel, and agent-device drives it (open + snapshot 2.1 s, tap 2.1 s, back 1.6 s). The workaround:

  • an sshd ForceCommand for that account that runs SSH_ORIGINAL_COMMAND in Git Bash, with ANDROID_HOME, SERVE_EMU_FFMPEG and EXPO_DEVICE_HUB_EMULATOR_EXTRA_ARGS=-gpu host set;
  • a NODE_OPTIONS=--require shim that:
    • runs npm as node npm-cli.js;
    • answers ps -p and agent-device's Get-CimInstance Win32_Process queries from records each Node process writes about itself (pid, parent, start time, command line);
    • falls back from symlink locks to exclusively created files;
    • starts detached children of the SSH session through Start-Process.

start then returns in 7 s and agent-start in 37 s on first install. Two more Windows details: winget's machine-scope ffmpeg folder is readable only by administrators, so a standard account needs its own copy; and the hub's default -gpu auto is worth overriding in an SSH session.

Filed by

claude (claude-opus-5-5), following the t3 triage playbook inside a T3 Code thread

Activity

  1. bramtechs commented on Oct 31, 2026

    @bramtechs

    Still happens on 0.1.6-nightly.20261031.3956, which is newer than the build in the report. I hit it from a different setup:

    • T3 server: Windows 11 IoT Enterprise LTSC 2024 (build 26100), running t3 serve behind Tailscale Serve.
    • Device host: Windows (build 26300), stock OpenSSH Server with default shell cmd.exe, key sign-in. Node is at C:\Program Files\nodejs, ANDROID_HOME is set and the Android SDK is installed. No Git for Windows; the only bash.exe on PATH is WSL's.

    The failure is point 1 of the report. SSH sign-in works, then the remote side rejects the bootstrap:

    ssh -o BatchMode=yes -o ConnectTimeout=10 ... -- user@windows-pc sh -c '<remoteDeviceEnvironment> ... exec node'
    exitCode: 1
    stderr: "'sh' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n"

    The dialog hides the cause

    From the user's side the failure is hard to diagnose. In Edit device host → Test connection the row only shows the generic request_failed text, followed by a minified client stack:

    DeviceOperationError: Device probe host failed: Could not communicate with device support. Try refreshing devices.
    e@https://<host>/assets/utils-DA8g6pxN.js:14:35678
    @https://<host>/assets/utils-DA8g6pxN.js:14:37420
    ...
    

    The real cause, 'sh' is not recognized ..., only appears in the server's ssh.command.failed log. Until Windows hosts are supported, the probe could:

    1. Recognize the cmd.exe or PowerShell "not recognized" stderr, or a CRLF-only reply, and say "This SSH host runs Windows. SSH device hosts currently need macOS or Linux." DeviceService already maps the "Node is missing from the non-interactive SSH PATH" stderr line to its own message (DeviceService.ts:93), so a similar check would fit there.
    2. Show the remote stderr under Show error instead of the client stack trace.

    The device host here is the same PC as another T3 environment, so its devices already work locally there. The only thing broken is the cross-machine path from the other server.

    Related open PRs: #18172 (Windows SSH login shells), #24370 (Windows startup not closing the SSH session, #24367).


    Filed by Claude Code (claude-opus-5-5), investigating inside a T3 Code thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions