Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ If this hook is ever "simplified" back to a bare `bun install`, the exact same b
**Round 4 (same PR, Bugbot re-ran again):** Flagged that `git worktree add` runs without a preceding `git worktree prune`, so if a worktree's directory was ever deleted without going through `git worktree remove` (crash, manual `rm -rf`, disk cleanup) git still has it registered internally (shows as `prunable` in `git worktree list`) and `git worktree add` at that path/branch fails outright with `fatal: '<branch>' is already used by worktree at '<path>'` — even though `[ ! -d "$dir" ]` is true (directory is gone) and my round-2 stale-dir check doesn't catch it either (that check only fires when the directory _exists_ but lacks `.git`; here the directory doesn't exist at all, only git's internal metadata does). Repro: created a worktree, `rm -rf`'d just the directory (not `git worktree remove`), re-ran the hook with the same name — confirmed exit 1 with that exact fatal error. Fix: `git worktree prune >&2` immediately before `git worktree add`, cheap and safe to run unconditionally every time. Verified the repro now succeeds, plus regression-tested round-2's stale-dir case and the plain happy path still work.

**Running tally: 4 rounds of Bugbot findings on one hook, all true positives, all found by actually reproducing before fixing.** This hook is deceptively easy to get subtly wrong — small shell script, but git worktree state has more failure modes (missing dir, stale dir, stale git registration, failed dep install) than are obvious up front. If touching this hook again, re-run all four repros above as a regression suite before pushing, not just the happy path.

**Round 5 (2026-07-01, different machine, reported directly by the user, not Bugbot):** `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 exact phrasing is cmd.exe's own stderr text (PowerShell's equivalent says "is not recognized as the name of a cmdlet, function, script file, or operable program"), and cmd.exe tokenizes on `=` — explaining why it complained about bare `name` rather than `name=$(jq`. Confirmed via `where bash` that Git Bash was installed and on `PATH`, and confirmed via the project's own `Bash` tool description that it uses Git Bash — yet the **hook** runner still fell back to cmd.exe. Root cause (confirmed by extracting and grepping the real shipped `claude.exe` binary for source strings, not guessing): the hook object had no `"shell"` field, and on this machine the platform-default resolution for hooks specifically did not land on bash the way the interactive Bash tool's own Windows detection does — the two have separate detection paths inside the CLI. The binary's source strings confirmed hooks support an explicit `"shell": "bash" | "powershell"` field, and that when `shell` resolves to anything other than `"powershell"`, Claude Code resolves a Git Bash path via, in order: (1) `CLAUDE_CODE_GIT_BASH_PATH` env var (hard-errors if set but invalid), (2) hardcoded `C:\Program Files\Git\bin\bash.exe` / `C:\Program Files (x86)\Git\bin\bash.exe`, (3) `git` resolved from `PATH` with `bash.exe` derived as `<gitdir>/../../bin/bash.exe`. If none resolve, it throws a distinct `Hook "..." requires bash but Git Bash was not found` error (different from the cmd.exe fallback the user actually saw) — so that explicit-shell path is not what silently failed; the _default resolution when `shell` is unset_ is what picked cmd.exe on this box.

**Fix:** add `"shell": "bash"` directly to the hook object in `.claude/settings.json` (sibling of `"command"`/`"timeout"`). **Verified by actually triggering the hook** — not just reasoning about the binary strings — via `EnterWorktree({name: "hook-shell-fix-test"})`, confirmed a real git worktree with `.git` file + `package.json` + correct branch via `git worktree list`, then `ExitWorktree({action: "remove", discard_changes: true})` and `git branch -D` to clean up the leftover branch (per the existing note above, `ExitWorktree`/`git worktree remove` never deletes the branch).

**Lesson:** never assume a documented "sane default" (e.g. "defaults to bash when Git Bash is installed") actually applies uniformly across every code path in the same CLI — the interactive `Bash` tool and the `hooks` runner turned out to have independently-implemented Windows-shell detection in this binary, and only one of them worked correctly on this machine. When a hook mixes POSIX-only syntax (`$(...)`, `[ -z ]`, `&&`, `;`) with Windows, set `"shell": "bash"` explicitly rather than relying on autodetection — it's one extra JSON field and removes an entire class of platform-dependent failure. See also [CLAUDE.md](../../../CLAUDE.md) "Claude Code Harness Config" for the user-facing version of this note.
1 change: 1 addition & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
{
"type": "command",
"command": "name=$(jq -r '.name // empty' | tr -d '\\r'); [ -z \"$name\" ] && name=\"wt-$$\"; dir=\"$CLAUDE_PROJECT_DIR/.claude/worktrees/$name\"; branch=\"claude/$name\"; if [ -d \"$dir\" ] && [ ! -e \"$dir/.git\" ]; then rm -rf \"$dir\"; fi; if [ ! -d \"$dir\" ]; then git worktree prune >&2; git worktree add \"$dir\" -B \"$branch\" >&2 || exit 1; fi; cd \"$dir\" || exit 1; bun install --silent >&2 || echo \"warning: bun install failed in $dir -- dependencies may be missing, run 'bun install' manually\" >&2; printf '%s\\n' \"$dir\"",
"shell": "bash",
"timeout": 60,
"statusMessage": "Installing dependencies in worktree..."
}
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Opt out of a specific hook: `git config --local hook.<name>.enabled false`. Skip

The `hooks.WorktreeCreate` entry is **not** a post-creation setup step — once it's configured, the Claude Code harness defers the _entire_ worktree creation to it (it does not also create a git worktree on its own). The hook receives a JSON payload on stdin (`{ "cwd", "name", ... }`, same pattern as the `PostToolUse` hook reading `.tool_input.file_path` via `jq`) and **must** create the worktree itself and echo _only_ the resulting absolute path as its final stdout line — any other stdout (e.g. unsilenced `bun install` or `git worktree add` output) gets misread as the path and breaks `EnterWorktree`/`ExitWorktree` with errors like `path contains control characters` or `ENOENT: ... chdir`. Redirect all setup-command output to stderr (`>&2`) and keep the trailing `printf` as the only real stdout. Do not simplify this hook back down to a bare `bun install` — that regression is exactly what caused the worktree-creation bug fixed here.

**Command-type hooks with POSIX shell syntax MUST set `"shell": "bash"` explicitly — do not rely on the platform default, even on Windows with Git Bash installed.** Without it, on at least one confirmed Windows setup, the hook runner fell back to spawning via `cmd.exe` (Node `child_process.spawn` with `shell: true` defaults to `%ComSpec%`) instead of detecting Git Bash — even though the interactive `Bash` tool on the same machine correctly used Git Bash. Symptom: `<hook> failed: <command text>: 'x' is not recognized as an internal or external command, operable program or batch file.` (that exact phrasing is cmd.exe's, not PowerShell's — PowerShell says `is not recognized as the name of a cmdlet, function, script file, or operable program`). Fix: add `"shell": "bash"` to the hook object (sibling of `"command"`). If Git Bash still isn't found (error becomes `Hook "..." requires bash but Git Bash was not found`), Claude Code checks, in order: the `CLAUDE_CODE_GIT_BASH_PATH` env var, then `C:\Program Files\Git\bin\bash.exe` / the `(x86)` variant, then a `git` on `PATH` resolved to `..\..\bin\bash.exe` — set `CLAUDE_CODE_GIT_BASH_PATH` if none of those apply. Verified end-to-end via `EnterWorktree`/`ExitWorktree` on `hooks.WorktreeCreate` on 2026-07-01.

## Architecture

### Commands
Expand Down