Skip to content

Windows

Windows support is experimental. Herdr’s API there is a named pipe, and the file at the socket path (%APPDATA%\herdr\herdr.sock by default) is only a marker holding the server’s pid and start time. Every client in this package (the extension, its watches, the sidebar plugin) connects to \\.\pipe\ followed by the socket path, as Herdr’s own CLI does. The socket path is found as Herdr finds it: HERDR_SOCKET_PATH, else the socket of the HERDR_SESSION session, else the default one in Herdr’s config directory (XDG_CONFIG_HOME, else %APPDATA%).

Details

The sb server’s control socket is a named pipe too. Pipe names are one namespace for every account on the machine, and the 700 directory that guards a Unix socket has no part in it, so the server names its pipe at random when it starts (\\.\pipe\sb-<16 hex>) and records the name in server.json, in your profile, where sessions read it. Another account cannot learn the name before the pipe exists, but the name can be listed once it does, and a server that ends leaves a name free for anyone to take.

So the pipe, like the Unix socket, uses a handshake (src/server/socket.ts, control protocol 2): the server draws a new 32-byte secret at start and writes it into server.json (mode 600), and the secret never crosses the pipe. A client opens with hello and a fresh nonce; the server answers its own nonce and HMAC(secret, "sb-control server" + both nonces); the client checks that, and only then sends its own proof ("sb-control client", the same nonces) together with the request, and the server acts only once that proof checks.

Something that squats a dead server’s name learns nothing it can reuse (it sees a nonce and cannot make the proof) and never gets the request; a peer that never answers costs the hello’s 5 s, not the whole wait. This stops a squatter of a name whose server is gone; it does not stop a process that sits between a client and a server that runs and relays both ways. A failure after the request was written (the server closed the connection or did not answer in time) is “the request may still be carried out”: the client does not send it again.

After an update the processes still running keep the protocol they started with: a session of the new package ends an sb server of protocol 1 (its server.json has no proto) by SIGTERM, once its start time says it is that process, under the server lock, and starts a new one: the first session after the update restarts the sb server. An older server that already speaks protocol 2 and answers is upgraded gracefully (restart request), never signalled.

One rule holds for every way a server is started (ensure, a session’s look, /boss server, up, a tower’s ask, restart, and serve) and for every stopped mark (stop): while server.json names a process that runs and did not answer hello, no server is started and nothing is marked stopped, unless this package signalled that process and saw it leave (launch and the stopped mark check it themselves, whatever the caller saw before).

Such a server is told as left alone, status shows it as not reachable, and restart, stop, up, and a tower’s ask fail with a message that names the command that ends it (taskkill /F /PID n on Windows, which needs /F for a process with no window; kill n elsewhere, and kill -9 n if a stuck loop leaves it running); a session’s look that finds such a server twice in a row tells the boss once, with those words, and tells again only after the hold has cleared and come back; the rule covers a server that is only slow or stuck as well (on Windows a hello that timed out waits for the start time too, so a stuck server whose time is not cached yet is held, not joined by a second one), and a process whose start time was read and is not the record’s (a pid reused since) is not the server.

On Windows a held verdict that rests on a time cached from before is read again first, so a pid reused since is not held and its number is not named. On Windows the start time comes from a PowerShell call that takes 3.5 to 5 s and more under load: the first look of a server that refused the handshake waits up to 3 s for it, and a time not read in that wait leaves the server alone, with status saying the start time could not be read and that the next look reads it again by itself (the read goes on in the background, and at most one runs at a time, so a session’s look every 2 s does not start a PowerShell each time).

Just before the signal a time cached from before the call is forgotten and read again, and that read is waited for as long as a read takes (15 s, or the time the caller has left); a time that the same call read is used as it is. If the read does not come in that time, or is not the record’s, nothing is signalled, nothing is started, and nothing is marked stopped; a look a little later finds the time read and ends the server then. Nothing a peer says before it has proved the secret is shown in an error.

Relaying needs the user’s own account (a second instance of the live pipe, or replacing the socket inside the 700 directory). A session of the older package (reload pi with /reload) is told the mismatch by a newer server. A pipe ends with its server, so there is no file to remove.

