Skip to content

feat(plugin): PreToolUse hook to reject direct git worktree commands#54

Merged
emmax86 merged 11 commits intomainfrom
plugin-hook-reject-git-worktree
Mar 13, 2026
Merged

feat(plugin): PreToolUse hook to reject direct git worktree commands#54
emmax86 merged 11 commits intomainfrom
plugin-hook-reject-git-worktree

Conversation

@emmax86
Copy link
Copy Markdown
Owner

@emmax86 emmax86 commented Mar 13, 2026

Summary

Adds a Claude Code `PreToolUse` hook to the grove plugin that intercepts Bash tool calls containing `git worktree` and denies them, steering Claude toward grove's `/worktree` skills and `create-grove-worktree` instead.

  • `plugins/grove/hooks/reject-git-worktree.ts` — Bun hook script with a single-pass, quote-aware tokenizer that handles shell operators (`;` `&` `|` `\n` `(` `)` `{` `}`) as segment boundaries. Each segment is walked structurally: skip leading env-var assignments, require `git` as the first token, consume VALUE_FLAGS with their argument, then check for the `worktree` subcommand.
  • `plugins/grove/.claude-plugin/plugin.json` — Wires the hook into `hooks.PreToolUse` for the `Bash` matcher via `bun run ${CLAUDE_PLUGIN_ROOT}/hooks/reject-git-worktree.ts`.
  • `src/tests/integration/hooks.test.ts` — 43 table-driven integration tests covering deny cases (quoted subcommands, env-var prefixes, stacked VALUE_FLAGS, shell operators, subshell/brace grouping, command substitution) and allow cases (worktree in filenames/branch names/config keys/flag values, fail-open on missing/malformed input).

Tokenizer highlights

Scenario Handled
`git "worktree" list` ✅ quote stripping
`echo ok; git worktree list` ✅ operator segment split
`git -C -weird-dir worktree list` ✅ VALUE_FLAGS consume next token regardless of `-` prefix
`git -c user.name=test worktree list` ✅ `-c` consumes key=val
`(git worktree list)` ✅ `(` is a segment boundary
`$(git worktree list)` ✅ caught via `(` boundary
`{ git worktree list; }` ✅ `{` is a segment boundary
`git --exec-path worktree list` ✅ `--exec-path` in VALUE_FLAGS
`VAR="a b" git worktree list` ✅ env-var skip with quoted value
````git worktree list```` ⚠️ known limitation (backtick substitution)
`1>&2 git worktree list` ⚠️ known limitation (`>&` redirect prefix)

Test plan

  • `bun test src/tests/integration/hooks.test.ts` — 43/43 pass
  • `bun test` — full suite green
  • `bun run format:check && bun run typecheck` — clean

Intercepts Bash tool calls containing `git ... worktree` and denies them
with additionalContext directing Claude to use grove skills instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 13, 2026 06:45
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 13, 2026

Claude finished @emmax86's task in 2m 27s —— View job


Code Review

  • Gather context and read changed files
  • Review hook implementation (reject-git-worktree.ts)
  • Review plugin manifest (plugin.json)
  • Review integration tests (hooks.test.ts)
  • Post findings

Overall: Well-designed, carefully implemented hook. The structural tokenizer handles the hard cases cleanly, the fail-open behaviour is correct, and the test matrix is thorough. Several automated reviewer comments above are false positives — see notes below. No blocking issues found.


plugins/grove/hooks/reject-git-worktree.ts

Tokenizer (tokenizeIntoSegments): The quote-aware, character-by-character approach is the right call here. It correctly:

  • Strips quotes so git "worktree" list → denied (test line 38) ✓
  • Treats (, ), {, } as boundaries so $(git worktree list) and { git worktree list; } are caught ✓
  • Passes operators inside quoted strings through without splitting (test lines 94–102) ✓

