Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions .claude/hooks/context-drift-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ fi
CONTEXT_FILES=("CLAUDE.md" "AGENTS.md" "GEMINI.md" ".cursorrules"
".github/copilot-instructions.md" "llms.txt" ".windsurfrules" ".clinerules")

# Generated-bridge filenames the plugin emits into *other* projects. A context
# file may list these as outputs without them existing in this repo, so a
# missing local copy is not drift. (Basename match.)
GENERATED_BRIDGES=("GEMINI.md" "AGENTS.md" "CLAUDE.md" "llms.txt" "llms-full.txt"
".windsurfrules" ".cursorrules" ".clinerules"
"copilot-instructions.md" "agents.mdc" "agents.md")
Comment on lines +44 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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


# Same-line cue words that mark a reference as descriptive rather than a live
# local path: rename history ("renamed from `index.md`") or a file in another
# repo ("the marketing-site `scripts/docs-sync.config.ts` mapping").
DESCRIPTIVE_CUE='renamed|formerly|previously|used to be|marketing[- ]site|external repo|upstream repo|other repo'

STALE=()
BROKEN_PATHS=()

Expand All @@ -63,9 +75,23 @@ for CTX in "${CONTEXT_FILES[@]}"; do
# Quick broken-path check: extract backtick-quoted file references
while IFS= read -r REF_PATH; do
if [ -n "$REF_PATH" ] && [ ! -e "$REF_PATH" ]; then
# Fallback: check if basename exists anywhere in repo (tracked or untracked)
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)")
fi
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* Context Guard drift check no longer reports false positives for backtick references to generated-bridge files (e.g. `GEMINI.md`), historical rename mentions ("renamed from `index.md`"), or paths in other repos ("marketing-site `scripts/docs-sync.config.ts`") ([#31](https://github.com/littlebearapps/contextdocs/issues/31))
* Context Guard hooks no longer flag infrastructure rule files as false positives ([#13](https://github.com/littlebearapps/contextdocs/issues/13))
* ai-context skill triggers more reliably when you describe stale context or MEMORY.md promotion scenarios
* SKILL.md and CLAUDE.md descriptions now match the canonical product definition across all tools
Expand Down
30 changes: 28 additions & 2 deletions hooks/context-drift-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ fi
CONTEXT_FILES=("CLAUDE.md" "AGENTS.md" "GEMINI.md" ".cursorrules"
".github/copilot-instructions.md" "llms.txt" ".windsurfrules" ".clinerules")

# Generated-bridge filenames the plugin emits into *other* projects. A context
# file may list these as outputs without them existing in this repo, so a
# missing local copy is not drift. (Basename match.)
GENERATED_BRIDGES=("GEMINI.md" "AGENTS.md" "CLAUDE.md" "llms.txt" "llms-full.txt"
".windsurfrules" ".cursorrules" ".clinerules"
"copilot-instructions.md" "agents.mdc" "agents.md")

# Same-line cue words that mark a reference as descriptive rather than a live
# local path: rename history ("renamed from `index.md`") or a file in another
# repo ("the marketing-site `scripts/docs-sync.config.ts` mapping").
DESCRIPTIVE_CUE='renamed|formerly|previously|used to be|marketing[- ]site|external repo|upstream repo|other repo'

STALE=()
BROKEN_PATHS=()

Expand All @@ -63,9 +75,23 @@ for CTX in "${CONTEXT_FILES[@]}"; do
# Quick broken-path check: extract backtick-quoted file references
while IFS= read -r REF_PATH; do
if [ -n "$REF_PATH" ] && [ ! -e "$REF_PATH" ]; then
# Fallback: check if basename exists anywhere in repo (tracked or untracked)
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)")
Comment on lines 78 to 96

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.sh

Repository: 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))
PY

Repository: littlebearapps/contextdocs

Length of output: 439


Restrict bridge skipping to exact filename refs.

  • Matching GENERATED_BRIDGES on basename lets docs/other-project/CLAUDE.md bypass 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.

fi
Expand Down
35 changes: 35 additions & 0 deletions tests/test-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,41 @@ run_test "Stale llms.txt → drift detected" "$HOOK" \
'{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "AI CONTEXT DRIFT DETECTED"
teardown_git_repo

echo ""
echo "--- Broken-path false-positive suppression (issue #31) ---"

# Generated-bridge filename referenced but absent locally → not drift
setup_git_repo
printf '%s\n' 'Thin bridges include `GEMINI.md` for Gemini CLI.' > "$TEMP_REPO/CLAUDE.md"
git add CLAUDE.md && git commit -q -m "add context"
run_test "Generated-bridge ref (GEMINI.md) → not flagged" "$HOOK" \
'{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "{}"
teardown_git_repo

# Rename-history mention on the same line → not drift
setup_git_repo
printf '%s\n' 'The FAQ was renamed from `index.md` to faq.md.' > "$TEMP_REPO/CLAUDE.md"
git add CLAUDE.md && git commit -q -m "add context"
run_test "Rename mention (index.md) → not flagged" "$HOOK" \
'{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "{}"
teardown_git_repo

# External-repo path flagged by cue word → not drift
setup_git_repo
printf '%s\n' 'Synced by the marketing-site `scripts/build.ts` mapping.' > "$TEMP_REPO/CLAUDE.md"
git add CLAUDE.md && git commit -q -m "add context"
run_test "External-repo path (marketing-site) → not flagged" "$HOOK" \
'{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "{}"
teardown_git_repo

# Regression guard: a genuinely missing repo-local path is still flagged
setup_git_repo
printf '%s\n' 'See `src/missing.ts` for the handler.' > "$TEMP_REPO/CLAUDE.md"
git add CLAUDE.md && git commit -q -m "add context"
run_test "Genuinely broken ref (src/missing.ts) → still flagged" "$HOOK" \
'{"tool_name":"Bash","tool_input":{"command":"git commit -m test"}}' 0 "not found"
teardown_git_repo

echo ""

########################################################################
Expand Down
Loading