feat(rules): add warn-bash-mutation-without-leading-cd (v1.11.0)#19
Conversation
New PreToolUse warn rule on Bash that fires when a state-mutating command (git commit/push/mv/rm/reset/merge/revert, sed -i, gh pr create/merge/edit) is invoked without a leading `cd /<path>` prefix. ## Why this rule exists Real bug pattern observed 2026-05-10 (DOJ-4007 PR-2/PR-3 cross-ref fixup): When working across multiple parallel git worktrees in the same session, an agent ran a sed + commit + push chain WITHOUT explicit `cd` to the target worktree. The previous bash invocation left cwd in a different worktree (PR-2's), so the new commit + push (intended for PR-3) silently landed on PR-2 branch instead. Required revert on PR-2 + correct re-apply on PR-3 + confused commit history. This warn rule surfaces that pattern at the Bash boundary so the agent gets a heads-up before committing to the wrong branch. ## Scope - Triggers on: `git commit/push/mv/rm/reset/merge/revert`, `sed -i`, `gh pr create/merge/edit` - Skips on: cd/pushd at command start (legitimate worktree-aware bash) - Skips on: read-only ops (`git status`, `git log`, `git diff`, `gh pr list`, `gh pr view`) - Bypass marker: `cd-worktree-rule` (use when working with absolute paths or after explicit `pwd` verification) ## Tests 15 tests added, all PASS: - 7 warns (commit, push, mv, sed -i, gh pr create, gh pr merge, reset --hard) - 7 passes (cd prefix, pushd prefix, status, log, diff, gh pr list, gh pr view) - 1 bypass (with cd-worktree-rule marker) Total test suite now 192/192 (was 177). ## Memory backing `~/.claude/.../memory/feedback_cd_between_worktrees.md` (saved 2026-05-10) captures the bug history + how-to-apply guidance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR adds a new
Path to 5/5 Confidence1. Enforce absolute paths in 2. Add 3. Broaden the 4. Fix description field — Confidence Score: 3/5The rule is advisory-only (exit 0, warn action) so no existing workflows are blocked, but the not_pattern gap means the rule can silently skip warnings in exactly the relative-cd scenario where a developer might feel falsely assured they've satisfied the requirement. The not_pattern accepts relative cd paths as valid bypasses, yet the motivating bug can still occur when the agent's cwd is already wrong and a relative cd resolves to the wrong worktree. Combined with uncovered sed variants and a missing git rm test, the rule has real gaps in its detection logic that reduce trust in its correctness. hooks/rules/rules.yaml and hooks/rules/rules.json both carry the relative-cd bypass gap in the not_pattern and the incomplete description field — both files need the same fixes.
|
| Filename | Overview |
|---|---|
| hooks/rules/rules.yaml | Adds the new warn-bash-mutation-without-leading-cd rule; the not_pattern accepts relative cd paths, undermining the absolute-path requirement the rule documents, and the sed pattern misses -i.bak and --in-place variants. |
| hooks/rules/rules.json | Generated counterpart to rules.yaml; carries the same relative-cd bypass gap and incomplete description field (missing 'edit' in the gh pr list). |
| .claude-plugin/marketplace.json | Version bump 1.10.0 → 1.11.0 in both top-level and plugin-entry version fields; no logic changes. |
| .claude-plugin/plugin.json | Version bump 1.10.0 → 1.11.0; no logic changes. |
| package.json | Version bump 1.10.0 → 1.11.0; no logic changes. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Bash PreToolUse Hook] --> B{bypass marker present?}
B -->|Yes| Z[Pass — exit 0]
B -->|No| C{Positive match: git commit/push/mv/rm/reset/revert/merge OR sed -i OR gh pr create/merge/edit}
C -->|No match| Z
C -->|Match| D{Negative match: starts with cd or pushd}
D -->|Starts with cd or pushd| Z
D -->|No leading cd or pushd| E[WARN to stderr — exit 0 command still runs]
style E fill:#f90,color:#000
style Z fill:#6c6,color:#fff
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 4
hooks/rules/rules.yaml:1930-1931
**Relative `cd` bypasses the rule's core safety guarantee**
The `not_pattern` `^[[:space:]]*(cd|pushd)[[:space:]]+` accepts any `cd`, including relative paths like `cd worktree-dir`. A relative-path `cd` doesn't fix the worktree-confusion bug the rule exists to prevent — if the agent's cwd is already wrong, `cd relative-dir` can still resolve to the wrong worktree. The warning message and PR description both say the required form is `cd /full/path/to/worktree` (absolute path), but the regex doesn't enforce the leading `/`. Changing the `not_pattern` to `^[[:space:]]*(cd|pushd)[[:space:]]+/` would enforce the absolute-path requirement that the rule documents. The same fix is needed in the parallel `rules.json` entry.
### Issue 2 of 4
hooks/rules/rules.yaml:1924
The `description` field lists "gh pr create/merge" but the actual pattern and tests also cover `gh pr edit`. This makes the rule's one-line description out of sync with its real scope.
```suggestion
description: Warn when bash mutation commands (git commit/push/mv/rm/reset/merge/revert, sed -i, gh pr create/merge/edit) don't start with explicit `cd /<path>` — prevents commits landing on the wrong branch when working across multiple parallel worktrees
```
### Issue 3 of 4
hooks/rules/rules.yaml:1928-1929
**`sed -i.bak` and `sed --in-place` are not caught**
The positive pattern `sed[[:space:]]+-i[[:space:]]` requires a space immediately after `-i`. This misses `sed -i.bak s/foo/bar/ file` (backup-extension variant — no space after `-i`) and `sed --in-place` (long-form flag). Both perform in-place mutation and could land in the wrong worktree. Broadening the pattern to cover `-i` followed by a non-flag character, or adding a `--in-place` alternation, would close these gaps.
### Issue 4 of 4
hooks/rules/rules.yaml:1975-2040
**No warn test for `git rm`**
The pattern includes `rm[[:space:]]` as a mutation trigger and `warns-on-git-mv-no-cd` covers `git mv`, but there is no corresponding `warns-on-git-rm-no-cd` test. Every other mutating subcommand in the PR description table has at least one warn-side test. Adding a case like `git rm path/to/file` ensures the `rm[[:space:]]` alternation branch is exercised and won't silently regress.
Reviews (1): Last reviewed commit: "feat(rules): add warn-bash-mutation-with..." | Re-trigger Greptile
| # Negative: command does NOT begin with `cd ` or `pushd ` | ||
| - field: command |
There was a problem hiding this comment.
Relative
cd bypasses the rule's core safety guarantee
The not_pattern ^[[:space:]]*(cd|pushd)[[:space:]]+ accepts any cd, including relative paths like cd worktree-dir. A relative-path cd doesn't fix the worktree-confusion bug the rule exists to prevent — if the agent's cwd is already wrong, cd relative-dir can still resolve to the wrong worktree. The warning message and PR description both say the required form is cd /full/path/to/worktree (absolute path), but the regex doesn't enforce the leading /. Changing the not_pattern to ^[[:space:]]*(cd|pushd)[[:space:]]+/ would enforce the absolute-path requirement that the rule documents. The same fix is needed in the parallel rules.json entry.
Prompt To Fix With AI
This is a comment left during a code review.
Path: hooks/rules/rules.yaml
Line: 1930-1931
Comment:
**Relative `cd` bypasses the rule's core safety guarantee**
The `not_pattern` `^[[:space:]]*(cd|pushd)[[:space:]]+` accepts any `cd`, including relative paths like `cd worktree-dir`. A relative-path `cd` doesn't fix the worktree-confusion bug the rule exists to prevent — if the agent's cwd is already wrong, `cd relative-dir` can still resolve to the wrong worktree. The warning message and PR description both say the required form is `cd /full/path/to/worktree` (absolute path), but the regex doesn't enforce the leading `/`. Changing the `not_pattern` to `^[[:space:]]*(cd|pushd)[[:space:]]+/` would enforce the absolute-path requirement that the rule documents. The same fix is needed in the parallel `rules.json` entry.
How can I resolve this? If you propose a fix, please make it concise.| expected_exit: 0 | ||
|
|
||
| - id: warn-bash-mutation-without-leading-cd | ||
| description: Warn when bash mutation commands (git commit/push/mv/rm/reset/merge/revert, sed -i, gh pr create/merge) don't start with explicit `cd /<path>` — prevents commits landing on the wrong branch when working across multiple parallel worktrees |
There was a problem hiding this comment.
The
description field lists "gh pr create/merge" but the actual pattern and tests also cover gh pr edit. This makes the rule's one-line description out of sync with its real scope.
| description: Warn when bash mutation commands (git commit/push/mv/rm/reset/merge/revert, sed -i, gh pr create/merge) don't start with explicit `cd /<path>` — prevents commits landing on the wrong branch when working across multiple parallel worktrees | |
| description: Warn when bash mutation commands (git commit/push/mv/rm/reset/merge/revert, sed -i, gh pr create/merge/edit) don't start with explicit `cd /<path>` — prevents commits landing on the wrong branch when working across multiple parallel worktrees |
Prompt To Fix With AI
This is a comment left during a code review.
Path: hooks/rules/rules.yaml
Line: 1924
Comment:
The `description` field lists "gh pr create/merge" but the actual pattern and tests also cover `gh pr edit`. This makes the rule's one-line description out of sync with its real scope.
```suggestion
description: Warn when bash mutation commands (git commit/push/mv/rm/reset/merge/revert, sed -i, gh pr create/merge/edit) don't start with explicit `cd /<path>` — prevents commits landing on the wrong branch when working across multiple parallel worktrees
```
How can I resolve this? If you propose a fix, please make it concise.| - field: command | ||
| pattern: 'git[[:space:]]+(commit|push|mv|rm[[:space:]]|reset|revert|merge)|sed[[:space:]]+-i[[:space:]]|gh[[:space:]]+pr[[:space:]]+(create|merge|edit)' |
There was a problem hiding this comment.
sed -i.bak and sed --in-place are not caught
The positive pattern sed[[:space:]]+-i[[:space:]] requires a space immediately after -i. This misses sed -i.bak s/foo/bar/ file (backup-extension variant — no space after -i) and sed --in-place (long-form flag). Both perform in-place mutation and could land in the wrong worktree. Broadening the pattern to cover -i followed by a non-flag character, or adding a --in-place alternation, would close these gaps.
Prompt To Fix With AI
This is a comment left during a code review.
Path: hooks/rules/rules.yaml
Line: 1928-1929
Comment:
**`sed -i.bak` and `sed --in-place` are not caught**
The positive pattern `sed[[:space:]]+-i[[:space:]]` requires a space immediately after `-i`. This misses `sed -i.bak s/foo/bar/ file` (backup-extension variant — no space after `-i`) and `sed --in-place` (long-form flag). Both perform in-place mutation and could land in the wrong worktree. Broadening the pattern to cover `-i` followed by a non-flag character, or adding a `--in-place` alternation, would close these gaps.
How can I resolve this? If you propose a fix, please make it concise.| command: 'git mv content/old content/new' | ||
| expected_exit: 0 | ||
| expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' | ||
| - name: warns-on-sed-i-no-cd | ||
| input: | ||
| tool_input: | ||
| command: 'sed -i s/foo/bar/g content/file.md' | ||
| expected_exit: 0 | ||
| expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' | ||
| - name: warns-on-gh-pr-create-no-cd | ||
| input: | ||
| tool_input: | ||
| command: 'gh pr create --base main --title foo' | ||
| expected_exit: 0 | ||
| expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' | ||
| - name: warns-on-gh-pr-merge-no-cd | ||
| input: | ||
| tool_input: | ||
| command: 'gh pr merge 27 --squash --delete-branch' | ||
| expected_exit: 0 | ||
| expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' | ||
| - name: warns-on-git-reset-hard-no-cd | ||
| input: | ||
| tool_input: | ||
| command: 'git reset --hard origin/main' | ||
| expected_exit: 0 | ||
| expected_stderr_contains: 'warn-bash-mutation-without-leading-cd' | ||
| - name: passes-with-cd-prefix | ||
| input: | ||
| tool_input: | ||
| command: 'cd /home/user/repo && git commit -m fix && git push' | ||
| expected_exit: 0 | ||
| - name: passes-with-pushd-prefix | ||
| input: | ||
| tool_input: | ||
| command: 'pushd /home/user/repo && git commit -m fix' | ||
| expected_exit: 0 | ||
| - name: passes-on-git-status | ||
| input: | ||
| tool_input: | ||
| command: 'git status' | ||
| expected_exit: 0 | ||
| - name: passes-on-git-log | ||
| input: | ||
| tool_input: | ||
| command: 'git log --oneline -3' | ||
| expected_exit: 0 | ||
| - name: passes-on-git-diff | ||
| input: | ||
| tool_input: | ||
| command: 'git diff --stat' | ||
| expected_exit: 0 | ||
| - name: passes-on-gh-pr-list | ||
| input: | ||
| tool_input: | ||
| command: 'gh pr list --author=@me' | ||
| expected_exit: 0 | ||
| - name: passes-on-gh-pr-view | ||
| input: | ||
| tool_input: | ||
| command: 'gh pr view 27 --json mergeable' | ||
| expected_exit: 0 | ||
| - name: passes-with-bypass-marker | ||
| input: | ||
| tool_input: | ||
| command: 'git push origin main # hook-bypass: cd-worktree-rule' |
There was a problem hiding this comment.
The pattern includes rm[[:space:]] as a mutation trigger and warns-on-git-mv-no-cd covers git mv, but there is no corresponding warns-on-git-rm-no-cd test. Every other mutating subcommand in the PR description table has at least one warn-side test. Adding a case like git rm path/to/file ensures the rm[[:space:]] alternation branch is exercised and won't silently regress.
Prompt To Fix With AI
This is a comment left during a code review.
Path: hooks/rules/rules.yaml
Line: 1975-2040
Comment:
**No warn test for `git rm`**
The pattern includes `rm[[:space:]]` as a mutation trigger and `warns-on-git-mv-no-cd` covers `git mv`, but there is no corresponding `warns-on-git-rm-no-cd` test. Every other mutating subcommand in the PR description table has at least one warn-side test. Adding a case like `git rm path/to/file` ensures the `rm[[:space:]]` alternation branch is exercised and won't silently regress.
How can I resolve this? If you propose a fix, please make it concise.…without-leading-cd Greptile review on PR #19 (3/5 confidence) flagged 4 gaps: 1. **Relative cd bypassed the rule** — `not_pattern` accepted any `cd <anything>`, including `cd worktree-dir` (relative). Per the rule's docstring, an absolute path is required because if cwd is already in the wrong worktree, a relative `cd` resolves to the wrong target. Tightened pattern to require `cd /...`, `cd ~/...`, or `cd $VAR/...` (the 3 forms of absolute-or-known-absolute paths). 2. **Missing `git rm` test** — added `warns-on-git-rm-no-cd`. 3. **Sed pattern missed `-i.bak` and `--in-place`** — extended pattern to: `sed -i `, `sed -i.bak`, `sed -ibackup`, `sed --in-place `. Added 2 tests. 4. **Description omitted `gh pr edit`** — corrected to `create/merge/edit` and widened to mention long-form sed variants. ## Test additions (7 new tests, 199 total now) WARNS: - `warns-on-git-rm-no-cd` - `warns-on-sed-i-bak-no-cd` (`sed -i.bak ...`) - `warns-on-sed-in-place-long-form-no-cd` (`sed --in-place ...`) - `warns-on-git-commit-with-relative-cd` (`cd worktree-dir && git commit`) - `warns-on-git-push-with-dotdot-cd` (`cd ../sibling && git push`) PASSES: - `passes-with-tilde-cd-prefix` (`cd ~/repo && git commit`) - `passes-with-env-var-cd-prefix` (`cd $WORKTREE && git push`) All 199/199 tests pass after the changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
New PreToolUse warn rule that fires when state-mutating Bash commands (git commit/push/mv/rm/reset/merge/revert, sed -i, gh pr create/merge/edit) are invoked without a leading
cd /<path>prefix.Bumps version 1.10.0 → 1.11.0 (minor — additive new rule, no breaking changes).
Why this rule exists
Real bug pattern observed 2026-05-10 in DOJ-4007 PR-2/PR-3 cross-ref fixup work:
When working across multiple parallel git worktrees in the same session, an agent ran a
sed + commit + pushchain WITHOUT explicitcdto the target worktree. The previous bash invocation had left cwd in a different worktree, so the new commit + push silently landed on the wrong branch. Required revert on the polluted branch + correct re-apply on the intended branch + confused commit history visible to reviewers.This warn rule surfaces that pattern at the Bash boundary so the agent gets a heads-up BEFORE committing to the wrong branch.
How it works
git commit -m "..."cd /repo && git commit ...(cd prefix)git push origin mainpushd /repo && git push ...(pushd prefix)git mv src dstgit status(read-only)git rm <file>git log --oneline(read-only)git reset --hard ...git diff(read-only)git revert <sha>gh pr list(read-only)git merge <branch>gh pr view 27(read-only)sed -i s/foo/bar/g <file># hook-bypass: cd-worktree-rulegh pr create --title ...gh pr merge 27 --squashgh pr edit 27 --body ...Action is
warn(notblock) — exit code 0, message to stderr. Agent receives the reminder but the command still executes.Tests
15 tests added, all PASS. Total test suite now 192/192 (was 177).
Memory backing
The user's auto-memory captures the bug history + apply-guidance:
Rule cites this memory via
memory_reffield (per existing convention).Test plan
npm run build-rulesregeneratesrules.jsoncleanly (31 rules total, was 30)bash hooks/test-hooks.shreports 192/192 PASSRelated
feedback_cd_between_worktrees.md(added to MEMORY.md index 2026-05-10)Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com