Skip to content

fix(hooks): run hooks through sh on Windows when in a POSIX shell env - #120

Merged
timvw merged 1 commit into
mainfrom
fix/hooks-posix-shell-on-windows
Aug 10, 2026
Merged

fix(hooks): run hooks through sh on Windows when in a POSIX shell env#120
timvw merged 1 commit into
mainfrom
fix/hooks-posix-shell-on-windows

Conversation

@timvw

@timvw timvw commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Fixes #118.

The decision

Both open questions from the issue, resolved as recommended there:

Policy — option 1 (POSIX shell only when we are demonstrably in one). On Windows, sh -c is used when isPOSIXShellEnv() (from #113) reports a POSIX environment and sh resolves on PATH; otherwise cmd /c stays. Nothing changes for PowerShell/cmd users, so anyone with %WT_PATH%-flavoured hooks is unaffected. A missing sh falls back to cmd rather than failing every hook at exec time.

The wrinkle — convert in-process, to mixed form C:/a/b, not /c/a/b. The issue is right that picking sh fixes nothing on its own: cd $WT_PATH in bash eats the backslashes as escapes. WT_PATH and WT_MAIN are converted when the hook shell is POSIX. Mixed form rather than the /c/a/b sketched in the issue, because it is strictly wider:

C:/a/b /c/a/b
Git Bash / MSYS2 builtins
Cygwin builtins ✗ — drives mount at /cygdrive/c
native tool invoked by the hook (code $WT_PATH, npm) relies on MSYS argv mangling

Backslashes are what actually break the hook, so swapping the separator is the whole fix — no drive-letter special-casing, and already-POSIX paths pass through unchanged. WT_BRANCH is deliberately left alone: a branch may legitimately contain a backslash and is not a path.

No cygpath dependency, and no hooks.shell config key — not needed to close this.

Changes

  • cmd/hooks.gohookShell() picks the interpreter; adaptHookEnv() + toPOSIXPath() adapt the path-valued vars. Both take goos explicitly, matching the detectShell(args, goos) convention, and hookShell takes lookPath so the "POSIX env but no sh" branch is testable off Windows.
  • cmd/hooks_test.go — new: interpreter selection across Git Bash / Cygwin / PowerShell / no-sh, path conversion, and env adaptation (including that the caller's map is not mutated).
  • e2e/scenarios/hooks.yaml — the five scenarios lose skip_os: [windows] for skip_shells: [powershell, pwsh].
  • docs/configuration.md — the "sh -c (or cmd /c on Windows)" footnote replaced with a table of which shell runs a hook where, the exact rule, the path form, and a cmd-syntax counterexample. docs/examples.md gets a note at the top pointing there.

Verifying

One thing the issue did not anticipate: the five scenarios also needed their assertion steps moved from $WORKTREE_ROOT to $WORKTREE_ROOT_POSIX. Those steps run test -f / cat in the harness shell, and on Windows $WORKTREE_ROOT is the native form — so they would have failed on backslashes even with the fix in place, exactly the native/POSIX split #116 introduced.

Per the issue, read the counts rather than the tick — E2E (windows, bash) should go 96 passed / 9 skipped → 101 / 4, with the PowerShell legs unchanged at 71/34 and 72/33. copy_env_file_when_exists is the scenario that actually proves the fix; its sibling copy_env_file_skipped_when_missing would pass either way.

Locally on macOS: 310 e2e passed / 0 failed / 23 skipped. go test ./... passes except TestBashTabCompletionForCheckoutBranch and TestBashTabCompletionForCommands, which fail identically on a clean main here (bash 3.2).

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 37.71%. Comparing base (4984f16) to head (afa04b2).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #120      +/-   ##
==========================================
+ Coverage   36.89%   37.71%   +0.82%     
==========================================
  Files          28       28              
  Lines        3201     3216      +15     
==========================================
+ Hits         1181     1213      +32     
+ Misses       1934     1913      -21     
- Partials       86       90       +4     
Files with missing lines Coverage Δ
cmd/hooks.go 89.18% <100.00%> (+26.47%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@timvw

timvw commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

CI verified — counts, not the tick

All checks green. Reading the leg summaries as #118 asks, since a scenario that silently keeps skipping is indistinguishable from a passing one in the check status:

Leg main this PR
E2E (windows, bash) 102 passed / 11 skipped 107 / 6 +5 / −5 ✅
E2E (windows, pwsh) 78 / 35 78 / 35 unchanged ✅
E2E (windows, powershell) 77 / 36 77 / 36 unchanged ✅

The absolute numbers differ from the 96→101 and 71/34, 72/33 predicted in the issue — that baseline predates #117 adding wt cd. The delta is exactly the +5/−5 predicted, and the PowerShell legs are untouched, which is the property that actually matters: the conservative policy did not change behaviour for anyone outside a POSIX shell env.

All five scenarios pass by name in E2E (windows, bash):

PASS: hooks/post_create_hook_runs
PASS: hooks/post_checkout_hook_runs
PASS: hooks/checkout_hooks_run_on_existing_worktree
PASS: hooks/copy_env_file_when_exists
PASS: hooks/copy_env_file_skipped_when_missing

copy_env_file_when_exists is the one that proves the fix — it asserts the .env contents actually arrived in the worktree, so it can only pass if test -f, cp, $WT_MAIN and $WT_PATH all worked under a real Git Bash. (Its sibling would have passed either way, as the issue notes.)

Unit Tests (windows) also passes, so hookShell/adaptHookEnv are exercised on a real Windows host, not just via the injected goos.

wt spawns hook commands itself, so the shell the user is sitting in never
had a say in what ran them: on Windows every hook went through `cmd /c`.
That breaks essentially every hook we document, since `cmd` expands
neither `$WT_PATH` nor provides `test`, `cp` or `&&`. Post-hooks warned to
stderr and reported success, so a Git Bash user's .env was silently not
copied; pre-hooks aborted the operation naming a command that looks
perfectly valid.

Reuse isPOSIXShellEnv() from #113: on Windows, use `sh -c` only when we
can see a POSIX environment and find an sh to run it with, otherwise keep
`cmd /c`. PowerShell and cmd users who wrote %WT_PATH% hooks are
unaffected.

Choosing sh is not enough on its own — the hook vars hold native paths,
and `cd $WT_PATH` in bash eats the backslashes as escapes. WT_PATH and
WT_MAIN are therefore converted to mixed form (C:\a\b -> C:/a/b) when the
hook shell is POSIX. Mixed form over /c/a/b so it also works under Cygwin
(which mounts drives at /cygdrive) and survives being passed to a native
tool the hook invokes, e.g. `code $WT_PATH`.

The five hooks.yaml scenarios skipped for this exact cause are enabled,
with their assertions moved to $WORKTREE_ROOT_POSIX so they read paths in
the harness shell's own form.

Fixes #118
@timvw
timvw force-pushed the fix/hooks-posix-shell-on-windows branch from b1d0474 to afa04b2 Compare August 10, 2026 20:58
@timvw
timvw enabled auto-merge (squash) August 10, 2026 20:58
@timvw
timvw merged commit 253789e into main Aug 10, 2026
16 checks passed
@timvw
timvw deleted the fix/hooks-posix-shell-on-windows branch August 10, 2026 21:03
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.

hooks: wt runs hook commands through cmd /c on Windows, breaking every documented hook

1 participant