One minor edge-case to be aware of (documented, not blocking): && and || are each split twice (one &/| at a time), producing an empty middle segment. This is harmless — isGitWorktreeSegment([]) returns false immediately because tokens[0] is undefined. But it's worth knowing it's intentional.

isGitWorktreeSegment: Clean structural walk. The VALUE_FLAGS set is complete — note that --exec-path is present on line 36, despite Copilot's repeated claim otherwise. The logic to unconditionally consume the next token as a value (regardless of whether it starts with -) is correct and the comment at lines 106–108 accurately describes this.

extractCommand: The defensive type-narrowing chain at lines 121–131 is thorough and explicit. Returning "" as a fallback correctly fails open.

Error handling: try/catch around JSON.parse + process.exit(0) correctly fails open on malformed stdin (lines 136–140) ✓

Documented limitation — no blocking issue: The 1>&2 git worktree list bypass (redirect-before-command) is correctly documented at line 11. The & in >& creates a spurious segment boundary, making "2" the first token of the second segment rather than "git", so detection is missed. For a best-effort advisory hook this tradeoff is acceptable, but adding a test that explicitly asserts this case allows (so it's a known and locked-in gap rather than an accidental regression) would be useful in future.

Equals-separated VALUE_FLAGS: git --git-dir=.git/worktree log (the whole --git-dir=value as one token) is not in the VALUE_FLAGS set, so no value consumption happens. The subcommand is then "log" → allowed. This is the correct outcome — the test at line 131 covers it. ✓


plugins/grove/.claude-plugin/plugin.json

Clean. ${CLAUDE_PLUGIN_ROOT} is the correct runtime variable. timeout: 5 is appropriate for a lightweight Bun script. No issues.


src/__tests__/integration/hooks.test.ts

Strong test matrix — 19 deny cases and 17 allow cases covering the main attack surface. A few highlights:

  • stderr: "inherit" avoids the pipe-deadlock risk that Copilot flagged; this is already correctly handled (line 13) ✓
  • Promise.all([stdout, proc.exited]) reads stdout and waits for exit concurrently — no deadlock risk ✓
  • invokeScript throws on unexpected non-0/non-2 exit codes (lines 19–21) so a crashing hook doesn't silently pass ✓
  • The quoted subcommand deny cases (lines 38–39) and the VAR="a b" env-var case (line 40) directly validate the tokenizer's quote handling

Suggestion (non-blocking): Consider adding an explicit allow case for 1>&2 git worktree list as a documented bypass — this would make the known limitation visible in the test suite and prevent a future fix from silently changing behaviour without test coverage.


Addressing Automated Reviewer Comments

Claim Status
--exec-path missing from VALUE_FLAGS False positive — present on line 36
!tokens[i].startsWith("-") prevents consuming --prefixed values False positive — that guard applies to FLAGS only; values are consumed unconditionally
jq / shell portability issues N/A — this is a TypeScript file invoked via bun run, not a shell script (the .sh version was superseded)
stderr pipe deadlock Already fixedstderr: "inherit" on line 13
invokeScript treats crash (exit 1) as allow Already fixed — lines 19–21 throw on unexpected codes
JSON.parse crash on malformed input Already fixed — try/catch + exit 0 on lines 136–140

Verdict

LGTM. No blocking issues. The implementation is solid and the known limitations are correctly scoped and documented. The one optional improvement is adding a locked-in test for the 1>&2-prefix bypass so the gap stays intentional rather than accidental.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Grove Claude Code PreToolUse hook intended to deny direct git … worktree Bash invocations and steer users toward Grove worktree skills, with integration tests validating deny/allow behavior and JSON output.

Changes:

  • Add plugins/grove/hooks/reject-git-worktree.sh to detect and deny git … worktree commands.
  • Register the hook in plugins/grove/.claude-plugin/plugin.json under hooks.PreToolUse for the Bash tool.
  • Add src/__tests__/integration/hooks.test.ts integration tests that execute the hook script via Bun.spawn.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/__tests__/integration/hooks.test.ts Adds integration tests for allow/deny cases and deny JSON structure.
plugins/grove/hooks/reject-git-worktree.sh Implements the deny logic and returns hook-protocol JSON on matches.
plugins/grove/.claude-plugin/plugin.json Wires the hook into the plugin’s PreToolUse Bash matcher.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8ffb463db2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

emmax86 and others added 4 commits March 12, 2026 23:54
- Replace greedy .* with flag-aware POSIX ERE pattern to prevent false
  positives when "worktree" appears in commit messages or file paths
- Anchor regex to ^ so git must be the invoked command (fixes echo git worktree)
- Use [[:space:]] for BSD grep portability instead of \s/\b
- Guard jq failure with || COMMAND="" so set -e does not exit the hook
- Invoke script via `bash` in plugin.json for cross-platform safety
- Add tests: git commit -m "worktree...", echo git worktree
- Remove TDD cycle comments from test file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace CLI-spawning approach with @anthropic-ai/claude-agent-sdk,
  avoiding the nested Claude Code session restriction
- Register the hook shell script as a TypeScript HookCallback so the
  real script is exercised through the agent pipeline
- Run each test against an isolated throwaway git repo (mkdtemp +
  afterEach cleanup) to prevent pollution of the grove repo itself
- Assert on hook.denied() rather than Claude's response text
- Add .grove/commands.json so `grove ws exec test:file` works
- Skip tests automatically when CLAUDECODE env var is set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace direct bash-spawning with @anthropic-ai/claude-agent-sdk so
  tests exercise the hook through the real agent tool pipeline
- Register the hook shell script as a TypeScript HookCallback; assert
  on hook.denied() rather than Claude's response text
- Isolate each test in a throwaway git repo (mkdtemp/afterEach) to
  prevent pollution of the grove repo itself
- Skip tests automatically when CLAUDECODE env var is set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e tokenizer

- Replace bash regex with a TypeScript tokenizer that splits on shell
  operators (;  &  |  newline) before checking each segment, catching
  chained commands like `echo ok; git worktree list`
- Structural token walk (env vars → git → VALUE_FLAGS → subcommand)
  replaces single ERE regex, fixing false positives like
  `git -C worktree list` (worktree as -C argument)
- Remove @anthropic-ai/claude-agent-sdk dependency; tests invoke the
  hook script directly via bun, no claude needed in CI
- Expand test suite to table-driven deny/allow cases including shell
  separators, env var prefixes, flag value consumption, and fail-open
  edge cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a Claude Code PreToolUse hook to block direct git worktree ... Bash commands in grove workspaces, nudging users toward grove’s built-in worktree skills, and includes integration tests to validate the hook behavior.

Changes:

  • Added a Bun hook script that detects git worktree subcommands and returns a deny decision with guidance text.
  • Wired the hook into the grove plugin’s plugin.json for Bash PreToolUse.
  • Added table-driven integration tests that execute the hook script via bun run.
  • Added a repo-level .grove/commands.json command configuration.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
plugins/grove/hooks/reject-git-worktree.ts Implements structural detection for git worktree and emits deny JSON for PreToolUse.
plugins/grove/.claude-plugin/plugin.json Registers the new PreToolUse Bash hook command with a timeout.
src/__tests__/integration/hooks.test.ts Adds integration tests that run the hook script and assert allow/deny behavior and output shape.
.grove/commands.json Provides standard repo command mappings (setup/format/test/check).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

emmax86 and others added 2 commits March 13, 2026 01:41
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap JSON.parse in try/catch to fail open on malformed stdin
- Extract command via typed guard (extractCommand) instead of unsafe
  optional chaining with string cast
- Test helper: stderr: "inherit" to surface hook errors without pipe
  deadlock; throw on unexpected exit codes so crashes fail tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Claude Code PreToolUse hook for the grove plugin to block direct git worktree usage in Bash tool calls, and introduces integration tests that invoke the hook script via the hook protocol.

Changes:

  • Added plugins/grove/hooks/reject-git-worktree.ts to detect and deny git worktree invocations.
  • Wired the hook into plugins/grove/.claude-plugin/plugin.json under hooks.PreToolUse for Bash.
  • Added table-driven integration tests in src/__tests__/integration/hooks.test.ts that execute the hook via bun run.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
plugins/grove/hooks/reject-git-worktree.ts Implements structural detection of git worktree and emits deny JSON on match.
plugins/grove/.claude-plugin/plugin.json Registers the new hook for Bash PreToolUse events.
src/__tests__/integration/hooks.test.ts Adds integration coverage for deny/allow cases and output JSON shape.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

- Replace split(/\s+/) with a minimal shell tokenizer that strips
  single/double quotes and handles backslash escapes, so
  git "worktree" list and git 'worktree' list are correctly denied
- Add deny cases for quoted subcommand and quoted env var assignment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Claude Code (grove) PreToolUse hook to block direct git worktree usage in Bash tool calls, steering users toward grove’s /worktree commands/skills, and introduces integration tests to validate the hook behavior.

Changes:

  • Added reject-git-worktree.ts PreToolUse hook script that detects git worktree across simple shell-chained commands and denies execution with guidance.
  • Wired the hook into the grove plugin manifest (plugins/grove/.claude-plugin/plugin.json) for the Bash matcher.
  • Added table-driven integration tests that invoke the hook via bun run and assert allow/deny behavior and deny JSON shape.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/__tests__/integration/hooks.test.ts Adds integration tests exercising the hook script through Bun spawn and checking deny payload structure.
plugins/grove/hooks/reject-git-worktree.ts Implements command splitting/tokenization and deny output for git worktree detection.
plugins/grove/.claude-plugin/plugin.json Registers the new PreToolUse hook for Bash tool calls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Add 8 new DENY cases (multiple VALUE_FLAGS stacked, multiple env vars,
pipe/&&/newline segments) and 6 new ALLOW cases (worktree in filenames,
grep patterns, config keys, and -c values). 39 tests total, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Claude Code PreToolUse hook to the grove plugin to deny direct git worktree usage (nudging users toward grove skills), wires it into the plugin manifest, and adds integration tests that exercise the hook script via the hook protocol.

Changes:

  • Added a Bun-based PreToolUse hook script that detects and denies git worktree invocations.
  • Registered the hook in plugins/grove/.claude-plugin/plugin.json for the Bash tool.
  • Added table-driven integration tests to validate deny/allow behavior and deny JSON output structure.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
plugins/grove/hooks/reject-git-worktree.ts Implements command tokenization + structural detection and emits a deny response for git worktree.
plugins/grove/.claude-plugin/plugin.json Registers the new PreToolUse hook for Bash tool invocations.
src/__tests__/integration/hooks.test.ts Adds integration coverage by invoking the hook script as a subprocess across allow/deny cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

- Treat (, ), {, } as segment boundaries so (git worktree list),
  { git worktree list; }, and $(git worktree list) are all detected
- Add --exec-path and --super-prefix to VALUE_FLAGS so tokens like
  `git --exec-path worktree list` correctly consume worktree as the
  flag value rather than misidentifying it as the subcommand
- Update known-limitation comment: $(…) is now caught; backtick
  substitution and >&-prefixed redirections remain out of scope

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@emmax86 emmax86 changed the title feat(plugin): add PreToolUse hook to reject direct git worktree commands feat(plugin): PreToolUse hook to reject direct git worktree commands Mar 13, 2026
@emmax86 emmax86 merged commit 0b689f5 into main Mar 13, 2026
4 checks passed
@emmax86 emmax86 deleted the plugin-hook-reject-git-worktree branch March 13, 2026 09:44
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.

2 participants