A process’s start time has no Node API and Windows has no ps, so there it is read through PowerShell’s CIM query (powershell.exe -NoProfile -NonInteractive -Command "$ErrorActionPreference='Stop'; (Get-CimInstance Win32_Process -Filter 'ProcessId=<pid>').CreationDate.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ', [cultureinfo]::InvariantCulture)", hidden, 15 s limit), which answers without elevation for a service or an elevated process too, where Get-Process would not. One call takes 3.5 to 4 s on a Windows 11 ARM VM, more under load, so nothing waits on it: a pi reads its own start time in the background when its session starts, retrying after 1, 2, 4, 8, and 15 s while it fails, and keeps it for its life; another process’s is cached by PID for ten minutes, forgotten when that PID is seen dead, and a failed read is asked again after 5 s, then 10, 20, 40, and at most 60 s.

Until the first answer the start time is unknown, which counts as “cannot be told” (the record stands). A record written before the own start time is known (gui.json, remote.json, the link holder’s holder.json) is written again when it arrives; the “could not record its own start time” warning comes only when the reads give up or that write fails. With start times, a survivor adopts a GUI whose owner died, as on macOS, and the link holder’s record and lock are no longer taken for a PID another process reused. Both sides of a comparison read through the same function, so the ISO text only has to be stable. The detached node that balances panes is started with windowsHide, so no console window appears.

The sidebar plugin declares windows. Windows has no bash, so its startup hook and action each have a Windows twin that runs node (which Herdr finds on PATH) instead of run.sh, and since an action id is unique across platforms, the Windows action is wayne930242.straw-boss.net-windows. Herdr’s plugin support on Windows is itself a preview.

The project-trust relay (Project trust) needs pi’s agent folder inside your user profile. The relay’s files in <state>/trust/ are trusted only when the folder is a real directory of this user, and Windows has no user ids to tell that by, so the folder must lie under %USERPROFILE% (the home directory Node reports, compared after expanding 8.3 short names), whose folders are private to the user. With PI_CODING_AGENT_DIR pointing elsewhere (another drive, a shared folder), the pi answers undecided and asks in its terminal as before, and the GUI never sees the question; src/trust/files.ts (ensureDir) is the check.

The tower prompt is typed as node "<cli.mjs>" prompt …, which cmd and PowerShell take as they take sh, and it talks to the server’s named pipe through the same client as status (the record is read once the owner of the state folder is judged). Its exit keys are Node’s: Ctrl+D and Ctrl+C in raw mode. Not run on Windows.

Closing a pane after pi quits (a tower’s quit, /boss close) waits for pi’s exit in a node process rather than sh, so it works without a POSIX shell.

Wi-Fi access (Browser view) works on Windows with these differences. Windows Defender Firewall blocks unsolicited inbound connections unless a rule allows them, and an enabled block rule that matches wins over any allow rule. The first listen on a private address normally asks “Windows Defender Firewall has blocked some features of Node.js”; cancelling that prompt adds block rules for node.exe, and allowing it needs an administrator. The server makes no firewall rule itself.

In one PowerShell start, without elevation, it reads the category of each served network, whether the firewall is on for it, and every enabled inbound rule that could match (by program, port, address, and interface type), and then says what to do, in this order: the block rules that apply, with a Disable-NetFirewallRule -Name '…' command for an elevated PowerShell (a rule name with a wildcard, a control character, or an invisible character is left out of the command and named, with the wf.msc steps instead); no allow rule, with a New-NetFirewallRule command for TCP on the port, LocalSubnet, Private profile; a network Windows calls Public, with where to set it to Private.

A read that fails says the state is unknown and gives both commands. The read runs in the background after the listeners are up and never holds the GUI. The computer’s name is its Windows name lowercased plus .local; the address in the QR works when the phone cannot resolve it. Hyper-V and WSL adapters are served (local VMs only); VPN adapters with an all-zero hardware address, such as WireGuard, are not. Which corporate VPN adapters are served is recorded in the release notes.

As a client, a Windows browser installs the certificate in Current User, Trusted Root Certification Authorities, and compares the one Security Warning’s SHA-1 thumbprint with the one the tile shows (a second warning means choose No and remove the first in certmgr.msc).

The worktree reaper stays macOS and Linux only. It closes a pane only when the pane’s shell runs nothing in its foreground, and on Windows Herdr’s pane process-info reports the shell alone unless an agent runs under it, so a pane running a build, a server, or an editor would look idle and be closed. Removing a checkout also needs each process’s working directory, which lsof and /proc give and Node cannot read on Windows without native code.

tests/platform.test.ts checks these on every platform, and the Windows workflow runs it on a Windows runner against real named pipes, after loading every module a session and the sidebar plugin load. Herdr itself does not run in CI, so pi starting inside Herdr on Windows is checked by hand.