fix(hooks): resolve Git Bash by absolute path on Windows - #639
Merged
Merged
Conversation
On Windows, CreateProcess resolves a bare `bash` to %SystemRoot%\System32\bash.exe (the WSL launcher) before any PATH entry, so every rendered `bash -lc "teamai hook-dispatch ..."` hook ran inside WSL instead of Git Bash. There the user's npm-global `teamai`/Node 20 are typically absent, and the trailing `2>/dev/null || true` silently swallowed the failure, so hooks appeared to succeed but did nothing. ZCode already sidesteps this via a `cmd /c` fallback and WorkBuddy/CodeBuddy use a PATH-prefix wrapper, but shell-string tools had no protection. On win32 we now emit the resolved Git Bash path (quoted, forward slashes so it stays JSON-safe and tolerates the "Program Files" space), discovered from the standard install locations first, then the HKLM GitForWindows InstallPath. When Git truly is not found we fall back to bare `bash` (the old form was already inert there). POSIX keeps the bare `bash`, so the golden fixtures stay byte-identical. The golden comparison is skipped on win32 since the rendered path is machine-specific.
|
Blocking Findings
Additional Findings
|
… Windows getDispatchCommand() now prefixes the launcher with a quoted Git Bash path on Windows, which COPILOT_BUILTIN_COMMAND_RE did not recognize — the raw bash command landed verbatim in the powershell field, where PowerShell cannot parse a quoted executable without the call operator (&). Accept the quoted launcher in the regex and render it back with & in front and ; exit 0 in place of || true; the bare-bash form keeps the existing POSIX output byte-for-byte. Add a Windows rendering test that asserts both fields exactly (stringContaining could not catch this).
The suite-level skipIf(win32) also dropped codebuddy and workbuddy, whose wrapper dispatch commands stay machine-independent — restore their compatibility coverage by skipping only the getDispatchCommand cases (claude, claude-internal, cursor) whose rendered launcher differs per machine on Windows.
Document the absolute-path Git Bash dispatch on Windows in both languages, and stop presenting the WSL-bash sidestep as a ZCode-only perk now that every shell-string target avoids the same trap.
Contributor
Author
|
Follow-up commits pushed to this branch, addressing all four findings:
Also disclosed in the PR description: the full local |
|
The PR description includes a detailed test plan and real-CLI Windows e2e record, so no testing-description blocker. |
A 32-bit Node process on 64-bit Windows resolves %ProgramFiles% to the x86 tree, so a standard 64-bit Git install stayed undiscovered and hooks degraded to bare bash (the WSL launcher). Cover it with a test.
The PR description includes a detailed test plan and real-CLI Windows e2e record, so its testing documentation is sufficient. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Windows, rendered hook commands use a bare
bash, whichCreateProcessresolves toSystem32\bash.exe(the WSL launcher) before anyPATHentry. The hook then runs in WSL where the user's globalteamai/Node are typically absent, and2>/dev/null || truehides the failure — so hooks silently no-op. This resolves Git Bash to an absolute path on win32 (quoted, forward slashes) while leaving POSIX output byte-identical.Follow-up commits address the automated review findings:
aeca182— Copilotpowershellfield:copilotPowershellCommand()now recognizes the quoted Git Bash launcher and renders& "D:/…/bash.exe" -lc "teamai hook-dispatch … 2>/dev/null"; exit 0— PowerShell requires the call operator (&) before a quoted executable, and|| truemaps to; exit 0. The bare-bashform keeps the previous PowerShell output byte-for-byte. Adds a Windows rendering test asserting both fields exactly (the previousstringContainingassertion could not catch this regression).42553c1— Golden coverage narrowed: only the machine-dependent dispatch cases (claude, claude-internal, cursor) skip on win32; the wrapper renderers (codebuddy, workbuddy) keep their byte-identical compatibility coverage on Windows.cf74c3b—%ProgramW6432%discovery: a 32-bit Node on 64-bit Windows resolves%ProgramFiles%to the x86 tree; the discovery now also probes the 64-bit directory (with a test).ddf982a— Docs synced (usage-guide.md+usage-guide.zh-CN.md): the Hooks section documents the Windows Git Bash resolution, and the ZCode bullet no longer presents the WSL-bash sidestep as ZCode-only.Type of Change
Test Plan
npx tsc --noEmitpassesnpx vitest runpasses on CI (ubuntu/macos); win32-local failures disclosed in Notes — all pre-existingnpm run build+ real-CLI e2e on Windows (record below)Verification run locally (Windows 25H2 / Node v22.17.1, Git Bash resolved at
D:/Program Files/Git/bin/bash.exe):npx tsc --noEmit→ exit 0.npx vitest run src/__tests__/hooks-windows-bash.test.ts→ 7/7 pass.npx vitest run src/__tests__/copilot-adapter.test.ts→ 16/16 pass, including the new "Windows Git Bash launcher rendering" case asserting exactly:bash/command="C:/…/bash.exe" -lc "teamai hook-dispatch session-start --tool copilot 2>/dev/null" || truepowershell=& "C:/…/bash.exe" -lc "teamai hook-dispatch session-start --tool copilot 2>/dev/null"; exit 0npx vitest run src/__tests__/hooks-golden.test.tson win32 → 2 passed (codebuddy, workbuddy fixtures byte-identical), 3 skipped (claude, claude-internal, cursor — machine-specific launcher).npm run build→ tsup success.Real-CLI e2e (sandboxed
HOME/COPILOT_HOME, no real tool configs touched):npm link→teamai --version→ 0.22.0.teamai hooks inject→ writes~/.claude/settings.json,~/.cursor/hooks.json,$COPILOT_HOME/hooks/teamai.json. The generated Copilot entry:bash/command="D:/Program Files/Git/bin/bash.exe" -lc "teamai hook-dispatch session-start --tool copilot 2>/dev/null" || truepowershell=& "D:/Program Files/Git/bin/bash.exe" -lc "teamai hook-dispatch session-start --tool copilot 2>/dev/null"; exit 0powershellfield verbatim under Windows PowerShell (powershell -NoProfile -File …) → parses and exits 0 (this field was unparsable before the follow-up commit).bashfield under Git Bash → exit 0.teamai hooks inject→ byte-identical files (idempotent);teamai hooks listreports claude / cursor / copilot installed.Related Issues
Closes #638
Notes for Reviewers
src/hooks.ts— shell-string tools just never got the same protection. WorkBuddy/CodeBuddy (wrapper) are untouched; ZCode now launches via its hidden wscript VBS launcher (merged from main).ProgramFiles,ProgramFiles(x86),LOCALAPPDATA, per-userAppData\Local\Programs\Git) first, then theHKLM\SOFTWARE\GitForWindowsInstallPathfor custom installs. When Git genuinely isn't found we fall back to barebash(the old form was already inert there) and log at debug._winBashLauncherCacheand reset by_resetShellCache();queryGitInstallPathis a thinreg.exewrapper andfindGitBashWindowstakes an injectablereadInstallPathso tests don't shell out to the real registry.$, backticks or embedded double quotes), so echoing it inside a PowerShell double-quoted string is interpolation-safe.getHookStatuscompares against the sametoCopilotEntryoutput, so reconcile/status stay consistent (covered by the new idempotency assertion).docs/+README*for the old wording — only the ZCode bullet presented the WSL sidestep as ZCode-specific; the team-hooks YAML examples are user-declared commands rendered verbatim and unaffected;docs/designs/checked, no update needed.vitest run(Windows 25H2,core.autocrlf=falsecheckout — the CRLF pitfall/.gitattributesdocuments — and--no-file-parallelism): 33 test files / 119 tests fail on this branch (3268 pass, 3 skipped = the designed golden skips). The identical 33-file failure set reproduces on a clean checkout of the base commit470d229on the same machine (122 tests there; ±3 is spawn/timing flakiness). The failures are Windows-environment issues unrelated to this change (POSIX-path assertions like'/home/testuser', unix permission bits438 vs 384, real spawn/clone timing) — none touch Git Bash resolution, the Copilotpowershellfield, or golden fixture output. CI (ubuntu/macos) is green.