You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Windows: herdr backend refuses every spawn - canonical socket path rejects the drive-letter path herdr emits on both sides of the identity check #3283
fm_backend_herdr_canonical_socket_path (bin/backends/herdr.sh:699) requires the socket path to be POSIX-absolute. On native Windows, herdr emits a drive-letter path on both sides of the launcher identity check, so fm_backend_herdr_launcher_identity always returns 1 and every herdr-backend spawn is refused:
error: herdr launcher pane 'wN:pM' reports an unusable socket path;
refusing to place a worker from an unverifiable parent identity
launcher_identity canonicalizes the first (line 1559) and presentation_session_socket_path canonicalizes the second (line 728). Both hit the /* guard, so the check fails before the two are ever compared.
There is no environment workaround. Setting HERDR_SOCKET_PATH to a POSIX path by hand fixes only the launcher side; the session side is read from herdr's own JSON, which the caller does not control.
Relaxing the guard alone is not sufficient either: dirname / cd "$sock_dir" / pwd -P at lines 706-712 cannot consume a backslash path, so the drive form has to be converted, not merely admitted.
Environment
Windows 11 (10.0.26200), native (not WSL)
Git for Windows 2.49.0, msys2 runtime 3.5.7
herdr backend, auto-detected via HERDR_ENV=1
firstmate at 1fd7ea2
Local patch that works
Convert only the drive-letter shape, only when cygpath is present, and re-verify the result is POSIX; every other shape stays refused and the POSIX branch is untouched:
Verified: C:\Users\...\herdr.sock and C:/Users/.../herdr.sock both canonicalize to the same /c/Users/.../herdr.sock, which is what the identity comparison needs; relative paths, bare filenames, empty strings and garbage are still refused.
With this applied, the spawn gets past the identity check and reaches the next Windows blocker, #1796 (treehouse worktree entry undetected because the cwd move happens inside a nested cmd.exe).
Related
Same family as #3267 (POSIX-only assumption in a foundation layer — there ln -s for the lock primitive). Direct evidence for the #504 intent (incremental native-Windows support via the Herdr backend).
Speaking as Kun's firstmate: whole thread read (no comments yet; related #504 Windows-herdr intent, #3267 POSIX lock assumption, #1796 nested cmd.exe spawn blocker). Verified still unfixed on latest main (f66be0f).
fm_backend_herdr_canonical_socket_path in bin/backends/herdr.sh still requires /* POSIX-absolute and then dirname/cd/pwd -P. Native Windows herdr emits a drive-letter path on both the launcher env and session list --json, so fm_backend_herdr_launcher_identity returns 1 before the two paths are compared and every herdr-backend spawn is refused. Setting HERDR_SOCKET_PATH to a POSIX path cannot fix the session-list side. No open PR covers this socket canonicalizer. #3231/#3228 are Git Bash watcher-lock, not this. Do not open a competing PR from this pass.
A correct fix must stay fail-closed: convert only the drive-letter shape (and only when a converter such as cygpath exists), re-verify the result is POSIX-absolute, and keep refusing relative/empty/garbage paths so parent identity remains verifiable.
VISION.md per-rule:
One captain, one interface: aligns (every spawn refused is not a usable crew).
Authority is explicit and never inferred: aligns (no new autonomy; identity check stays a refusal when unverifiable).
Scripts own the mechanics, agents own the judgment: aligns (path canonicalization is script mechanics).
A restart is a non-event: n/a.
Delegation with a spine: aligns (spawn is the worker-delivery contract).
The fleet outlives any vendor: aligns (bind to the socket path herdr actually emits, not POSIX pixels).
contract-class: restore (launcher-identity is supposed to compare two spellings of the same socket and allow spawn when they match; the POSIX-only guard makes a same-socket Windows pair unverifiable).
Following up with the rest of the picture: the socket-path refusal above is the first of four independent defects sitting on one code path on native Windows (Git for Windows / MSYS2, no WSL). Each is only reachable once the previous one is cleared, so none of them can be observed in isolation — which is presumably why they have not surfaced together before.
I have a working patch and a measured result. I am posting it here rather than opening a PR because CONTRIBUTING.md requires PRs to main to be raised through no-mistakes. Happy to run it through the gate and raise a proper PR if you want it — just say the word. Equally happy to split it into four PRs if that reviews better.
The four defects, in the order a spawn hits them
1. Socket identity refuses every drive-letter path (this issue)
fm_backend_herdr_canonical_socket_path only accepts a POSIX-absolute path, but herdr emits a native path on both sides of the launcher identity check, so there is no environment workaround — overriding HERDR_SOCKET_PATH fixes one side, the other comes from herdr session list --json. Relaxing the guard alone is not enough either: dirname / cd / pwd -P just below cannot consume a backslash path.
2. Worktree discovery polls a field herdr never populates
fm_backend_herdr_current_path reads .pane.foreground_cwd. On herdr 0.8.0-preview that field is declared in the API schema as ["string","null"] but emitted for no pane at all — measured across 27 panes, in api snapshot, and with a live foreground process demonstrably sitting in another directory. The poll returns empty forever, so treehouse get did not enter a worktree within 60s cannot be avoided by any timeout value.
Neither fallback works: .pane.cwd does follow a cd in the pane's own shell here, but not into the nested subshell treehouse get opens; and pane process-info's foreground_processes resolves to the pane's own shell, since Windows has no tty foreground process group to read.
The patch adds an active probe in the same spirit as fm_backend_zellij_current_path. It is wired only into the treehouse-get discovery loop, via a new spawn_discover_path, and deliberately not into fm_backend_herdr_current_path — that function is also the relaunch reader, relaunch adopts an existing endpoint whose pane may hold a live agent, and pane run submits, so probing there would type into that agent's composer. On a platform with no foreground_cwd, relaunch keeps returning empty and refuses, which is the safe outcome.
3. Task panes get cmd.exe, and the spawn still exits 0
This is the nastiest one. treehouse get picks its subshell from $SHELL; a herdr pane runs the configured default shell with no SHELL set, so treehouse falls back to %COMSPEC%. fm-spawn.sh then types POSIX into cmd.exe:
C:\...\fm-sandbox>export GOTMPDIR=/tmp/fm-<id>/gotmp
'export' is not recognized as an internal or external command
C:\...\fm-sandbox>env -u CURSOR_AGENT ... claude --dangerously-skip-permissions "$(...)"
'env' is not recognized as an internal or external command
The crewmate never launches, while fm-spawn.sh prints spawned <id> ... and exits 0.
Pinning SHELL alone is not sufficient: treehouse execs bash.exe non-login, so /etc/profile never runs and that shell has no msys toolchain at all — ls, head and env are all missing, which kills the launch line twice over (once directly, once because fm-operational-input.sh's #!/usr/bin/env bash interpreter is gone).
MSYS matters too: fm_lock_try_create's primitive is ln -s, msys silently copies by default, the copy still carries the owner's pid file, and fm_lock_acquire_wait (no timeout, no bail-out) then spins forever on a lock nothing can take. Measured in isolation:
with MSYS=winsymlinks:nativestrict ACQUIRE OK lock=symlink
without HANG (timeout, rc=124) lock=absent
That is what makes fm-captain-hold.sh complete hang indefinitely. Related to #3279, but a different mechanism — no PID reuse involved. Note that native symlinks additionally need Developer Mode or elevation; without either, nativestrict at least makes ln -s fail loudly instead of leaving a booby-trapped directory behind.
The patch hands a new pane SHELL, MSYS and the caller's own PATH, spliced into every pane-creating call: tab create, both workspace create sites, the projection task-tab, and the husk replacement. All five are needed — the projection ones only become reachable once (4) is fixed, and missing them puts cmd.exe back silently.
4. Teardown refuses because a mode bit cannot exist
fm_backend_herdr_presentation_lock_namespace_valid requires mode 700. Git for Windows mounts / and /tmpnoacl, so a directory always stats 755 regardless:
$ mount | grep /tmp
C:/Users/<user>/AppData/Local/Temp on /tmp type ntfs (binary,noacl,posix=0,usertemp)
$ mkdir -m 700 d && stat -c '%a' d
755
$ chmod 700 d; echo "chmod rc=$?"; stat -c '%a' d
chmod rc=0
755
The check can never pass. Spawn tolerates it (warns, drops the projection); teardown treats it as fatal — session presentation lock could not be resolved — and leaves the pane and the pooled worktree behind.
Relaxing this needed care. My first attempt kept the ownership assertion and dropped only the mode check, which was wrong: on a noacl mount stat is uninformative in both fields — every directory, /c/Windows and /c/Program Files included, reports the caller's own uid and 755. So the patch skips the mode assertion only when the filesystem provably cannot carry mode bits (probed at runtime, so an acl-mounted path on the same machine keeps the strict check) and re-anchors the guarantee on location: the namespace must sit inside the caller's own temp root, which on Windows is the per-user, ACL-protected %TEMP%.
Result
Verified on Windows 11 (10.0.26200), Git for Windows 2.49.0 / msys2 3.5.7, herdr 0.8.0-preview.2026-08-04, firstmate at debe4bf, against a scout task in a throwaway sandbox repo:
measured
spawn
25-33s every cycle, well inside the 60s/180s budgets
crewmate
scout-probe.txt committed in the pooled worktree + report written
teardown
pane count, workspace count and worktree pool back to baseline every time, via fm-teardown.sh itself
4 of 5 cycles passed end to end. The one failure lost its crewmate to an API Error: Connection lost mid-responseafter it had already committed in the worktree — not a firstmate defect.
Notes for review
Non-Windows behaviour is unchanged throughout: every new branch is gated on cygpath plus a drive-letter shape, and fm_backend_herdr_posix_env prints nothing elsewhere, so no --env is passed and no probe runs.
The repo has no .gitattributes; my tree is core.autocrlf=true and the committed blobs contain no CR, but worth a glance.
tests/fm-backend-herdr.test.sh has one pre-existing failure on a clean origin/main here — the ambiguity refusal did not name the candidate workspaces (missing: 'w1 w7'). Unrelated to this change, but flagging it since it shows up in any Windows run.
bin/fm-lint.sh needs ShellCheck, which I have not installed, so the patch is unlinted.
Happy to add tests for the new paths. tests/fm-backend-herdr.test.sh already mocks pane get JSON, so a foreground_cwd-absent case and a canonical_socket_path table would slot straight in.
The branch is on my fork as trngthnh369/firstmate:windows-herdr-backend (one commit, +220/-7, applies cleanly to debe4bf). Tell me which you prefer — a PR through no-mistakes, the patch inline here, or nothing at all if you would rather solve these differently.
Speaking as Kun's firstmate: whole thread re-read, including the 2026-08-30T08:55:43Z follow-up. Verified still unfixed on latest main (debe4bfafa2ad52bd96f088a8d8ba418931e828f). No open PR covers this spawn path. Do not open a competing PR from this pass.
The follow-up expands this from one refusal to four independent defects on the same native-Windows herdr spawn/teardown path (Git for Windows / MSYS2, no WSL). Each is only reachable after the previous one is cleared. Inspected on main:
Socket identity (original title): fm_backend_herdr_canonical_socket_path in bin/backends/herdr.sh still requires /* then dirname/cd/pwd -P. No drive-letter/cygpath conversion. Both launcher HERDR_SOCKET_PATH and session list --json still fail the guard before comparison, matching the original error.
Worktree discovery: fm_backend_herdr_current_path still only reads .result.pane.foreground_cwd. No spawn_discover_path (or zellij-style probe) exists.
Teardown mode bits: fm_backend_herdr_presentation_lock_namespace_valid still requires owner uid AND mode 700. No noacl/runtime probe, so a Git-for-Windows noacl/tmp can never satisfy teardown.
Related and still distinct: #504 (Windows-herdr intent), #3267 (lock ln -s), #1796 (nested cmd.exe cwd). The author's fork branch trngthnh369/firstmate:windows-herdr-backend is not an open PR against this repo.
A correct fix must stay fail-closed and Windows-gated so POSIX is unchanged: convert only the drive-letter socket shape when a converter exists, then re-verify POSIX-absolute; keep parent identity a refusal when unverifiable; do not type a probe into a live agent pane on relaunch; do not treat a cmd.exe spawn as success; skip mode 700 only when the filesystem cannot carry mode bits, re-anchored on location inside the caller's temp root.
Yes — raise through no-mistakes per CONTRIBUTING.md. One PR or four is your call; keep each defect independently fail-closed. Do not skip the gate. Firstmate will not open a competing PR.
VISION.md per-rule:
One captain, one interface: aligns (every spawn refused, or spawn reporting success with no crewmate, is not a usable crew).
Authority is explicit and never inferred: aligns (identity and lock stay refusals when unverifiable; no new autonomy).
Scripts own the mechanics, agents own the judgment: aligns (path canonicalization, pane env, and mode probe are script mechanics).
A restart is a non-event: aligns (teardown that leaves panes and pooled worktrees behind is leftover state after the session ends).
Delegation with a spine: aligns (spawn/teardown is the worker-delivery contract).
The fleet outlives any vendor: aligns (bind to the socket path and pane fields herdr actually emits, not POSIX pixels).
contract-class: restore (herdr launcher-identity, worktree discovery after treehouse get, POSIX spawn into a usable shell, and teardown lock-namespace are already specified; native Windows makes those promised paths unverifiable or false-success. A PR that changes unconfigured POSIX behavior would be new-default and is not requested here.)
Keeping ready-for-pr. Expanded scope noted. Still open.
Second Windows blocker, upstream of the one this issue reports: the session lock can never be acquired
While verifying the herdr fix above on a real workload I hit a defect that sits earlier in the startup path, and it is arguably the more serious of the two: on Git Bash fm-lock.sh can never acquire the session lock, so every session runs read-only and the Claude Stop auto-arm — which only arms while its own session holds the lock — never fires at all.
LOCK
--------------------------------------------------------------------------------
error: cannot locate harness process in ancestry
● READ-ONLY SESSION - FLEET LOCK OWNERSHIP WAS NOT VERIFIED
There is no fleet operation at all from such a session: no spawn, no steer, no merge, no wake drain.
Four independent causes, any one of which alone breaks the ancestry walk
1. Cygwin ps has no -o option.fm_harness_ancestry_pids (bin/fm-session-lock-lib.sh:112) starts with ps -o comm= -p "$pid", and Cygwin's ps (3.5.7) answers:
The walk therefore breaks on its first hop. This one fails loudly.
2. The Cygwin process table lists only MSYS processes. A native harness binary is simply absent from it, and the shell it launched reports PPID 1, so even with a working -o the walk stops at a root that is not the harness. ps -W does list native processes but reports PPID 0 for every one of them, so it cannot rebuild a chain either.
3. MSYS emulates fork with a fresh Windows process, and Windows never reparents. Every subshell has its own Windows pid while $$ keeps naming the script's shell, and ParentProcessId keeps pointing at a pid that has already exited. So a walk that follows the Windows link from a short-lived subshell steps into a dead pid. This one fails silently — measured here, it turned a working chain into a one-hop chain.
4. /proc/<pid>/exename carries no trailing newline, so read reports failure after assigning, and the common read ... || var='' idiom discards the value. Also silent.
There is also a namespace consequence worth stating explicitly, because it is easy to get wrong: Cygwin kill -0 does not resolve a native Windows pid. Measured against a live harness process it returns 1, exactly as it does for a pid that never existed — so kill -0 cannot distinguish a live lock owner from a dead one here, and fm_harness_pid_alive cannot use it.
A second, independent defect this exposed
bin/fm-sessionstart-nudge.sh duplicates the ancestry logic instead of using the shared library — its own kill -0 at :30 and its own ps -o ppid= walk at :33. Both are dead on Windows, so lock_is_in_ancestry always returns false and the nudge prints the session-start instruction on every single turn, even while the lock is held. bin/fm-lock.sh:20-23 describes the shared lib as the single owner of this identity contract; the nudge was quietly outside it.
The shape of the fix I am running
A new bin/fm-ps-lib.sh becomes the single owner of comm/args/ppid/liveness, and the two call sites route through it. The parts that seem worth your attention as design decisions:
Selection is by capability, not platform name.ps -o working wins; only uname saying MSYS/MINGW/CYGWIN andps -o failing takes the Windows path. This matters for your own tests: tests/fm-session-lock-ancestry.test.sh shadows ps with a fake that does support -o, so the unit layer keeps the original POSIX semantics untouched. Gating on uname alone regressed four assertions in that file; capability gating restores them.
On Windows the library speaks WINPID throughout, because a native harness exists only as a WINPID and an ancestry that must reach it cannot be expressed in Cygwin pids.
Parent links for MSYS processes come from the Cygwin table, never from Windows, since the Windows one dangles across fork emulation. Identity for those processes is read from /proc/<cyg>/exename and cmdline, so a harness that runs as a script under MSYS is not hidden behind a generic bash.exe — that matters for the node-hosted adapters, and FM_HARNESS_RE's anchored ^pi$ / ^pi-signed$ alternatives are the case where path-vs-basename is decisive.
The Windows table is crossed into only once the Cygwin chain is exhausted, at the outermost MSYS process, whose Windows parent is real.
Cost, because this runs at every turn end
Making the walk actually work puts fm_harness_process_matches on the Stop hook's hot path for the first time on this platform, and under MSYS each fork is a real Windows process (~85 ms here). The first working version cost 8.5–15 s per walk. Four changes brought it to 2.4 s:
builtins instead of basename and grep in fm_harness_process_matches (verified semantically identical against the current implementation across 14 probe cases, including the anchored pi alternatives in both bare and path form);
one combined per-hop lookup that assigns to caller variables instead of three command substitutions;
PowerShell returning the ancestry of one pid rather than the whole machine (10 rows / 3.7 KB instead of ~340 rows / 83 KB);
fetching that snapshot lazily, from inside the hop lookup, so an ancestry that never leaves MSYS spawns no PowerShell at all.
That last one is order-dependent: lazy fetching was slower before the combined lookup existed, because the fallback then ran inside a command substitution and re-snapshotted on every read.
On the remaining test failures — please do not read them as an incomplete patch
Against tests/fm-session-lock-ancestry.test.sh, fm-claude-stop-autoarm.test.sh, fm-sessionstart-nudge.test.sh and fm-watcher-lock.test.sh, the patched tree is assertion-for-assertion identical to the unpatched tree on this machine: 22 ok, 4 not ok, same four. Nothing that passed before fails now.
Two of those four are POSIX assumptions in the fixtures rather than implementation bugs:
tests/fm-sessionstart-nudge.test.sh:129 writes printf '%s\n' "$$" into state/.lock. On Windows a valid lock holds a WINPID, and a Cygwin pid is not one. Accepting both namespaces is not safe — the two numbering spaces can collide on the same integer while naming different processes.
The ancestry e2e fixture builds its harness as ln -s /bin/bash "$FAKEBIN/claude", i.e. an MSYS process.
I have not touched either fixture; making them platform-aware is your call, and I did not want to change tests to make my own patch look green.
What is verified here
Lock acquires and reports held by live harness pid <n> from a separate shell; fm_session_lock_owned_by_self passes; liveness correctly accepts the harness and rejects both a dead pid and a live non-harness process; session start no longer prints the read-only banner and hands supervision to the Stop-owned auto-arm; the nudge is correctly silent while the lock is held.
Not verified: the ancestry was only exercised under a herdr-hosted chain (pwsh → herdr → herdr → pwsh → cmd → claude); a session launched directly from a terminal has a different shape, including possibly the nested worker chain the library's own comment describes. And the auto-arm is only proven past its identity gate — need_supervision correctly keeps it inert with an empty fleet, so "it arms a watcher end to end" is still untested.
Same offer as before: happy to run this through the no-mistakes gate and raise a proper PR, as one change or split from the herdr work, whichever you prefer. If you would rather these Windows differences not land upstream at all, that is a useful answer too — it tells me to stop maintaining a fork.
Speaking as Kun's firstmate: re-read whole thread including the 2026-08-30 expanded four-defect follow-up and the 2026-08-31 session-lock writeup. Verified still unfixed on latest main 4ad8cbae.
fm_backend_herdr_canonical_socket_path in bin/backends/herdr.sh still requires /* POSIX-absolute then dirname/cd/pwd -P. Native Windows herdr drive-letter sockets on either side of the launcher-identity check still fail closed before comparison.
Open PR #2918 (3min-lang) already converts drive-letter shapes via cygpath -u before identity comparison — the same fail-closed shape this issue's local patch proposes. Removing ready-for-pr and leaving this issue open against #2918 rather than inviting a second socket-path PR.
contract-class: restore (launcher-identity is supposed to compare two spellings of the same socket; the POSIX-only guard makes a same-socket Windows pair unverifiable).
@trngthnh369, do you still plan to raise your windows-herdr-backend patch (d17e85f) as a PR? Asking before anyone else picks it up.
On current main, the open PRs #2918 and #5419 fix only the socket path (1). Folder discovery (2), the pane shell (3) and the mode-700 teardown check (4) are still unfixed, and your commit is the only fix for those three I could find. It is now about 540 commits behind main.
I also checked Herdr 0.9.5, the latest release, from its source. None of the four blockers changed there. foreground_cwd is still always empty on Windows, a pane's cwd is still its own shell's folder, panes still start PowerShell unless [terminal] default_shell is set, and the per-pane env still reaches the pane process. So your active probe and your SHELL/MSYS/PATH approach should still hold. I have not run your branch against 0.9.5.
If you'd rather not continue, would you be OK with someone rebasing it onto main and opening the PR with credit to you?
Speaking as Kun's firstmate: thanks @cr101 for the careful Herdr 0.9.5 source check, and for asking @trngthnh369 before picking anything up — that's exactly the right order.
Re-checked on latest main 3c1e4684: still unfixed. fm_backend_herdr_canonical_socket_path in bin/backends/herdr.sh still refuses anything that isn't /*, and fm_backend_herdr_current_path still reads only .result.pane.foreground_cwd. Nothing has landed for the pane shell or the mode-700 teardown check either.
Folder discovery (2), pane shell (3), mode-700 teardown (4): no open PR covers these. Whether @trngthnh369's branch goes up as-is or someone rebases it with credit is between the two of you. Whoever raises it, please go through git push no-mistakes per CONTRIBUTING.md, keep each fix Windows-gated and fail-closed so POSIX homes stay byte-for-byte the same, and attach live Windows evidence. Don't probe a live agent pane on relaunch, and don't count a cmd.exe spawn as a success.
Nothing is waiting on Kun here.
VISION.md (unchanged from the earlier verdict): aligns on every rule. This is backend-adapter mechanics that let the fleet run on another platform (#504), and identity stays a refusal whenever it can't be verified.
contract-class (socket fix): restore.
Since there has been no reply here in about two weeks, I'm picking up @trngthnh369's windows-herdr-backend fix (d17e85f) for defects 2-4: folder discovery, the pane shell, and the mode-700 teardown check. It is rebased onto current main, with @trngthnh369 credited as co-author on each commit carrying their work.
The socket path (1) stays with #2918 / #5419 and is not included; the PR will note that dependency. I checked #2918 and #5419 first, and neither touches 2-4.
Per the conditions above, it goes through git push no-mistakes, every change is Windows-gated and fail-closed with POSIX unchanged, and it carries live Windows evidence (Herdr 0.9.3 in an isolated lab session: Git Bash pane spawn plus cleanup passing, and a PowerShell pane refused before launch). @trngthnh369, if you would rather raise it yourself, say so and I'll step back.
Speaking as Kun's firstmate: thanks @cr101 for picking up @trngthnh369's branch with co-author credit, and for checking #2918 and #5419 before you built.
Still unfixed on latest main 840cebed. Open PRs that now cover this issue:
Please carry on in those PRs rather than opening another one. Nothing here is waiting on the reporters or on Kun.
VISION.md: my earlier per-rule verdict still holds. Everything aligns, because these are backend-adapter mechanics that let the fleet run on native Windows (#504), and identity is still refused whenever it can't be verified. contract-class for the socket fix: restore.
Activity
kunchenguid commentedon Aug 29, 2026
Speaking as Kun's firstmate: whole thread read (no comments yet; related #504 Windows-herdr intent, #3267 POSIX lock assumption, #1796 nested cmd.exe spawn blocker). Verified still unfixed on latest main (
f66be0f).fm_backend_herdr_canonical_socket_pathinbin/backends/herdr.shstill requires/*POSIX-absolute and thendirname/cd/pwd -P. Native Windows herdr emits a drive-letter path on both the launcher env andsession list --json, sofm_backend_herdr_launcher_identityreturns 1 before the two paths are compared and every herdr-backend spawn is refused. SettingHERDR_SOCKET_PATHto a POSIX path cannot fix the session-list side. No open PR covers this socket canonicalizer. #3231/#3228 are Git Bash watcher-lock, not this. Do not open a competing PR from this pass.A correct fix must stay fail-closed: convert only the drive-letter shape (and only when a converter such as
cygpathexists), re-verify the result is POSIX-absolute, and keep refusing relative/empty/garbage paths so parent identity remains verifiable.VISION.md per-rule:
contract-class: restore (launcher-identity is supposed to compare two spellings of the same socket and allow spawn when they match; the POSIX-only guard makes a same-socket Windows pair unverifiable).
Labeling
ready-for-pr.trngthnh369 commentedon Aug 30, 2026
Following up with the rest of the picture: the socket-path refusal above is the first of four independent defects sitting on one code path on native Windows (Git for Windows / MSYS2, no WSL). Each is only reachable once the previous one is cleared, so none of them can be observed in isolation — which is presumably why they have not surfaced together before.
I have a working patch and a measured result. I am posting it here rather than opening a PR because
CONTRIBUTING.mdrequires PRs tomainto be raised throughno-mistakes. Happy to run it through the gate and raise a proper PR if you want it — just say the word. Equally happy to split it into four PRs if that reviews better.The four defects, in the order a spawn hits them
1. Socket identity refuses every drive-letter path (this issue)
fm_backend_herdr_canonical_socket_pathonly accepts a POSIX-absolute path, but herdr emits a native path on both sides of the launcher identity check, so there is no environment workaround — overridingHERDR_SOCKET_PATHfixes one side, the other comes fromherdr session list --json. Relaxing the guard alone is not enough either:dirname/cd/pwd -Pjust below cannot consume a backslash path.2. Worktree discovery polls a field herdr never populates
fm_backend_herdr_current_pathreads.pane.foreground_cwd. On herdr0.8.0-previewthat field is declared in the API schema as["string","null"]but emitted for no pane at all — measured across 27 panes, inapi snapshot, and with a live foreground process demonstrably sitting in another directory. The poll returns empty forever, sotreehouse get did not enter a worktree within 60scannot be avoided by any timeout value.Neither fallback works:
.pane.cwddoes follow acdin the pane's own shell here, but not into the nested subshelltreehouse getopens; andpane process-info'sforeground_processesresolves to the pane's own shell, since Windows has no tty foreground process group to read.The patch adds an active probe in the same spirit as
fm_backend_zellij_current_path. It is wired only into the treehouse-get discovery loop, via a newspawn_discover_path, and deliberately not intofm_backend_herdr_current_path— that function is also the relaunch reader, relaunch adopts an existing endpoint whose pane may hold a live agent, andpane runsubmits, so probing there would type into that agent's composer. On a platform with noforeground_cwd, relaunch keeps returning empty and refuses, which is the safe outcome.3. Task panes get cmd.exe, and the spawn still exits 0
This is the nastiest one.
treehouse getpicks its subshell from$SHELL; a herdr pane runs the configured default shell with noSHELLset, so treehouse falls back to%COMSPEC%.fm-spawn.shthen types POSIX into cmd.exe:The crewmate never launches, while
fm-spawn.shprintsspawned <id> ...and exits 0.Pinning
SHELLalone is not sufficient: treehouse execsbash.exenon-login, so/etc/profilenever runs and that shell has no msys toolchain at all —ls,headandenvare all missing, which kills the launch line twice over (once directly, once becausefm-operational-input.sh's#!/usr/bin/env bashinterpreter is gone).MSYSmatters too:fm_lock_try_create's primitive isln -s, msys silently copies by default, the copy still carries the owner'spidfile, andfm_lock_acquire_wait(no timeout, no bail-out) then spins forever on a lock nothing can take. Measured in isolation:That is what makes
fm-captain-hold.sh completehang indefinitely. Related to #3279, but a different mechanism — no PID reuse involved. Note that native symlinks additionally need Developer Mode or elevation; without either,nativestrictat least makesln -sfail loudly instead of leaving a booby-trapped directory behind.The patch hands a new pane
SHELL,MSYSand the caller's ownPATH, spliced into every pane-creating call:tab create, bothworkspace createsites, the projection task-tab, and the husk replacement. All five are needed — the projection ones only become reachable once (4) is fixed, and missing them puts cmd.exe back silently.4. Teardown refuses because a mode bit cannot exist
fm_backend_herdr_presentation_lock_namespace_validrequires mode700. Git for Windows mounts/and/tmpnoacl, so a directory always stats755regardless:The check can never pass. Spawn tolerates it (warns, drops the projection); teardown treats it as fatal —
session presentation lock could not be resolved— and leaves the pane and the pooled worktree behind.Relaxing this needed care. My first attempt kept the ownership assertion and dropped only the mode check, which was wrong: on a
noaclmountstatis uninformative in both fields — every directory,/c/Windowsand/c/Program Filesincluded, reports the caller's own uid and755. So the patch skips the mode assertion only when the filesystem provably cannot carry mode bits (probed at runtime, so an acl-mounted path on the same machine keeps the strict check) and re-anchors the guarantee on location: the namespace must sit inside the caller's own temp root, which on Windows is the per-user, ACL-protected%TEMP%.Result
Verified on Windows 11 (10.0.26200), Git for Windows 2.49.0 / msys2 3.5.7, herdr
0.8.0-preview.2026-08-04, firstmate atdebe4bf, against a scout task in a throwaway sandbox repo:scout-probe.txtcommitted in the pooled worktree + report writtenfm-teardown.shitself4 of 5 cycles passed end to end. The one failure lost its crewmate to an
API Error: Connection lost mid-responseafter it had already committed in the worktree — not a firstmate defect.Notes for review
cygpathplus a drive-letter shape, andfm_backend_herdr_posix_envprints nothing elsewhere, so no--envis passed and no probe runs..gitattributes; my tree iscore.autocrlf=trueand the committed blobs contain no CR, but worth a glance.tests/fm-backend-herdr.test.shhas one pre-existing failure on a cleanorigin/mainhere —the ambiguity refusal did not name the candidate workspaces (missing: 'w1 w7'). Unrelated to this change, but flagging it since it shows up in any Windows run.bin/fm-lint.shneeds ShellCheck, which I have not installed, so the patch is unlinted.tests/fm-backend-herdr.test.shalready mockspane getJSON, so aforeground_cwd-absent case and acanonical_socket_pathtable would slot straight in.The branch is on my fork as
trngthnh369/firstmate:windows-herdr-backend(one commit, +220/-7, applies cleanly todebe4bf). Tell me which you prefer — a PR throughno-mistakes, the patch inline here, or nothing at all if you would rather solve these differently.kunchenguid commentedon Aug 30, 2026
Speaking as Kun's firstmate: whole thread re-read, including the 2026-08-30T08:55:43Z follow-up. Verified still unfixed on latest main (
debe4bfafa2ad52bd96f088a8d8ba418931e828f). No open PR covers this spawn path. Do not open a competing PR from this pass.The follow-up expands this from one refusal to four independent defects on the same native-Windows herdr spawn/teardown path (Git for Windows / MSYS2, no WSL). Each is only reachable after the previous one is cleared. Inspected on main:
fm_backend_herdr_canonical_socket_pathinbin/backends/herdr.shstill requires/*thendirname/cd/pwd -P. No drive-letter/cygpathconversion. Both launcherHERDR_SOCKET_PATHandsession list --jsonstill fail the guard before comparison, matching the original error.fm_backend_herdr_current_pathstill only reads.result.pane.foreground_cwd. Nospawn_discover_path(or zellij-style probe) exists.fm_backend_herdr_create_task, projection task-tab create, and husk replacement all calltab createwith--cwd/--label/--no-focusonly — noSHELL/MSYS/PATH. A herdr pane can still land in cmd.exe so POSIX spawn types into cmd and exits 0. The MSYSln -ssilent-copy hang overlaps Windows: fm_lock_try_create can never succeed without symlink privilege (ln -s silently degrades to a copy) #3267.fm_backend_herdr_presentation_lock_namespace_validstill requires owner uid AND mode700. No noacl/runtime probe, so a Git-for-Windowsnoacl/tmpcan never satisfy teardown.Related and still distinct: #504 (Windows-herdr intent), #3267 (lock
ln -s), #1796 (nested cmd.exe cwd). The author's fork branchtrngthnh369/firstmate:windows-herdr-backendis not an open PR against this repo.A correct fix must stay fail-closed and Windows-gated so POSIX is unchanged: convert only the drive-letter socket shape when a converter exists, then re-verify POSIX-absolute; keep parent identity a refusal when unverifiable; do not type a probe into a live agent pane on relaunch; do not treat a cmd.exe spawn as success; skip mode
700only when the filesystem cannot carry mode bits, re-anchored on location inside the caller's temp root.Yes — raise through
no-mistakesper CONTRIBUTING.md. One PR or four is your call; keep each defect independently fail-closed. Do not skip the gate. Firstmate will not open a competing PR.VISION.md per-rule:
contract-class: restore (herdr launcher-identity, worktree discovery after
treehouse get, POSIX spawn into a usable shell, and teardown lock-namespace are already specified; native Windows makes those promised paths unverifiable or false-success. A PR that changes unconfigured POSIX behavior would be new-default and is not requested here.)Keeping
ready-for-pr. Expanded scope noted. Still open.trngthnh369 commentedon Aug 31, 2026
Second Windows blocker, upstream of the one this issue reports: the session lock can never be acquired
While verifying the herdr fix above on a real workload I hit a defect that sits earlier in the startup path, and it is arguably the more serious of the two: on Git Bash
fm-lock.shcan never acquire the session lock, so every session runs read-only and the Claude Stop auto-arm — which only arms while its own session holds the lock — never fires at all.There is no fleet operation at all from such a session: no spawn, no steer, no merge, no wake drain.
Four independent causes, any one of which alone breaks the ancestry walk
1. Cygwin
pshas no-ooption.fm_harness_ancestry_pids(bin/fm-session-lock-lib.sh:112) starts withps -o comm= -p "$pid", and Cygwin'sps(3.5.7) answers:The walk therefore
breaks on its first hop. This one fails loudly.2. The Cygwin process table lists only MSYS processes. A native harness binary is simply absent from it, and the shell it launched reports
PPID 1, so even with a working-othe walk stops at a root that is not the harness.ps -Wdoes list native processes but reportsPPID 0for every one of them, so it cannot rebuild a chain either.3. MSYS emulates
forkwith a fresh Windows process, and Windows never reparents. Every subshell has its own Windows pid while$$keeps naming the script's shell, andParentProcessIdkeeps pointing at a pid that has already exited. So a walk that follows the Windows link from a short-lived subshell steps into a dead pid. This one fails silently — measured here, it turned a working chain into a one-hop chain.4.
/proc/<pid>/exenamecarries no trailing newline, soreadreports failure after assigning, and the commonread ... || var=''idiom discards the value. Also silent.There is also a namespace consequence worth stating explicitly, because it is easy to get wrong: Cygwin
kill -0does not resolve a native Windows pid. Measured against a live harness process it returns 1, exactly as it does for a pid that never existed — sokill -0cannot distinguish a live lock owner from a dead one here, andfm_harness_pid_alivecannot use it.A second, independent defect this exposed
bin/fm-sessionstart-nudge.shduplicates the ancestry logic instead of using the shared library — its ownkill -0at:30and its ownps -o ppid=walk at:33. Both are dead on Windows, solock_is_in_ancestryalways returns false and the nudge prints the session-start instruction on every single turn, even while the lock is held.bin/fm-lock.sh:20-23describes the shared lib as the single owner of this identity contract; the nudge was quietly outside it.The shape of the fix I am running
A new
bin/fm-ps-lib.shbecomes the single owner of comm/args/ppid/liveness, and the two call sites route through it. The parts that seem worth your attention as design decisions:ps -oworking wins; onlyunamesaying MSYS/MINGW/CYGWIN andps -ofailing takes the Windows path. This matters for your own tests:tests/fm-session-lock-ancestry.test.shshadowspswith a fake that does support-o, so the unit layer keeps the original POSIX semantics untouched. Gating onunamealone regressed four assertions in that file; capability gating restores them./proc/<cyg>/exenameandcmdline, so a harness that runs as a script under MSYS is not hidden behind a genericbash.exe— that matters for the node-hosted adapters, andFM_HARNESS_RE's anchored^pi$/^pi-signed$alternatives are the case where path-vs-basename is decisive.Cost, because this runs at every turn end
Making the walk actually work puts
fm_harness_process_matcheson the Stop hook's hot path for the first time on this platform, and under MSYS each fork is a real Windows process (~85 ms here). The first working version cost 8.5–15 s per walk. Four changes brought it to 2.4 s:basenameandgrepinfm_harness_process_matches(verified semantically identical against the current implementation across 14 probe cases, including the anchoredpialternatives in both bare and path form);That last one is order-dependent: lazy fetching was slower before the combined lookup existed, because the fallback then ran inside a command substitution and re-snapshotted on every read.
On the remaining test failures — please do not read them as an incomplete patch
Against
tests/fm-session-lock-ancestry.test.sh,fm-claude-stop-autoarm.test.sh,fm-sessionstart-nudge.test.shandfm-watcher-lock.test.sh, the patched tree is assertion-for-assertion identical to the unpatched tree on this machine: 22 ok, 4 not ok, same four. Nothing that passed before fails now.Two of those four are POSIX assumptions in the fixtures rather than implementation bugs:
tests/fm-sessionstart-nudge.test.sh:129writesprintf '%s\n' "$$"intostate/.lock. On Windows a valid lock holds a WINPID, and a Cygwin pid is not one. Accepting both namespaces is not safe — the two numbering spaces can collide on the same integer while naming different processes.ln -s /bin/bash "$FAKEBIN/claude", i.e. an MSYS process.I have not touched either fixture; making them platform-aware is your call, and I did not want to change tests to make my own patch look green.
What is verified here
Lock acquires and reports
held by live harness pid <n>from a separate shell;fm_session_lock_owned_by_selfpasses; liveness correctly accepts the harness and rejects both a dead pid and a live non-harness process; session start no longer prints the read-only banner and hands supervision to the Stop-owned auto-arm; the nudge is correctly silent while the lock is held.Not verified: the ancestry was only exercised under a herdr-hosted chain (
pwsh → herdr → herdr → pwsh → cmd → claude); a session launched directly from a terminal has a different shape, including possibly the nested worker chain the library's own comment describes. And the auto-arm is only proven past its identity gate —need_supervisioncorrectly keeps it inert with an empty fleet, so "it arms a watcher end to end" is still untested.Same offer as before: happy to run this through the
no-mistakesgate and raise a proper PR, as one change or split from the herdr work, whichever you prefer. If you would rather these Windows differences not land upstream at all, that is a useful answer too — it tells me to stop maintaining a fork.kunchenguid commentedon Aug 31, 2026
Speaking as Kun's firstmate: re-read whole thread including the 2026-08-30 expanded four-defect follow-up and the 2026-08-31 session-lock writeup. Verified still unfixed on latest main
4ad8cbae.fm_backend_herdr_canonical_socket_pathinbin/backends/herdr.shstill requires/*POSIX-absolute thendirname/cd/pwd -P. Native Windows herdr drive-letter sockets on either side of the launcher-identity check still fail closed before comparison.Open PR #2918 (3min-lang) already converts drive-letter shapes via
cygpath -ubefore identity comparison — the same fail-closed shape this issue's local patch proposes. Removingready-for-prand leaving this issue open against #2918 rather than inviting a second socket-path PR.Newer activity notes:
VISION.md per-rule:
contract-class: restore (launcher-identity is supposed to compare two spellings of the same socket; the POSIX-only guard makes a same-socket Windows pair unverifiable).
fix(herdr): make the herdr backend work on native Windows
devin-ai-integration commentedon Sep 24, 2026
cr101 commentedon Oct 25, 2026
@trngthnh369, do you still plan to raise your
windows-herdr-backendpatch (d17e85f) as a PR? Asking before anyone else picks it up.On current main, the open PRs #2918 and #5419 fix only the socket path (1). Folder discovery (2), the pane shell (3) and the mode-700 teardown check (4) are still unfixed, and your commit is the only fix for those three I could find. It is now about 540 commits behind main.
I also checked Herdr 0.9.5, the latest release, from its source. None of the four blockers changed there.
foreground_cwdis still always empty on Windows, a pane'scwdis still its own shell's folder, panes still start PowerShell unless[terminal] default_shellis set, and the per-paneenvstill reaches the pane process. So your active probe and your SHELL/MSYS/PATH approach should still hold. I have not run your branch against 0.9.5.If you'd rather not continue, would you be OK with someone rebasing it onto main and opening the PR with credit to you?
kunchenguid commentedon Oct 25, 2026
Speaking as Kun's firstmate: thanks @cr101 for the careful Herdr 0.9.5 source check, and for asking @trngthnh369 before picking anything up — that's exactly the right order.
Re-checked on latest main
3c1e4684: still unfixed.fm_backend_herdr_canonical_socket_pathinbin/backends/herdr.shstill refuses anything that isn't/*, andfm_backend_herdr_current_pathstill reads only.result.pane.foreground_cwd. Nothing has landed for the pane shell or the mode-700 teardown check either.Where it stands:
existing-pr— fix(bin): canonicalize Windows drive-letter herdr socket paths before identity comparison #2918 (and the socket part of fix(bin): run the session lock, herdr spawns, and claim locks on native Windows #5419) cover it. Please don't open a third socket-path PR.git push no-mistakesper CONTRIBUTING.md, keep each fix Windows-gated and fail-closed so POSIX homes stay byte-for-byte the same, and attach live Windows evidence. Don't probe a live agent pane on relaunch, and don't count a cmd.exe spawn as a success.Nothing is waiting on Kun here.
VISION.md (unchanged from the earlier verdict): aligns on every rule. This is backend-adapter mechanics that let the fleet run on another platform (#504), and identity stays a refusal whenever it can't be verified.
contract-class (socket fix): restore.
cr101 commentedon Nov 5, 2026
Since there has been no reply here in about two weeks, I'm picking up @trngthnh369's
windows-herdr-backendfix (d17e85f) for defects 2-4: folder discovery, the pane shell, and the mode-700 teardown check. It is rebased onto current main, with @trngthnh369 credited as co-author on each commit carrying their work.The socket path (1) stays with #2918 / #5419 and is not included; the PR will note that dependency. I checked #2918 and #5419 first, and neither touches 2-4.
Per the conditions above, it goes through
git push no-mistakes, every change is Windows-gated and fail-closed with POSIX unchanged, and it carries live Windows evidence (Herdr 0.9.3 in an isolated lab session: Git Bash pane spawn plus cleanup passing, and a PowerShell pane refused before launch). @trngthnh369, if you would rather raise it yourself, say so and I'll step back.kunchenguid commentedon Nov 5, 2026
Speaking as Kun's firstmate: thanks @cr101 for picking up @trngthnh369's branch with co-author credit, and for checking #2918 and #5419 before you built.
Still unfixed on latest main
840cebed. Open PRs that now cover this issue:Please carry on in those PRs rather than opening another one. Nothing here is waiting on the reporters or on Kun.
VISION.md: my earlier per-rule verdict still holds. Everything aligns, because these are backend-adapter mechanics that let the fleet run on native Windows (#504), and identity is still refused whenever it can't be verified. contract-class for the socket fix: restore.