Skip to content

fix(hooks): resolve Git Bash by absolute path on Windows - #639

Merged
jeff-r2026 merged 6 commits into
Tencent:mainfrom
BuXiuDN:fix/windows-hook-bash-wsl
Sep 20, 2026
Merged

jeff-r2026 merged 6 commits into
Tencent:mainfrom
BuXiuDN:fix/windows-hook-bash-wsl

Conversation

@BuXiuDN

@BuXiuDN BuXiuDN commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

On Windows, rendered hook commands use a bare bash, which CreateProcess resolves to System32\bash.exe (the WSL launcher) before any PATH entry. The hook then runs in WSL where the user's global teamai/Node are typically absent, and 2>/dev/null || true hides 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:

  • aeca182Copilot powershell field: 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 || true maps to ; exit 0. The bare-bash form keeps the previous PowerShell output byte-for-byte. Adds a Windows rendering test asserting both fields exactly (the previous stringContaining assertion could not catch this regression).
  • 42553c1Golden 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).
  • ddf982aDocs 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

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

  • npx tsc --noEmit passes
  • npx vitest run passes on CI (ubuntu/macos); win32-local failures disclosed in Notes — all pre-existing
  • Added/updated tests for the change
  • npm 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" || true
    • powershell = & "C:/…/bash.exe" -lc "teamai hook-dispatch session-start --tool copilot 2>/dev/null"; exit 0
    • and a byte-identical second reconcile (idempotency).
  • npx vitest run src/__tests__/hooks-golden.test.ts on 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 linkteamai --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" || true
    • powershell = & "D:/Program Files/Git/bin/bash.exe" -lc "teamai hook-dispatch session-start --tool copilot 2>/dev/null"; exit 0
  • Executed the generated powershell field verbatim under Windows PowerShell (powershell -NoProfile -File …) → parses and exits 0 (this field was unparsable before the follow-up commit).
  • Executed the generated bash field under Git Bash → exit 0.
  • A second teamai hooks inject → byte-identical files (idempotent); teamai hooks list reports claude / cursor / copilot installed.

Related Issues

Closes #638

Notes for Reviewers

  • The win32 fix mirrors the rationale already documented for ZCode's launcher fallback in 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).
  • Discovery order: standard install locations (ProgramFiles, ProgramFiles(x86), LOCALAPPDATA, per-user AppData\Local\Programs\Git) first, then the HKLM\SOFTWARE\GitForWindows InstallPath for custom installs. When Git genuinely isn't found we fall back to bare bash (the old form was already inert there) and log at debug.
  • The result is cached in _winBashLauncherCache and reset by _resetShellCache(); queryGitInstallPath is a thin reg.exe wrapper and findGitBashWindows takes an injectable readInstallPath so tests don't shell out to the real registry.
  • Copilot conversion detail: the inner dispatch command is builtin-generated (no $, backticks or embedded double quotes), so echoing it inside a PowerShell double-quoted string is interpolation-safe. getHookStatus compares against the same toCopilotEntry output, so reconcile/status stay consistent (covered by the new idempotency assertion).
  • Docs: grepped 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.
  • Transparency on the full local vitest run (Windows 25H2, core.autocrlf=false checkout — the CRLF pitfall /.gitattributes documents — 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 commit 470d229 on 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 bits 438 vs 384, real spawn/clone timing) — none touch Git Bash resolution, the Copilot powershell field, or golden fixture output. CI (ubuntu/macos) is green.

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.
@jeff-r2026 jeff-r2026 self-assigned this Sep 18, 2026
@github-actions

Copy link
Copy Markdown

Blocking Findings

  • [P1] Copilot’s PowerShell hook becomes invalid on Windowssrc/builtin-hooks.ts:243 now prefixes the command with a quoted Git Bash path. However, copilotPowershellCommand() only recognizes commands beginning with literal bash -lc, so Copilot’s powershell field receives "C:/Program Files/Git/bin/bash.exe" -lc .... PowerShell requires the call operator (&) before a quoted executable path, so these hooks fail to parse. Update the Copilot conversion and add a Windows Copilot rendering test.
  • [P1] Required real-CLI end-to-end verification is missing — The PR description lists type-checking and Vitest runs only. It does not record npm run build followed by real CLI verification, as explicitly required by the repository’s Code Review Rules. This is blocking until an actual e2e/test-plan record is included.

