fix: repair broken WorktreeCreate hook and add PR approval policy - #441
Conversation
The .claude/settings.json WorktreeCreate hook only ran `bun install`, which is not enough to satisfy the harness's hook contract: once a WorktreeCreate hook is configured, the harness defers the entire worktree creation to it and expects only the resulting absolute path on stdout. The old hook never created a worktree or emitted a path, so EnterWorktree failed with errors like "path contains control characters" or an ENOENT chdir into stray bun install output. 21 stale empty directories under .claude/worktrees/ plus orphaned claude/* branches were leftover evidence of this. Fixed the hook to parse the requested name from stdin JSON, run `git worktree add`, install deps silently, and echo only the resulting path. Verified end-to-end via EnterWorktree/ExitWorktree. Documented the hook contract in CLAUDE.md so it doesn't regress. Also adds APPROVAL_POLICY.md, read by the repo's "Archgate CLI Approver" Cursor Automation. Per Cursor's Bugbot docs, a `success` check conclusion already means "no issues found, no unresolved comments" -- Bugbot does not always leave a comment. The automation's prior fallback logic treated a clean pass with no comment as an unverified signal and withheld approval (see PR #439). The policy clarifies that the check's own conclusion is sufficient on its own. Session findings captured in agent memory for future reference. Signed-off-by: Rhuan Barreto <rhuan@barreto.work>
|
Warning Review limit reached
Next review available in: 3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
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 |
Deploying archgate-cli with
|
| Latest commit: |
da69664
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b29a3177.archgate-cli.pages.dev |
| Branch Preview URL: | https://fix-worktree-hook-and-approv.archgate-cli.pages.dev |
There was a problem hiding this comment.
Stale comment
Not approving: Cursor Bugbot completed with a skipping status and reported 2 unresolved medium-severity issues in the WorktreeCreate hook (.claude/settings.json). Human review is needed to address the stale-directory and Windows CRLF findings before merge; no reviewers were assigned.
Sent by Cursor Approval Agent: Archgate CLI Approver
Code Coverage
Full HTML report available in workflow artifacts. Per-directory breakdown
|
Two issues flagged by Cursor Bugbot on this PR, both reproduced before fixing: - The hook only ran `git worktree add` when the target path was missing, so a pre-existing stale/empty directory under .claude/worktrees/ (exactly the kind of leftover this PR documents finding 21 of) would be skipped, cd'd into, and still reported as success with no real checkout. Now removes non-worktree leftovers (no .git) before the create-if-missing check. - `jq -r` can emit CRLF on Windows Git Bash; command substitution only strips the trailing \n, leaving a stray \r in the parsed name and therefore the final stdout path -- reproducing the exact "path contains control characters" failure this PR set out to fix. Now strips \r from the parsed name. Signed-off-by: Rhuan Barreto <rhuan@barreto.work>
There was a problem hiding this comment.
Stale comment
Not approving: Cursor Bugbot completed with a skipping status and reported one unresolved medium-severity issue (silent
bun installfailure in the WorktreeCreate hook). Human review is needed before merge; recommend @rhuanbarreto address or resolve the Bugbot finding.Sent by Cursor Approval Agent: Archgate CLI Approver
…wallowing them Cursor Bugbot flagged that a failed `bun install --silent` in the WorktreeCreate hook went unchecked, so the hook always exited 0 and the harness treated setup as fully successful even with node_modules missing. Reproduced by corrupting package.json in a fresh worktree and confirming exit 0 despite bun's own parse error. Deliberately did not hard-fail here the way `git worktree add` does: by this point the worktree already exists and may be a *reused* existing one with real uncommitted work, not just a fresh checkout. Hard-failing without cleanup would orphan directories again (the bug fixed two commits ago); adding cleanup risks deleting real work on a transient install failure. Instead the hook now prints an explicit warning to stderr on failure, so the condition is visible rather than silently swallowed, while still completing successfully since a worktree without deps is still usable. Signed-off-by: Rhuan Barreto <rhuan@barreto.work>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c14e9d6. Configure here.
There was a problem hiding this comment.
Stale comment
Not approving: Cursor Bugbot completed with a skipping status and reported one unresolved medium-severity finding (missing
git worktree prunebefore add). This PR also introduces a newAPPROVAL_POLICY.mdwith no base-branch version, which requires human review. Recommend @rhuanbarreto address or resolve the Bugbot finding before merge.Sent by Cursor Approval Agent: Archgate CLI Approver
Cursor Bugbot flagged that WorktreeCreate never ran `git worktree prune` before `git worktree add`. If a worktree's directory was ever removed without going through `git worktree remove` (crash, manual rm -rf), git still has it registered internally and `add` fails outright with "fatal: '<branch>' is already used by worktree at '<path>'" -- even though the directory is gone, blocking EnterWorktree until manual cleanup. The round-2 stale-dir check doesn't catch this case either, since it only fires when the directory exists but lacks .git; here the directory doesn't exist at all, only git's internal registration does. Reproduced by creating a worktree, rm -rf'ing just the directory, and re-running the hook with the same name -- confirmed the exact fatal error. Fixed with `git worktree prune` immediately before `add`; cheap and safe to run unconditionally. Regression-tested against the earlier stale-dir fix and the plain happy path. Signed-off-by: Rhuan Barreto <rhuan@barreto.work>
There was a problem hiding this comment.
Not approving: Cursor Bugbot passed with all prior findings resolved, but this PR introduces a new APPROVAL_POLICY.md with no base-branch version, which requires human review before merge. Recommend @rhuanbarreto confirm the policy wording; no reviewers were 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
hooks.WorktreeCreatecommand in.claude/settings.json, which only ranbun install. Once aWorktreeCreatehook is configured, the Claude Code harness defers the entire worktree creation to it and expects only the resulting absolute path on stdout — the old hook did neither, soEnterWorktreefailed with errors likepath contains control charactersor anENOENTchdir into straybun installoutput. (21 stale empty directories under.claude/worktrees/plus orphanedclaude/*branches were leftover evidence of this having been broken for a while.)git worktree add, installs deps silently, and echoes only the resulting path. Verified end-to-end viaEnterWorktree/ExitWorktree— worktree created with full content +node_modules, session cwd matched, clean removal.CLAUDE.mdso it doesn't regress back to a barebun install.APPROVAL_POLICY.md, read by this repo's "Archgate CLI Approver" Cursor Automation (confirmed via its own PR comments — it's custom logic in that automation's prompt, not a documented Cursor platform feature). Per Cursor's Bugbot docs, asuccesscheck conclusion already means "no issues found, and no unresolved comments" — Bugbot does not always leave a review comment. The automation's prior fallback logic treated a clean pass with no comment as an unverified signal and withheld approval (see PR #439). The policy clarifies that the check's own conclusion is sufficient on its own, while still requiring human review when the check is missing/pending/failed or Bugbot left unresolved comments..claude/agent-memory/archgate-developer/) for future reference.Test plan
bun run src/cli.ts check— 39/39 ADR rules passbun run validate— full pipeline (lint, typecheck, format, 1310 tests, ADR check, knip, build check) passesoxfmt --check .— clean@reviewerskill — APPROVED, 0 violations/warnings across General/Process, Distribution, and General domainsWorktreeCreatehook viaEnterWorktree/ExitWorktreeAPPROVAL_POLICY.md(unverified — the automation's policy-file lookup is self-reported, not independently confirmed)