Skip to content

feat(rules): add warn-bash-mutation-without-leading-cd (v1.11.0)#19

Merged
lapc506 merged 2 commits into
mainfrom
feat/cd-between-worktrees-hook
May 10, 2026
Merged

feat(rules): add warn-bash-mutation-without-leading-cd (v1.11.0)#19
lapc506 merged 2 commits into
mainfrom
feat/cd-between-worktrees-hook

Conversation

@lapc506
Copy link
Copy Markdown
Collaborator

@lapc506 lapc506 commented May 10, 2026

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 + push chain WITHOUT explicit cd to 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

Triggers warning Doesn't trigger (skipped)
git commit -m "..." cd /repo && git commit ... (cd prefix)
git push origin main pushd /repo && git push ... (pushd prefix)
git mv src dst git 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> Bypass: # hook-bypass: cd-worktree-rule
gh pr create --title ...
gh pr merge 27 --squash
gh pr edit 27 --body ...

Action is warn (not block) — 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).

PASS  warn-bash-mutation-without-leading-cd / warns-on-git-commit-no-cd
PASS  warn-bash-mutation-without-leading-cd / warns-on-git-push-no-cd
PASS  warn-bash-mutation-without-leading-cd / warns-on-git-mv-no-cd
PASS  warn-bash-mutation-without-leading-cd / warns-on-sed-i-no-cd
PASS  warn-bash-mutation-without-leading-cd / warns-on-gh-pr-create-no-cd
PASS  warn-bash-mutation-without-leading-cd / warns-on-gh-pr-merge-no-cd
PASS  warn-bash-mutation-without-leading-cd / warns-on-git-reset-hard-no-cd
PASS  warn-bash-mutation-without-leading-cd / passes-with-cd-prefix
PASS  warn-bash-mutation-without-leading-cd / passes-with-pushd-prefix
PASS  warn-bash-mutation-without-leading-cd / passes-on-git-status
PASS  warn-bash-mutation-without-leading-cd / passes-on-git-log
PASS  warn-bash-mutation-without-leading-cd / passes-on-git-diff
PASS  warn-bash-mutation-without-leading-cd / passes-on-gh-pr-list
PASS  warn-bash-mutation-without-leading-cd / passes-on-gh-pr-view
PASS  warn-bash-mutation-without-leading-cd / passes-with-bypass-marker

Memory backing

The user's auto-memory captures the bug history + apply-guidance:

~/.claude/.../memory/feedback_cd_between_worktrees.md

Rule cites this memory via memory_ref field (per existing convention).

Test plan

  • npm run build-rules regenerates rules.json cleanly (31 rules total, was 30)
  • bash hooks/test-hooks.sh reports 192/192 PASS
  • CI on this PR confirms the same

Related

  • Memory: feedback_cd_between_worktrees.md (added to MEMORY.md index 2026-05-10)
  • Bug history: DOJ-4007 PR-2 / PR-3 in dojo-academy repo (cross-ref fixup work)

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

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-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 10, 2026

Greptile Summary

This PR adds a new warn PreToolUse hook rule (warn-bash-mutation-without-leading-cd) that emits a stderr advisory — without blocking — when a state-mutating Bash command (git commit/push/mv/rm/reset/merge/revert, sed -i, or gh pr create/merge/edit) is invoked without a leading cd/pushd prefix. Version is bumped 1.10.0 → 1.11.0 across package.json, plugin.json, and marketplace.json.

  • The rule's not_pattern (^[[:space:]]*(cd|pushd)[[:space:]]+) allows any cd, including relative paths, to suppress the warning — while the rule's own message and documentation require cd /full/path/to/worktree (absolute path). A relative cd can still commit to the wrong worktree if the agent's cwd is already wrong.
  • The sed detection pattern misses sed -i.bak and sed --in-place; the description field also omits edit from the gh pr list despite the regex and tests covering it.

Path to 5/5 Confidence

1. Enforce absolute paths in not_patternhooks/rules/rules.yaml line 1931 and matching field in hooks/rules/rules.json: change '^[[:space:]]*(cd|pushd)[[:space:]]+''^[[:space:]]*(cd|pushd)[[:space:]]+/'. Add a warn test warns-on-git-commit-with-relative-cd with command 'cd worktree-dir && git commit -m \"fix\"' expecting the warning in both files.

2. Add warns-on-git-rm-no-cd test in both hooks/rules/rules.yaml and hooks/rules/rules.json with command 'git rm path/to/file.md' and expected_stderr_contains: 'warn-bash-mutation-without-leading-cd'.

3. Broaden the sed patternhooks/rules/rules.yaml line 1929 and rules.json: replace sed[[:space:]]+-i[[:space:]] with sed[[:space:]]+-i([[:space:]]|[^[:space:]-])|sed[[:space:]]+--in-place[[:space:]]. Add warn tests for sed -i.bak and sed --in-place forms.

4. Fix description fieldhooks/rules/rules.yaml line 1924 and rules.json: change \"gh pr create/merge\"\"gh pr create/merge/edit\" to match the actual regex and tests.

Confidence Score: 3/5

The 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.

Important Files Changed

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
Loading
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

Comment thread hooks/rules/rules.yaml Outdated
Comment on lines +1930 to +1931
# Negative: command does NOT begin with `cd ` or `pushd `
- field: command
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

Comment thread hooks/rules/rules.yaml Outdated
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Suggested change
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.

Comment thread hooks/rules/rules.yaml Outdated
Comment on lines +1928 to +1929
- field: command
pattern: 'git[[:space:]]+(commit|push|mv|rm[[:space:]]|reset|revert|merge)|sed[[:space:]]+-i[[:space:]]|gh[[:space:]]+pr[[:space:]]+(create|merge|edit)'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Comment thread hooks/rules/rules.yaml
Comment on lines +1975 to +2040
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'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

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>
@lapc506 lapc506 merged commit 9aced72 into main May 10, 2026
1 check passed
@lapc506 lapc506 deleted the feat/cd-between-worktrees-hook branch May 10, 2026 09:50
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.

1 participant