fix(init): detect Git Bash on Windows instead of assuming PowerShell - #113
Merged
Conversation
Both `wt init` and `wt shellenv` decided the target shell from GOOS alone, so on Windows they always chose PowerShell. Under Git Bash / MSYS2 that means `wt init` writes the integration into the PowerShell $PROFILE, which the bash session never loads — leaving `wt co` with no wrapper function, so it prints "wt navigating to: ..." and the shell never changes directory. Naming the shell explicitly did not help either: the installed bash block was `eval "$(wt shellenv)"`, and shellenv re-ran the same GOOS-only detection at every startup, evaluating PowerShell code inside bash. - Detect POSIX shell environments on Windows via MSYSTEM and $SHELL, and only fall back to PowerShell when neither indicates one. $SHELL is matched on its basename so "powershell" is not read as a POSIX shell. - Write the resolved shell into the config block (`wt shellenv bash`), so the installed line no longer depends on startup-time detection. - Fall back to a non-PTY mode when script(1) is absent, as it is in Git Bash. stdout is redirected and replayed rather than piped through tee, since a pipeline would report tee's exit status and PIPESTATUS is clobbered by the next command run. - Translate native Windows paths through cygpath before cd, when available. Fixes #112
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #113 +/- ##
==========================================
+ Coverage 32.11% 32.32% +0.20%
==========================================
Files 27 27
Lines 3036 3066 +30
==========================================
+ Hits 975 991 +16
- Misses 1975 1989 +14
Partials 86 86
🚀 New features to boost your workflow:
|
timvw
added a commit
that referenced
this pull request
Aug 10, 2026
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
added a commit
that referenced
this pull request
Aug 10, 2026
…#120) 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
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.
Fixes #112.
Problem
wt coprintedwt navigating to: ...but the shell stayed put, andwt initfrom Git Bash installed a PowerShell script.Two places decided the target shell from
runtime.GOOSalone, so Windows always resolved to PowerShell:detectShell(cmd/init.go) — short-circuited topowershellbefore the$SHELLchecks, sowt initunder Git Bash wrote toDocuments\WindowsPowerShell\Microsoft.PowerShell_profile.ps1. The bash session never loads that, so thewtwrapper function was never defined and there was nothing to perform thecd.shellenvTargetShell(cmd/shellenv.go) — had the identical short-circuit. This is why passing the shell explicitly did not help: the installed bash block waseval "$(wt shellenv)", and with no argumentshellenvre-ran the same GOOS-only detection at every shell startup and emitted PowerShell into bash.Changes
Detection — a shared
isPOSIXShellEnv()gates the Windows→PowerShell default. Git Bash and MSYS2 exportMSYSTEM; Cygwin does not, but all of them set$SHELLto a Unix shell path, which neither PowerShell nor cmd does.$SHELLis matched on its basename against a known set (sh,bash,zsh,fish,dash,ash,ksh, with an optional.exe) — a substring match would have readpowershellas a POSIX shell, since it ends inshell. Explicit arguments still win, sowt init powershellfrom Git Bash works.Config block —
getShellConfigContentnow writes the resolved shell into the block:eval "$(wt shellenv bash)",Invoke-Expression (& wt shellenv powershell). The installed line no longer depends on startup-time detection. Existing blocks are rewritten in place on the nextwt init(marker handling is unchanged, and removal of old-format blocks still works).script(1)fallback — Git for Windows does not shipscript, so the bash wrapper would fail there even with detection fixed. Added acommand -v scriptguard; without it, stdout is redirected to the log file and replayed withcat. Deliberately nottee: a pipeline makes$?tee's status, andPIPESTATUS/pipestatusare clobbered by the very next command — including the test needed to choose between bash's and zsh's spelling of it. I hit exactly that while writing this (bash returned 1 on success, zsh returned 0 on failure); the redirect form has no such trap. stderr is left alone so errors still stream live. Interactive promptui menus need a TTY and won't render in this mode — documented, andwt co <branch>works fine.Path translation —
cygpath -uis applied to the navigation target when available, so nativeC:\...paths become the POSIX formcdunderstands. No-op everywhere else, sincecygpathonly exists on MSYS/Cygwin.Also refactored
detectShellto take an injectedgoos, matching the patternshellenvTargetShellalready used, so the Windows branches are testable off-Windows.Testing
TestDetectShellandTestShellenvTargetShellcover Git Bash viaMSYSTEM, via Unix$SHELL, the.exesuffix, explicit-argument override, and thepowershell/bash-named-directoryfalse-positive guards. Env vars are set unconditionally viat.Setenvso an ambientMSYSTEMon a Windows runner cannot leak in.bash -nandzsh -n.script(1)branch against a stub binary on a strippedPATHin both bash and zsh: auto-cdworks and exit codes propagate (confirming thePIPESTATUSfix).go test ./...and all 292 e2e scenarios pass. TwoTestBashTabCompletion*tests fail on this machine, but they fail identically on unmodifiedmain(macOS bash 3.2) — pre-existing and unrelated.Not verified
I have no Windows machine, so the Git Bash path is reasoned from the code and tested by proxy (stubbed
PATH, injectedgoos) rather than run for real. The three assumptions worth a second pair of eyes: that Git for Windows genuinely lacksscript(1), thatcygpathis onPATHin a Git Bash session, and that$SHELL/MSYSTEMare exported to thewt.exechild process. @celluj34 — if you're able to try this branch, that would confirm it.