Repository navigation
herdr --remote on Windows: remote UI renders but local keyboard and mouse input never reach the session #4038
Description
- I confirm this is a reproducible bug, not a feature request, idea, question, contribution proposal, or direction check.
- I reproduced this bug on the version and environment reported below using the exact steps provided.
Current behavior
On a native Windows 11 client, herdr --remote <host> attaches to a Linux host and the remote UI renders and streams frames, but the window never accepts local keyboard or mouse input. No keys register and clicking panes, tabs, or the sidebar has no effect. The view stays on the last frame; the window is effectively read-only.
Observed facts:
- The remote server log shows the client connected and staying connected:
client connected client_id=58 cols=160 rows=64 ... surface_active=true render_encoding=SemanticFrame, with no disconnect. herdr-client.logshows onlyherdr starting,connecting to server path=...\herdr-r-<pid>-<host>-<id>.sock, andendpoint handshake succeeded. No input, error, or warning lines.- The bridge process is
ssh -F <temp config> -T <host> "exec <remote>/.local/bin/herdr remote-client-bridge". The same command carries stdin both ways (an interactivecatround-trip over that SSH path echoes input), so the SSH pipe itself responds. - A local (non-remote) herdr client on the same machine, same terminal, and same binary accepts keyboard and mouse normally.
- Process state at the time:
herdr --remote <host>launcher alive, its childherdr clientalive, and thesshbridge process alive.
Expected behavior
Keyboard and mouse input in the herdr --remote window should drive the remote session, matching a local herdr client and the Linux/macOS remote attach behavior described in the docs.
Reproduction
- Windows 11 client, Windows Terminal, PowerShell 7, herdr 0.9.0-preview on the preview channel. Remote target is a Linux host running the same build, reachable with plain
ssh <host>(Tailscale SSH). - From a plain terminal (not inside an existing herdr), run
herdr --remote <host>. - The remote UI renders.
- Type into the window (prefix keys, characters into a pane) and click panes, tabs, and the sidebar.
- Nothing responds. The same input works in a local herdr client on the same machine.
Impact
herdr --remote from Windows is unusable: the remote session cannot be interacted with at all, even though it renders. The only working path today is ssh <host> and running herdr on the host. This matches the documented Windows remote support, so the Windows --remote client path appears broken for input.
Environment
- Herdr version: 0.9.0-preview.2026-09-08-62431dbd033b
- Update channel (stable or preview): preview
- Operating system: Windows 11 Pro 25H2, build 26200 (client); Linux (remote host)
- Terminal: Windows Terminal
- Shell, if relevant: PowerShell 7 (pwsh)
- Relevant config, if any: defaults;
ui.mouse_captureunset (defaulttrue)
Activity
HUAN2022A commentedon Sep 13, 2026
Static analysis of the Windows
--remoteinput pathI did a full static trace of the input chain on
fc44f61(input source → client routing → endpoint gating → ssh bridge → server injection). No runtime verification: I have no Rust toolchain or Windows reproduction environment here, so everything below is code reading, including crossterm 0.29.0 sources. The herdr-internal routing checks out symmetric between local and remote Windows clients; the evidence points at the Windows console input buffer being consumed by a competing process, plus one related latent defect.1. Symptom recap
Windows 11 client,
herdr --remote <host>to a Linux host: UI renders and streams (SemanticFrame), server seesclient connected ... surface_active=truewith no disconnect, but no keyboard or mouse input ever takes effect — including client-side UI (tabs, sidebar), so events never reach the client shell at all. Local herdr on the same machine/terminal/binary works.2. Chain analysis
Input source (Windows). The reader thread is spawned once (
src/client/mod.rs:462-475). The backend is chosen once at startup (src/client/input.rs:324-339): the VTI console reader (windows_vti::raw_console_reader_loop) is used iffGetConsoleModereportsENABLE_VIRTUAL_TERMINAL_INPUT, else the crossterm loop. The VTI reader waits onWaitForSingleObject(handle, 10)and thenReadConsoleInputW(src/client/input/windows_vti.rs:184-219). Console input records are destructively consumed from the single shared console input buffer — any other process attached to the same console that reads console input starves herdr completely: keys and mouse dead, view frozen, while stdout rendering keeps working. That matches the reported symptom exactly.Terminal setup.
setup_terminal_with_capabilities(src/client/terminal_setup.rs:39-111) runsEnableMouseCaptureat line 59 before enabling VT input. On Windows, crossterm 0.29 always takes the winapi path for mouse capture becauseEnableMouseCapture::is_ansi_code_supported()returnsfalseunconditionally (crosstermsrc/event.rs:321-346; dispatch insrc/command.rs:121-129), andenable_mouse_capture()replaces the console input mode withENABLE_MOUSE_MODE(0x0098) (crosstermsrc/event/sys/windows.rs:13,36-41), which clearsENABLE_VIRTUAL_TERMINAL_INPUT. Herdr compensates by re-enabling VT input afterwards (terminal_setup.rs:73-76non-SSH,51-56SSH), leaving mode0x0298. This is identical for local and remote, so setup is not the divergence.Local vs remote process topology. A local attach runs the client in-process (
src/server/autodetect.rs:295-319→client::run_client).herdr --remoteinstead spawnsherdr clientas a child with inherited console stdio (src/remote/attach.rs:2720-2748) plus the long-lived ssh bridge as a sibling:ssh -F <cfg> -T <host> "exec ... remote-client-bridge"with stdin/stdout piped but stderr inherited into the shared console, and without-n(src/remote/attach.rs:2410-2429). The repo itself documents that Windows OpenSSH reads the console even with stdin redirected to NUL — see the comment and-nworkaround for the transient prepare-phase ssh calls atsrc/remote/attach.rs:619-634— yet the persistent bridge ssh gets neither-nnor a detached stderr. In the remote setup that ssh is the only extra console-attached reader relative to the working local setup.Routing/gating (verified symmetric, not the cause). Windows
StdinEvents→shell.handle_client_events(src/client/mod.rs:988-1055) →finish_client_shell_input(src/client/shell_runtime.rs:614-734). The bridge connection registers as endpointLocal, generation 1,surface_active=true(src/client/mod.rs:510-535,src/client/endpoint/registry.rs:101-128), soactive_surface_available()is true and nopending_activationexists in a fresh remote attach; the silent drop-gates atshell_runtime.rs:675-679/717-723don't fire. Server-side,ClientShellPaneInputrequiresis_active_shell_client=shell_surface_active(src/server/headless.rs:2356-2368,src/server/clients.rs:462-464), which is true per your log. Linux/macOS remote clients read stdin bytes from a pipe (src/client/input.rs:73-218), immune to console-record theft — consistent with remote working there.3. Root-cause conclusion
Primary (most probable): the bridge
ssh.exeprocess, attached to the same console via inherited stderr, consumes console input records that herdr's VTI reader (ReadConsoleInputW) needs. Windows console reads are destructive, so a competing reader yields exactly "renders fine, zero input of any kind from the start". The in-repo comment atattach.rs:622("Windows OpenSSH can still read the console with stdin redirected to NUL") shows this hazard is already known for the prepare-phase calls; the bridge applies no mitigation. I could not execute a reproduction to confirm this, so treat it as the leading hypothesis rather than a proven fact.Secondary (verified latent defect, but not reachable with your defaults): when the server sends
MouseCapture { enabled: false },set_mouse_capture(false)runs crosstermDisableMouseCapture→ winapi restores the ORIGINAL mode snapshot captured before VT input ever existed (crosstermsrc/event/sys/windows.rs:20-30,44-49), permanently clearingENABLE_VIRTUAL_TERMINAL_INPUT(and mouse bits) while the already-running VTI reader keeps expecting VT sequences. Herdr re-enables VT input only whenenabled == true(src/client/mod.rs:1770-1772SSH branch,1776-1778non-SSH branch) — there is no re-enable on the disable path. With defaultui.mouse_capture = truethe server never sendsenabled: false, so this does not explain this report, but it's a real bug reachable viaui.mouse_capture = falseor any future disable path.4. Suggested fix direction
CREATE_NO_WINDOW/DETACHED_PROCESScreation flags to the bridge spawn insrc/remote/attach.rs:2410-2429; consider-nas well, though the comment atattach.rs:622indicates-nalone was not sufficient for the prepare-phase calls.set_mouse_capture(false, ..)on Windows with the VTI backend active, re-enableENABLE_VIRTUAL_TERMINAL_INPUT(mirror theenabled == truehandling insrc/client/mod.rs:1768-1779), or make the crossterm winapi mouse-mode restore preserve the VT-input bit.5. Cheap discriminating tests
HERDR_WINDOWS_INPUT_TRACE=1 herdr --remote <host>, then type: if nowindows input trace: console key recordlines appear inherdr-client.log(src/client/input/windows_vti.rs:1341-1343), the reader sees zero records → console-input theft confirmed; if they appear, it's the mapper/mode instead.HERDR_WINDOWS_INPUT_BACKEND=crossterm herdr --remote <host>(src/client/terminal_setup.rs:228-232): both backends dead → theft; crossterm works → VT-input mode lost.GetConsoleModehelper) for bit0x0200and watch for foreign modes (0x0098).ssh -F <cfg> -T <host> "exec cat"with piped stdin/stdout + inherited stderr alongside any console reader; if the reader starves, the bridge ssh is confirmed as the culprit.JJLiebig commentedon Sep 13, 2026
@akbash-bot can you check if this would already be tackled in #3687 or #3713, #3714, #3715 our remaining windows server work?
If no, see if the comment above is enough to start with a preliminary PR that I can verify locally on my Windows machines. Keep it draft and ping me in the PR comment to test + what to send my agent.
akbash-bot commentedon Sep 13, 2026
@Pimpmuckl I checked the current diffs. None directly addresses the proposed Windows client-side input failure:
The analysis above is a useful lead, not enough for a draft fix yet: it explicitly has no runtime verification. Redirecting stderr does not itself detach a process from its console, and missing input traces cannot establish another process consumed the events. Also, do not add
ssh -nto the bridge: its stdin carries protocol traffic. The mouse-disable observation is separate from the reported default configuration.Here is a bounded prompt for your Windows agent:
I have not opened a speculative PR. A captured failure on the unmodified path is the missing evidence before a draft fix and Windows verification request.
HUAN2022A commentedon Sep 13, 2026
Thanks @Pimpmuckl. Agreed with @akbash-bot's read — from static analysis on
fc44f61the four PRs look orthogonal to this failure mode. They are all server-side Windows work (remote host package install, hosting servers in the signed-in desktop, transferable PTY foundation, live server handoff), while the suspects in my earlier comment sit in the client input path on the attaching Windows machine:src/remote/attach.rs:2415-2425) passes-Tbut no-n, and usesStdio::inherit()for stderr in the interactive path — so ssh.exe keeps handles on the shared console and can consume input records that the VTI reader reads destructively viaReadConsoleInputW(src/client/input/windows_vti.rs:201). The code's own comment atsrc/remote/attach.rs:622("Windows OpenSSH can still read the console with stdin redirected to NUL") is why the framed path adds-n; the interactive bridge doesn't.MouseCapture { enabled: false }path calls intoset_mouse_capture(false, ..)(crosstermDisableMouseCapture,src/client/terminal_setup.rs:285-292), and the VTI re-enable atsrc/client/mod.rs:1770-1778only runs whenenabledis true — soENABLE_VIRTUAL_TERMINAL_INPUTcan stay cleared after a disable.The only tangential overlap I can see is the second one: server-side handoff/hosting changes could alter when
MouseCapture { enabled: false }messages arrive, but the mode-clearing defect itself lives client-side and triggers on any disable, so it wouldn't be fixed by those PRs either way.If a preliminary PR would help, I'm happy to prepare one per your process (draft, with a note on what to test locally) — otherwise deferring to @akbash-bot. Still static-analysis-only on my side; no runtime repro yet.
JJLiebig commentedon Sep 13, 2026
Reproduced and narrowed on native Windows 11 (26200), Windows OpenSSH
9.5p2, WSL2 Linux target, client running natively on Windows.Versions: reported client/server
0.9.0-preview.2026-09-08-62431dbd033b; master client built at540b20fd.Reported build — fails. Server logs
client connected client_id=2 ... surface_active=true render_encoding=SemanticFrame; client log stops right afterendpoint handshake succeeded. WithHERDR_WINDOWS_INPUT_TRACE=1, typing produces 1588console key recordlines and 56 parsedclient input events(incl. Enter) — console events do reach the input reader — but the remote pane never receives them.HERDR_WINDOWS_INPUT_BACKEND=crosstermfails the same way. The same binary attached locally accepts input normally.This rules out the "bridge ssh steals the console input buffer" theory: a stolen buffer would show zero records, while the reader sees all of them. The loss is on the client→server write path, not the Windows console reader.
Current master (
540b20fd) — passes. With the same setup, typing, a sidebar click (remote workspace focus changed), and independent remote output updates all work.The Windows endpoint-write fixes already on master line up with this:
c17dc3fd(#3721),d184b41f(#4004), plus thewindows_vtiwork in7265a2b1/9ad65d90. No source change appears needed.Caveat: I ran the client in a native Windows ConPTY console (Herdr pane), not a literal Windows Terminal window, so I reproduced the input failure but not the exact "renders then freezes" frame.
Closing as fixed on current master but please tag me if you can reproduce yourself on a current master build or if next preview release doesn't fix it.