fix: suppress context-drift-check false positives (generated-bridge, renamed, external-repo refs)#32
Conversation
…e, renamed, and external-repo path references
The broken-path check flagged every backtick-quoted path that did not
exist locally, producing false positives on three legitimate reference
categories: generated-bridge filenames the plugin emits into other
projects (e.g. GEMINI.md), historical rename mentions ("renamed from
`index.md`"), and paths in other repos ("marketing-site
`scripts/docs-sync.config.ts`").
Add a generated-bridge basename allowlist and a same-line cue-word skip.
Genuinely missing repo-local paths are still flagged. Applied to both the
canonical hooks/ copy and the dogfooded .claude/hooks/ copy (kept in
sync), plus 4 new hook unit tests (120/120 passing).
Closes #31
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughTwo copies of context-drift-check.sh gain GENERATED_BRIDGES and DESCRIPTIVE_CUE constants used to skip false-positive broken-path warnings for backtick-quoted references to generated bridge filenames, renamed files, or external-repo paths. Regression tests and a CHANGELOG entry accompany the change. ChangesBroken-path false-positive suppression
Estimated code review effort: 2 (Simple) | ~12 minutes Sequence Diagram(s)Not applicable — the changes are localized shell-script conditional logic, tests, and documentation without multi-component interaction flows. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.claude/hooks/context-drift-check.sh (1)
86-100: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPreserve per-occurrence context before the cue-word skip
Lines 86-100: this check is file-wide, so one descriptive mention ofdocs/api.mdwill skip every occurrence of that path in the file, including a genuinely broken one on another line. Keep line numbers through the match, or defersort -uuntil after the cue check.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.claude/hooks/context-drift-check.sh around lines 86 - 100, The reference-skip logic in context-drift-check.sh is applied too broadly because the grep for `$REF_PATH` is file-wide and the later `sort -u` collapses duplicate matches, so one descriptive mention can suppress a truly broken occurrence elsewhere. Update the matching flow in the loop around `SKIP_REF`, `REF_PATH`, and `BASENAME` so each occurrence is evaluated independently, preserving per-match line context before applying the descriptive cue check or deferring deduplication until after that check.
🧹 Nitpick comments (2)
tests/test-hooks.sh (1)
432-466: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winTests correctly validate the intended suppression cases.
Traced each scenario against the hook logic — bridge, rename-cue, marketing-site-cue, and genuinely-broken cases all resolve as expected. Shellcheck's SC2016 hints on the single-quoted
printflines are expected here (literal backticks, no expansion wanted) and can be ignored.Consider adding a regression test for the basename-collision edge case flagged in
hooks/context-drift-check.sh(a nested path like`docs/other/CLAUDE.md`that doesn't exist), to lock in the fix once addressed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test-hooks.sh` around lines 432 - 466, Add a regression test for the basename-collision edge case in the broken-path suppression suite. Extend the scenarios in tests/test-hooks.sh to cover a nested repo-local path like `docs/other/CLAUDE.md` that does not exist, and assert the hook still reports it as missing instead of suppressing it. Use the existing run_test pattern alongside the current false-positive suppression cases to keep the behavior locked in.hooks/context-drift-check.sh (1)
41-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSeveral
GENERATED_BRIDGESentries can never be matched.The extraction regex on line 99 only captures backtick tokens that start with
[a-zA-Z]and end in.(ts|js|py|go|rs|md|json|toml|yaml|yml|sh). That means.windsurfrules,.cursorrules,.clinerules(leading dot fails the alpha start), andllms.txt,llms-full.txt,agents.mdc(extension not in the allowed list) can never be extracted asREF_PATHin the first place, so their allowlist entries are dead code.Not a functional regression (these were never flagged before either), but it's misleading — the allowlist implies coverage for exactly the file types called out in the PR description as sources of false positives.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hooks/context-drift-check.sh` around lines 41 - 51, Several entries in GENERATED_BRIDGES are unreachable because the reference-extraction regex only matches backtick paths with an alphabetic start and a limited extension set. Update the allowlist in context-drift-check.sh so it only includes filenames that can actually be captured by the REF_PATH extraction logic, or broaden that extraction rule if those dotfiles and alternate extensions are meant to be supported. Keep the GENERATED_BRIDGES list aligned with the regex behavior to avoid dead entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.claude/hooks/context-drift-check.sh:
- Around line 44-46: The `context-drift-check.sh` bridge allowlist includes
`agents.mdc`, but the backtick-reference extraction regex used by the script
does not match the `.mdc` extension, so that entry can never be reached. Update
the reference extraction pattern in the script’s regex logic to include `mdc`,
and ensure the `GENERATED_BRIDGES` entry for `agents.mdc` remains aligned with
the symbols used for bridge detection.
In `@hooks/context-drift-check.sh`:
- Around line 78-96: The broken-path detection in context-drift-check.sh is too
broad because GENERATED_BRIDGES and the descriptive-cue fallback are applied by
basename or by any same-path match, which can wrongly skip real bad references.
Tighten the skip logic around REF_PATH/BASENAME so generated-bridge suppression
only applies to exact bare bridge filenames, and make the descriptive cue check
evaluate the specific reference occurrence rather than any matching path
elsewhere in the CTX content.
---
Outside diff comments:
In @.claude/hooks/context-drift-check.sh:
- Around line 86-100: The reference-skip logic in context-drift-check.sh is
applied too broadly because the grep for `$REF_PATH` is file-wide and the later
`sort -u` collapses duplicate matches, so one descriptive mention can suppress a
truly broken occurrence elsewhere. Update the matching flow in the loop around
`SKIP_REF`, `REF_PATH`, and `BASENAME` so each occurrence is evaluated
independently, preserving per-match line context before applying the descriptive
cue check or deferring deduplication until after that check.
---
Nitpick comments:
In `@hooks/context-drift-check.sh`:
- Around line 41-51: Several entries in GENERATED_BRIDGES are unreachable
because the reference-extraction regex only matches backtick paths with an
alphabetic start and a limited extension set. Update the allowlist in
context-drift-check.sh so it only includes filenames that can actually be
captured by the REF_PATH extraction logic, or broaden that extraction rule if
those dotfiles and alternate extensions are meant to be supported. Keep the
GENERATED_BRIDGES list aligned with the regex behavior to avoid dead entries.
In `@tests/test-hooks.sh`:
- Around line 432-466: Add a regression test for the basename-collision edge
case in the broken-path suppression suite. Extend the scenarios in
tests/test-hooks.sh to cover a nested repo-local path like
`docs/other/CLAUDE.md` that does not exist, and assert the hook still reports it
as missing instead of suppressing it. Use the existing run_test pattern
alongside the current false-positive suppression cases to keep the behavior
locked in.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e9688f85-86b7-45fb-938e-40ad658b014d
📒 Files selected for processing (4)
.claude/hooks/context-drift-check.shCHANGELOG.mdhooks/context-drift-check.shtests/test-hooks.sh
| GENERATED_BRIDGES=("GEMINI.md" "AGENTS.md" "CLAUDE.md" "llms.txt" "llms-full.txt" | ||
| ".windsurfrules" ".cursorrules" ".clinerules" | ||
| "copilot-instructions.md" "agents.mdc" "agents.md") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
agents.mdc bridge entry is unreachable — extraction regex has no .mdc extension.
GENERATED_BRIDGES includes "agents.mdc" (Line 46), but the backtick-reference extraction regex on Line 99 only matches ts|js|py|go|rs|md|json|toml|yaml|yml|sh extensions. A reference like `agents.mdc` will never even be captured, so this allowlist entry can never fire — it's dead. If Cursor's modern agents.mdc (mentioned in CHANGELOG Line 25) needs suppression too, the extraction regex needs the extension added.
🐛 Proposed fix
done < <(grep -oE '`[a-zA-Z][a-zA-Z0-9._/-]+\.(ts|js|py|go|rs|md|json|toml|yaml|yml|sh)`' "$CTX" 2>/dev/null \
+ done < <(grep -oE '`[a-zA-Z][a-zA-Z0-9._/-]+\.(ts|js|py|go|rs|md|mdc|json|toml|yaml|yml|sh)`' "$CTX" 2>/dev/null \Also applies to: 99-100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/hooks/context-drift-check.sh around lines 44 - 46, The
`context-drift-check.sh` bridge allowlist includes `agents.mdc`, but the
backtick-reference extraction regex used by the script does not match the `.mdc`
extension, so that entry can never be reached. Update the reference extraction
pattern in the script’s regex logic to include `mdc`, and ensure the
`GENERATED_BRIDGES` entry for `agents.mdc` remains aligned with the symbols used
for bridge detection.
| BASENAME=$(basename "$REF_PATH") | ||
| if ! git ls-files "*/$BASENAME" "$BASENAME" 2>/dev/null | grep -q . \ | ||
|
|
||
| # Skip generated-bridge filenames — plugin output, not a local file. | ||
| SKIP_REF=false | ||
| for BRIDGE in "${GENERATED_BRIDGES[@]}"; do | ||
| if [ "$BASENAME" = "$BRIDGE" ]; then SKIP_REF=true; break; fi | ||
| done | ||
|
|
||
| # Skip references marked descriptive by a same-line cue word (rename | ||
| # history or a path that lives in another repo). | ||
| if ! $SKIP_REF && grep -F -- "$REF_PATH" "$CTX" 2>/dev/null | grep -qiE "$DESCRIPTIVE_CUE"; then | ||
| SKIP_REF=true | ||
| fi | ||
|
|
||
| # Fallback: check if basename exists anywhere in repo (tracked or untracked) | ||
| if ! $SKIP_REF \ | ||
| && ! git ls-files "*/$BASENAME" "$BASENAME" 2>/dev/null | grep -q . \ | ||
| && ! find . -name "$BASENAME" -not -path './.git/*' -print -quit 2>/dev/null | grep -q .; then | ||
| BROKEN_PATHS+=("$CTX references \`$REF_PATH\` (not found)") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target script and related symbols.
git ls-files hooks/context-drift-check.sh
wc -l hooks/context-drift-check.sh
sed -n '1,220p' hooks/context-drift-check.sh
printf '\n--- search ---\n'
rg -n 'GENERATED_BRIDGES|CONTEXT_FILES|DESCRIPTIVE_CUE|SKIP_REF|BROKEN_PATHS|REF_PATH|BASENAME' hooks/context-drift-check.shRepository: littlebearapps/contextdocs
Length of output: 6267
🏁 Script executed:
python3 - <<'PY'
from pathlib import PurePosixPath
import re
GENERATED_BRIDGES = {"GEMINI.md", "AGENTS.md", "CLAUDE.md", "llms.txt", "llms-full.txt",
".windsurfrules", ".cursorrules", ".clinerules",
"copilot-instructions.md", "agents.mdc", "agents.md"}
DESCRIPTIVE_CUE = re.compile(r"renamed|formerly|previously|used to be|marketing[- ]site|external repo|upstream repo|other repo", re.I)
def should_skip(ref_path, ctx_text):
# mirrors the script's logic closely
if not ref_path:
return False
if True: # [ -n "$REF_PATH" ] && [ ! -e "$REF_PATH" ] is modeled as missing inputs only
basename = PurePosixPath(ref_path).name
skip = False
for bridge in GENERATED_BRIDGES:
if basename == bridge:
skip = True
break
if not skip:
# grep -F -- "$REF_PATH" "$CTX" | grep -qiE "$DESCRIPTIVE_CUE"
matching_lines = [line for line in ctx_text.splitlines() if ref_path in line]
if any(DESCRIPTIVE_CUE.search(line) for line in matching_lines):
skip = True
return skip
cases = [
(
"docs/other-project/CLAUDE.md",
"See `docs/other-project/CLAUDE.md` for details.",
"A missing nested CLAUDE.md reference"
),
(
"docs/other-project/CLAUDE.md",
"renamed from `docs/other-project/CLAUDE.md`\nLater we still reference `docs/other-project/CLAUDE.md` elsewhere.",
"Same ref string appears on a cue line and a non-cue line"
),
(
"scripts/docs-sync.config.ts",
"The marketing-site `scripts/docs-sync.config.ts` mapping.",
"Descriptive external path"
),
]
for ref, ctx, label in cases:
print(f"\nCASE: {label}")
print("ref:", ref)
print("skip:", should_skip(ref, ctx))
PYRepository: littlebearapps/contextdocs
Length of output: 439
Restrict bridge skipping to exact filename refs.
- Matching
GENERATED_BRIDGESonbasenameletsdocs/other-project/CLAUDE.mdbypass the broken-path check even though it is not a bare generated-bridge reference. - The cue-word fallback has the same per-path limitation: if the same path string appears more than once in a file, one descriptive mention can suppress another broken occurrence.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hooks/context-drift-check.sh` around lines 78 - 96, The broken-path detection
in context-drift-check.sh is too broad because GENERATED_BRIDGES and the
descriptive-cue fallback are applied by basename or by any same-path match,
which can wrongly skip real bad references. Tighten the skip logic around
REF_PATH/BASENAME so generated-bridge suppression only applies to exact bare
bridge filenames, and make the descriptive cue check evaluate the specific
reference occurrence rather than any matching path elsewhere in the CTX content.
Summary
Fixes #31 — the Context Guard drift check (
context-drift-check.sh) reported false positives for backtick-quoted references that legitimately do not exist in the local repo. Found by dogfooding: a trivial.gitignorecommit on this repo triggered five "not found" warnings, all false positives.Root cause
The broken-path check extracted every backtick-quoted
`….(ts|js|md|…)`token and flagged it unless it existed locally — assuming every such token is a claim the file exists in this repo. Three legitimate categories break that assumption:GEMINI.md,.windsurfrules, …)index.md" → the real file isfaq.md)scripts/docs-sync.config.tsmapping")Fix
renamed,formerly,previously,marketing-site,external repo, …).Applied identically to both tracked copies: the canonical
hooks/context-drift-check.sh(what ships to users / what the test suite runs) and the dogfooded.claude/hooks/context-drift-check.sh(kept byte-in-sync, as all 7 hooks are).Testing
tests/test-hooks.sh: three suppression cases (GEMINI.md / rename mention / marketing-site) + one regression guard (src/missing.tsstill flagged).{}(previously 5 false positives).typosclean.Not included
Two softer options from #31 (an ignore-file mechanism, repo-relative-path biasing) were left out — the allowlist + cue-word combination clears all observed false positives without them. Easy follow-ups if new categories appear.
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests
Documentation