fix(hooks): force bash shell for WorktreeCreate hook on Windows - #442
Conversation
The WorktreeCreate hook in .claude/settings.json is a POSIX shell script ($(...), [ -z ], &&, ;) but had no explicit "shell" field. On at least one Windows setup, the hook runner's shell-detection fell back to cmd.exe instead of Git Bash -- even though the interactive Bash tool on the same machine correctly detected and used Git Bash -- so the hook failed with cmd.exe's "'name' is not recognized as an internal or external command" error. Add "shell": "bash" to force Git Bash execution regardless of the platform default, and document the gotcha (plus the CLAUDE_CODE_GIT_BASH_PATH escape hatch) in CLAUDE.md and the persistent hook-contract memory, since this hook has already needed several rounds of Windows-specific fixes. Verified end-to-end via EnterWorktree/ExitWorktree: worktree created with a real checkout and correct branch registration. Signed-off-by: Rhuan Barreto <rhuan@barreto.work>
Deploying archgate-cli with
|
| Latest commit: |
0028cd4
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://3a84bc34.archgate-cli.pages.dev |
| Branch Preview URL: | https://fix-worktree-hook-windows-sh.archgate-cli.pages.dev |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📜 Recent review details⏰ Context from checks skipped due to timeout. (3)
|
| File | Change Summary |
|---|---|
.claude/settings.json |
Added "shell": "bash" to the WorktreeCreate command hook configuration. |
CLAUDE.md |
Documented requirement that command-type hooks set "shell": "bash", including failure symptoms and bash path resolution order. |
.claude/agent-memory/archgate-developer/project_worktree_create_hook_contract.md |
Added an incident record describing the cmd.exe fallback bug, the fix, and verification via EnterWorktree/ExitWorktree. |
Sequence Diagram(s)
Not applicable — this change consists of a configuration field addition and documentation updates with no new control flow.
Estimated code review effort: 1 (Low) — a single-line configuration addition accompanied by documentation.
Related issues: None specified.
Related PRs: None specified.
Suggested labels: documentation, configuration
Suggested reviewers: None specified.
🐰 A shell once ran in cmd's disguise,
"name not recognized" — a bash surprise.
One line was set, "shell": "bash" so true,
Now worktrees create as they ought to do.
Hop, commit, and cleanup too!
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | It clearly summarizes the main change: forcing Bash for the Windows WorktreeCreate hook. |
| Description check | ✅ Passed | It directly describes the hook shell fix and the documentation updates. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
|
@cursor approve |
|
Approved. Evaluated PR #442 against
Change review: The one-line Verdict: Approve. Ready to merge once remaining non-blocking checks (Coverage Report, CodeRabbit) finish. Note: This environment’s GitHub token cannot submit a formal |
Code Coverage
Full HTML report available in workflow artifacts. Per-directory breakdown
|
There was a problem hiding this comment.
Approved. Cursor Bugbot passed with no findings, and all required CI checks (Lint/Test, Windows and Linux smoke tests, Coverage) are green. This is a focused harness config fix adding explicit "shell": "bash" to the WorktreeCreate hook plus documentation; no additional reviewers assigned.
Sent by Cursor Approval Agent: Archgate CLI Approver
# archgate ## [0.46.0](v0.45.7...v0.46.0) (2026-07-02) ### ⚠ BREAKING CHANGES * **session-context:** per-editor list/show subcommands; remove --skip and its false premise (#446) ### Features * **session-context:** per-editor list/show subcommands; remove --skip and its false premise ([#446](#446)) ([dd96230](dd96230)) ### Bug Fixes * **ci:** read POSTHOG_PROJECT_ID from secrets, not vars, in release annotation ([#443](#443)) ([a53b305](a53b305)) * detect opencode Desktop app installs, not just the CLI ([#439](#439)) ([ed92b2b](ed92b2b)) * **hooks:** force bash shell for WorktreeCreate hook on Windows ([#442](#442)) ([9da86cf](9da86cf)) * **release:** cap breaking-change bumps to minor while pre-1.0 ([#447](#447)) ([3df6c91](3df6c91)), closes [#446](#446) [#440](#440) [#440](#440) [#440](#440) * repair broken WorktreeCreate hook and add PR approval policy ([#441](#441)) ([de5e97d](de5e97d)) * **session-context:** select top-level opencode sessions, add --root flag ([#445](#445)) ([29b13f4](29b13f4)) --- This PR was generated with [simple-release](https://github.com/TrigenSoftware/simple-release). <details> <summary>📄 Cheatsheet</summary> <br> You can configure the bot's behavior through a pull request comment using the `!simple-release/set-options` command. ### Command Format ````md !simple-release/set-options ```json { "bump": {}, "publish": {} } ``` ```` ### Useful Parameters #### Bump | Parameter | Type | Description | |-----------|------|-------------| | `version` | `string` | Force set specific version | | `as` | `'major' \| 'minor' \| 'patch' \| 'prerelease'` | Release type | | `prerelease` | `string` | Pre-release identifier (e.g., "alpha", "beta") | | `firstRelease` | `boolean` | Whether this is the first release | | `skip` | `boolean` | Skip version bump | | `byProject` | `Record<string, object>` | Per-project bump options for monorepos | #### Publish | Parameter | Type | Description | |-----------|------|-------------| | `skip` | `boolean` | Skip publishing | | `access` | `'public' \| 'restricted'` | Package access level | | `tag` | `string` | Tag for npm publication | ### Usage Examples #### Force specific version ````md !simple-release/set-options ```json { "bump": { "version": "2.0.0" } } ``` ```` #### Force major bump ````md !simple-release/set-options ```json { "bump": { "as": "major" } } ``` ```` #### Create alpha pre-release ````md !simple-release/set-options ```json { "bump": { "prerelease": "alpha" } } ``` ```` #### Publish with specific access and tag ````md !simple-release/set-options ```json { "bump": { "prerelease": "beta" }, "publish": { "access": "public", "tag": "beta" } } ``` ```` ### Access Restrictions The command can only be used by users with permissions: - repository owner - organization member - collaborator ### Notes - The last comment with `!simple-release/set-options` command takes priority - JSON must be valid, otherwise the command will be ignored - Parameters apply only to the current release execution - The command can be updated by editing the comment or adding a new one </details> <!-- Please do not edit this comment. simple-release-pull-request: true simple-release-branch-from: release simple-release-branch-to: main --> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Claude Code resolves a hook's shell per surface, and does not always honour the one a hook declares: the desktop app on Windows runs these commands under cmd.exe even with "shell": "bash" set and Git Bash pinned via CLAUDE_CODE_GIT_BASH_PATH, while the terminal CLI on the same machine uses bash. Any POSIX syntax in the command then dies on its first token. Each hook now runs a single `bun run hook:*` package script and holds no shell syntax at all -- no variable expansion, pipes, command substitution or quoting. That string is valid under Bash, cmd.exe and PowerShell alike, so the surface no longer decides whether a hook works. bun run also executes package scripts from the package root, which removes the working-directory assumption as well. The logic moves to scripts/*.ts, replacing an inline one-liner and install-cloud-deps.sh. Behaviour is preserved: the stale-directory removal, worktree prune, carriage-return strip, warn-don't-exit on a failed bun install, and stdout carrying only the worktree path (#441, #442). The port also rejects a worktree name containing a path separator, which previously produced a mangled directory. Dropping jq and shell built-ins leaves bun and git as the only external commands, both of which resolve from a bare Windows PATH. Signed-off-by: Rhuan Barreto <rhuan@barreto.work>
…557) ## Problem `WorktreeCreate` fails in the Claude desktop app on Windows while the identical hook works from the terminal CLI: ``` WorktreeCreate hook failed: name=$(jq -r '.name // empty' | tr -d '\r'); ... 'name' is not recognized as an internal or external command, operable program or batch file. ``` That is `cmd.exe`'s wording. The hook already declared `"shell": "bash"` (#442). Two rounds of diagnosis, both verified on the affected machine: 1. **Git Bash was not locatable.** Git for Windows puts only `Git\cmd` on `PATH` (which holds `git.exe`, not `bash.exe`), so the only `bash` on the Windows `PATH` is `C:\WINDOWS\system32\bash.exe` — the WSL launcher, which resolves the repo as `/mnt/e/...` and would break `$CLAUDE_PROJECT_DIR` even if selected. 2. **Pinning Git Bash did not help.** With `CLAUDE_CODE_GIT_BASH_PATH` set to a valid `Git\bin\bash.exe` in user-level settings and the app fully restarted, the desktop app still ran the command under `cmd.exe`. Confirmed by a uniquely-named first token: the error named *that* token, proving current settings were being read and `"shell": "bash"` was not honoured on that surface. So the shell a hook asks for is not guaranteed. The fix is to stop needing one. ## Change Each hook command is now a single package script and contains no shell syntax — no variable expansion, pipes, command substitution, or quoting: ```json "command": "bun run hook:worktree-create" ``` That exact string is valid under Bash, `cmd.exe`, and PowerShell, so the surface no longer decides whether the hook works. `bun run` also executes package scripts from the package root, removing the working-directory assumption. `"shell"` fields are gone — there is nothing left for a shell to interpret. Logic moves to `scripts/*.ts`: | Hook | Script | |---|---| | `WorktreeCreate` | `scripts/worktree-create.ts` | | `PostToolUse` | `scripts/format-file-hook.ts` | | `SessionStart` | `scripts/session-start.ts` (replaces `install-cloud-deps.sh`) | Formatting still routes through the `format:file` package script rather than the `oxfmt` binary, per GEN-003. **Behaviour preserved** (#441, #442): stale-directory removal, `git worktree prune`, carriage-return strip on the name, warn-don't-exit on a failed `bun install`, and stdout carrying only the worktree path. The port additionally rejects a name containing a path separator — the old inline version turned one into a mangled directory (`playful-orbiting-reddy;C` exists locally as evidence). Dropping `jq` and shell built-ins leaves `bun` and `git` as the only external commands, both resolvable from a bare Windows `PATH`. `PostToolUse` also gains correctness incidentally: it never declared `"shell": "bash"` at all, so it was relying on bash being the default. ## Verification The `WorktreeCreate` hook was executed for real under each shell — not merely syntax-checked: | Shell | Result | |---|---| | `cmd.exe` (the failing surface) | exit 0, worktree + branch created, path on stdout | | PowerShell | exit 0, same | | Git Bash | exit 0, same | Edge cases: unsafe name → exit 1 with a named error; malformed stdin → falls back to `wt-<pid>`; `session-start` outside a cloud session → exit 0; `format-file-hook` formats the target file and tolerates empty stdin. Invoked from a subdirectory, the hook still resolves the project root. All probe worktrees and branches removed afterwards. `bun run validate` green — 2358 pass / 0 fail, `archgate check` 51/51, build compiles. GEN-004 caught an oversized comment block during the run; fixed. ADR review across architecture, general, ci, distribution, and legal domains: 0 violations, 0 warnings. --------- Signed-off-by: Rhuan Barreto <rhuan@barreto.work>
# archgate ## [0.52.0](v0.51.0...v0.52.0) (2026-08-06) ### ⚠ BREAKING CHANGES * add --strict and --output <format> (SARIF), remove --json/--ci/--max-warnings from check (#536) ### Features * add --strict and --output <format> (SARIF), remove --json/--ci/--max-warnings from check ([#536](#536)) ([70b1ede](70b1ede)) * **dist:** distribute archgate via winget ([#552](#552)) ([93cb3a8](93cb3a8)), references [#544](#544) * **lint:** migrate to TypeScript 7 and adopt oxlint type-aware linting ([#534](#534)) ([29daad8](29daad8)), references [#529](#529) * **plugin:** install Copilot plugin declaratively, covering the desktop app ([#543](#543)) ([ae2b988](ae2b988)) ### Bug Fixes * bound scan loop, narrow install-method types, and extend ARCH-021 to markdown code spans ([#548](#548)) ([3793d80](3793d80)), closes [#541](#541) [#540](#540) [#515](#515), references [#541](#541) [#540](#540) [#515](#515) * **cli:** exit 0 quietly when the output pipe closes (EPIPE) ([#546](#546)) ([e7bfa19](e7bfa19)) * **hooks:** invoke hooks through package scripts instead of a shell ([#557](#557)) ([3b9e411](3b9e411)), references [#442](#442) [#441](#441) [#442](#442) --- This PR was generated with [simple-release](https://github.com/TrigenSoftware/simple-release). <details> <summary>📄 Cheatsheet</summary> <br> You can configure the bot's behavior through a pull request comment using the `!simple-release/set-options` command. ### Command Format ````md !simple-release/set-options ```json { "bump": {}, "publish": {} } ``` ```` ### Useful Parameters #### Bump | Parameter | Type | Description | |-----------|------|-------------| | `version` | `string` | Force set specific version | | `as` | `'major' \| 'minor' \| 'patch' \| 'prerelease'` | Release type | | `prerelease` | `string` | Pre-release identifier (e.g., "alpha", "beta") | | `firstRelease` | `boolean` | Whether this is the first release | | `skip` | `boolean` | Skip version bump | | `byProject` | `Record<string, object>` | Per-project bump options for monorepos | #### Publish | Parameter | Type | Description | |-----------|------|-------------| | `skip` | `boolean` | Skip publishing | | `access` | `'public' \| 'restricted'` | Package access level | | `tag` | `string` | Tag for npm publication | ### Usage Examples #### Force specific version ````md !simple-release/set-options ```json { "bump": { "version": "2.0.0" } } ``` ```` #### Force major bump ````md !simple-release/set-options ```json { "bump": { "as": "major" } } ``` ```` #### Create alpha pre-release ````md !simple-release/set-options ```json { "bump": { "prerelease": "alpha" } } ``` ```` #### Publish with specific access and tag ````md !simple-release/set-options ```json { "bump": { "prerelease": "beta" }, "publish": { "access": "public", "tag": "beta" } } ``` ```` ### Custom Changelog Preamble You can add custom markdown to the top of the changelog (right after the version header) using the `!simple-release/set-preamble` command. The markdown after the command line becomes the preamble. ```md !simple-release/set-preamble ## What's new? - The website was completely redesigned - The new API gives you awesome possibilities ``` In a monorepo, pass the full package name after the command to target a single package's changelog. Wrap the name in backticks so GitHub keeps it as text instead of a mention: ```md !simple-release/set-preamble `@your-org/core` ## Core changes - New plugin system ``` Use one comment per package, plus one without a name for the whole release. ### Access Restrictions The commands can only be used by users with permissions: - repository owner - organization member - collaborator ### Notes - The last comment with `!simple-release/set-options` command takes priority - The last `!simple-release/set-preamble` comment per package takes priority - JSON must be valid, otherwise the `set-options` command will be ignored - Parameters apply only to the current release execution - The commands can be updated by editing the comment or adding a new one </details> <!-- Please do not edit this comment. simple-release-pull-request: true simple-release-branch-from: release simple-release-branch-to: main --> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>