Additional Findings

  • [P2] Windows golden coverage is unnecessarily removedsrc/__tests__/hooks-golden.test.ts:31 skips the entire suite, although CodeBuddy and WorkBuddy still use getWrapperDispatchCommand() and remain machine-independent. Skip only affected cases so unchanged Windows renderers retain compatibility coverage.
  • [P2] Behavior documentation was not updated — This changes user-visible Windows hook execution but updates neither docs/usage-guide.md nor docs/usage-guide.zh-CN.md, violating the repository requirement to keep affected bilingual documentation synchronized.

… 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.
@BuXiuDN

BuXiuDN commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up commits pushed to this branch, addressing all four findings:

  • [P1] Copilot's PowerShell hook invalid on Windows — fixed in aeca182: copilotPowershellCommand() now recognizes the quoted Git Bash launcher and renders & "D:/…/bash.exe" -lc "teamai hook-dispatch … 2>/dev/null"; exit 0 — the & call operator before the quoted executable, with || true mapped to ; exit 0. The bare-bash form keeps the previous PowerShell output byte-for-byte. Added a Windows Copilot rendering test (copilot-adapter.test.ts) asserting bash/powershell exactly — the previous stringContaining assertion could not catch this regression.
  • [P1] Real-CLI end-to-end verification missing — recorded in the PR description: npm run build, then real-CLI e2e on Windows in a sandboxed HOME/COPILOT_HOME: teamai hooks inject → generated files inspected; the generated powershell field executed verbatim under Windows PowerShell → exit 0 (unparsable before this fix); the generated bash field executed under Git Bash → exit 0; a second inject is byte-identical (idempotent).
  • [P2] Windows golden coverage removed — fixed in 42553c1: only the machine-dependent dispatch cases (claude, claude-internal, cursor) skip on win32; codebuddy/workbuddy keep their byte-identical compatibility coverage and now pass on Windows.
  • [P2] Behavior documentation not updated — fixed in ddf982a: docs/usage-guide.md + docs/usage-guide.zh-CN.md document the Windows Git Bash resolution in the Hooks section, and the ZCode bullet no longer presents the WSL sidestep as ZCode-only.

Also disclosed in the PR description: the full local vitest run on Windows (serial, core.autocrlf=false checkout) shows 33 test files / 119 tests failing — the identical failure-file set reproduces on a clean checkout of the base commit 470d229 on the same machine, so these are pre-existing Windows-environment failures unrelated to this change. CI (ubuntu/macos) is green.

@github-actions

Copy link
Copy Markdown
  • Blocking — Missing required end-to-end verification. The Test Plan documents type-checking and Vitest only; it does not record npm run build followed by real-CLI testing. Per the repository’s review rules, the PR must include actual end-to-end results covering Claude, Codex, CodeBuddy, and OpenCode, plus the git, gitlab, and github providers. The targeted Windows unit test is not a substitute for executing the generated hooks through a real client.

@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] src/builtin-hooks.ts:199 — Windows discovery omits %ProgramW6432%. Under a 32-bit Node process on 64-bit Windows, %ProgramFiles% points to Program Files (x86), so a standard 64-bit Git installation in C:\Program Files\Git may remain undiscovered and hooks fall back to the broken WSL bash. Include ProgramW6432\Git\bin\bash.exe and cover this environment combination with a test.

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.
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Probe %ProgramW6432% when locating Git Bash — src/builtin-hooks.ts:199. Under 32-bit Node on 64-bit Windows, %ProgramFiles% and the registry view point to x86 locations, so a standard 64-bit Git installation under C:\Program Files\Git is missed. The generated hook falls back to bare bash and silently reproduces the WSL-launcher failure this PR intends to fix. Add env.ProgramW6432 to the candidates and cover this environment in the Windows test.

The PR description includes a detailed test plan and real-CLI Windows e2e record, so its testing documentation is sufficient.

@jeff-r2026
jeff-r2026 merged commit 26eb0df into Tencent:main Sep 20, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Windows hooks use bare ash and silently resolve to the WSL launcher (no-op)

2 participants