Summary
"shell": "bash"to thehooks.WorktreeCreatecommand in.claude/settings.jsonso the hook is always executed via Git Bash, never the platform-default shell.CLAUDE.md("Claude Code Harness Config" section) and in the persistentproject_worktree_create_hook_contract.mdagent memory (Round 5 of this hook's history).Why
On at least one Windows machine, the
WorktreeCreatehook — which is pure POSIX shell ($(...),[ -z ... ],&&,;) — was silently executed viacmd.exeinstead of Git Bash, even though Git Bash is installed and the interactiveBashtool on the same machine correctly detects and uses it. The failure surfaced as:That exact phrasing is cmd.exe's own stderr text (distinct from PowerShell's wording), and cmd.exe tokenizes on
=, which is why it complained about barename. Root cause, confirmed by extracting source strings from the shippedclaude.exebinary: the hooks runner and the interactiveBashtool have independent Windows-shell detection paths in the CLI, and only one of them resolved Git Bash correctly here when no"shell"field was set on the hook.Test plan
claude.exe(confirmed a documentedshellfield on command hooks, and theCLAUDE_CODE_GIT_BASH_PATH/ hardcoded-path /PATH-derived Git Bash resolution order).EnterWorktree({name: "hook-shell-fix-test"})created a real worktree (.gitfile,package.json, correctgit worktree list/ branch registration), then cleaned up viaExitWorktree({action: "remove", discard_changes: true})+git branch -D.bun run format(config/markdown only change; no source code touched, sobun run validate/ ADR checks are not applicable to this change per the repo's own follow-up-tweak vs. initial-implementation distinction — this is a harness config + doc fix, not application code).