Skip to content

fix(hooks): replace Write hook allowlist with targeted denylist#962

Closed
Zandereins wants to merge 2 commits intoaffaan-m:mainfrom
Zandereins:fix/write-hook-denylist-approach
Closed

fix(hooks): replace Write hook allowlist with targeted denylist#962
Zandereins wants to merge 2 commits intoaffaan-m:mainfrom
Zandereins:fix/write-hook-denylist-approach

Conversation

@Zandereins
Copy link
Copy Markdown

@Zandereins Zandereins commented Mar 27, 2026

Description

The current PreToolUse Write hook blocks all .md/.txt files unless they match a hardcoded allowlist (README.md, CLAUDE.md, AGENTS.md, CONTRIBUTING.md, .claude/plans/, .planning/). This causes false positives for users with legitimate markdown-heavy workflows:

Blocked path Use case
docs/specs/*.md Spec-driven development
docs/adr/*.md Architecture Decision Records
commands/*.md Claude Code command definitions
skills/*.md Skill files (SKILL.md, references)
.github/*.md Issue/PR templates
benchmarks/*.md Test fixtures
.claude/*/memory/*.md Auto-memory plugin files

The fix

Instead of allowlisting paths (breaks on every new directory), this PR denylists filenames — blocking only known ad-hoc anti-patterns:

NOTES.md, TODO.md, SCRATCH.md, TEMP.md, DRAFT.md, BRAINSTORM.md, SPIKE.md, DEBUG.md, WIP.md

These are still allowed in structured directories (docs/, .claude/, .github/, commands/, skills/, benchmarks/, templates/) since context matters — docs/specs/NOTES.md is intentional, ~/project/NOTES.md is impulse.

Why denylist > allowlist

Aspect Allowlist (current) Denylist (this PR)
New paths Must update hook Works automatically
Maintenance High (every new dir) Low (~1x/year)
Regex length ~276 chars ~142 chars
Philosophy "Where can you write?" "What shouldn't you name?"
Future-proof No Yes

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature
  • Breaking change
  • Documentation update

Testing

25/25 test cases verified:

Correctly blocked (6):

  • ~/project/NOTES.md, TODO.txt, SCRATCH.md, DRAFT.md, WIP.txt, BRAINSTORM.md

Correctly allowed (19):

  • docs/specs/v7.1-plan.md, docs/adr/001.md, README.md, CLAUDE.md, CHANGELOG.md, SECURITY.md, commands/triage.md, skills/SKILL.md, .claude/memory/feedback.md, .github/ISSUE_TEMPLATE/bug.md, benchmarks/test.md, LICENSE.txt
  • Edge cases: docs/specs/NOTES.md ✅ allowed, notes.md (lowercase) ✅ allowed, todo-list.md ✅ allowed
// Standalone test (run with node):
const hook = (p) => {
  if(/\.(md|txt)$/.test(p)
    &&/^(NOTES|TODO|SCRATCH|TEMP|DRAFT|BRAINSTORM|SPIKE|DEBUG|WIP)\.(md|txt)$/.test(p.split('/').pop())
    &&!/\/(docs|\.claude|\.github|commands|skills|benchmarks|templates)\//i.test(p)){
    return "BLOCKED";
  }
  return "ALLOWED";
};

Checklist

  • My code follows the project's coding standards
  • I have tested my changes locally
  • All validation scripts pass
  • My commits follow the Conventional Commits specification
  • I have updated the documentation accordingly

🤖 Generated with Claude Code


Summary by cubic

Switches the Write hook from a broad .md/.txt allowlist to a targeted denylist of ad‑hoc filenames. Adds relative path support and Windows path normalization to reduce false positives while still blocking scratch files.

  • Bug Fixes
    • Blocks NOTES|TODO|SCRATCH|TEMP|DRAFT|BRAINSTORM|SPIKE|DEBUG|WIP (UPPERCASE only) outside structured paths.
    • Allows files under docs/, .claude/, .github/, commands/, skills/, benchmarks/, templates/; matcher now handles relative paths and normalizes backslashes on Windows.
    • Improves hook message to suggest structured paths; 25/25 tests pass.

Written for commit 39fa933. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Refined file-creation rules for .md and .txt files: ad-hoc temporary filenames (e.g., notes, todo, draft, wip) are now blocked unless placed in recognized structured directories (docs, .claude, .github, commands, skills, benchmarks, templates). Console messages and descriptions were updated to reflect this clearer behavior.

The current PreToolUse Write hook blocks ALL .md/.txt files except a
hardcoded allowlist (README, CLAUDE, AGENTS, CONTRIBUTING, .claude/plans/,
.planning/). This causes false positives for legitimate workflows:

- docs/specs/ and docs/adr/ (spec-driven development)
- commands/ and skills/ (.md-based skill/command definitions)
- .github/ (issue templates, PR templates)
- benchmarks/ (.md test fixtures)
- .claude/*/memory/ (auto-memory plugin files)

The new approach flips the logic: instead of blocking everything and
allowlisting paths, it only blocks known ad-hoc filenames (NOTES, TODO,
SCRATCH, TEMP, DRAFT, BRAINSTORM, SPIKE, DEBUG, WIP) and exempts
structured directories (docs/, .claude/, .github/, commands/, skills/,
benchmarks/, templates/).

Benefits:
- Future-proof: new structured paths work without hook updates
- Shorter regex (142 vs 276 chars for equivalent allowlist)
- Targets the actual anti-pattern (impulse docs) not the file extension
- 25/25 test cases verified (6 blocked, 19 allowed including edge cases)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 534974d9-cd03-48b9-8e02-24b9155a2147

📥 Commits

Reviewing files that changed from the base of the PR and between cab0a49 and 39fa933.

📒 Files selected for processing (1)
  • hooks/hooks.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • hooks/hooks.json

📝 Walkthrough

Walkthrough

Updated the PreToolUse/Write hook in hooks/hooks.json to change the guard logic: instead of broadly blocking .md/.txt file creation with a small allowlist, it now normalizes paths and blocks only ad-hoc doc basenames (NOTES, TODO, SCRATCH, TEMP, DRAFT, BRAINSTORM, SPIKE, DEBUG, WIP) when those files are outside designated structured directories (docs, .claude, .github, commands, skills, benchmarks, templates). Console text and hook description were updated to match.

Changes

Cohort / File(s) Summary
Hook Configuration
hooks/hooks.json
Replaced the node -e guard expression and updated the hook description/error text for the PreToolUse Write matcher. New logic normalizes separators, extracts the basename, checks case-insensitive basename against an ad-hoc-doc list, ensures extension is .md/.txt, and exempts files inside structured directories (case-insensitive).

Sequence Diagram(s)

(omitted — change is a single-file hook logic update and does not introduce multi-component sequential flow)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • affaan-m

Poem

🐰 I hopped through hooks at break of dawn,

Sniffed paths and names on every lawn,
Only stray NOTES and TODOs I stop,
In proper nests they may hop and hop —
Hooray for tidier branches! ✨📄

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: replacing a denylist-based approach with a targeted blocklist for ad-hoc documentation filenames, which directly reflects the primary purpose of this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/write-hook-denylist-approach

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This PR replaces the PreToolUse Write hook's brittle allowlist approach (which caused false positives for any .md/.txt file outside a small hardcoded set of paths) with a focused denylist that only blocks known ad-hoc scratch-file names (NOTES, TODO, SCRATCH, TEMP, DRAFT, BRAINSTORM, SPIKE, DEBUG, WIP) when written outside recognised structured directories.

Key observations:

  • Previous issues resolved: Both prior review concerns are addressed — the relative-path bug (e.g. docs/specs/NOTES.md being incorrectly blocked) is fixed by changing the allowlist regex from \/(docs|...)\/ to (^|\\/)(docs|...)(\\/|$), and the unused const fs=require('fs') has been removed.
  • Logic is sound: The three-part guard (extension check → basename denylist → structured-dir allowlist) correctly models the intended policy; 25 stated test cases are consistent with the regex behaviour.
  • templates/ added silently: The structured-path allowlist gains templates/ without mention in the PR description table; this is a reasonable addition but worth noting.
  • Minor inconsistency (non-blocking): The first extension check (/\.(md|txt)$/i) is case-insensitive, while the basename check (/^(NOTES|...).(md|txt)$/) is case-sensitive, so an all-caps extension like NOTES.TXT would slip through. All-caps extensions are extremely rare in practice, and the hook is advisory rather than a security gate, so this does not warrant blocking the merge.

Confidence Score: 5/5

Safe to merge — all previously flagged P1 issues are resolved and no new blocking issues were found.

Both prior P1 concerns (relative-path allowlist bypass and unused fs require) are addressed in this revision. The remaining observation about case-insensitive extension matching is a minor P2 edge case (all-caps .TXT is vanishingly rare) and does not block merge.

No files require special attention.

Important Files Changed

Filename Overview
hooks/hooks.json Replaces Write hook allowlist with a targeted denylist; fixes relative-path allowlist bug (now uses `(^

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([Write tool fires]) --> B{Extension\n.md or .txt?}
    B -- No --> Z([ALLOWED])
    B -- Yes --> C{Basename matches\ndenylist?\nNOTES/TODO/SCRATCH/\nTEMP/DRAFT/BRAINSTORM/\nSPIKE/DEBUG/WIP}
    C -- No --> Z
    C -- Yes --> D{Path contains\nstructured dir?\ndocs/ .claude/ .github/\ncommands/ skills/\nbenchmarks/ templates/}
    D -- Yes --> Z
    D -- No --> E([BLOCKED\nexit 2])
Loading

Reviews (2): Last reviewed commit: "fix(hooks): handle relative paths and no..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="hooks/hooks.json">

<violation number="1" location="hooks/hooks.json:40">
P1: Write hook path checks are not normalized, causing relative-path false blocks and Windows-path denylist bypass.</violation>

<violation number="2" location="hooks/hooks.json:40">
P1: The structured-path exemption regex only matches directories preceded by `/`, so relative paths like `docs/specs/NOTES.md` are incorrectly blocked. Update the pattern to match either start-of-string or `/` before the directory segment.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hooks/hooks.json`:
- Line 40: The hook's inline command uses slash-specific checks
(split('/').pop(),
/\\/(docs|\\.claude|\\.github|commands|skills|benchmarks|templates)\\//i and a
leading `/`) which mis-handle relative and Windows paths; normalize input paths
before testing (e.g., replace backslashes with forward slashes or use a basename
extractor) and update the directory regex to not require a leading slash so
relative paths match (ensure the basename extraction handles both `\` and `/`
and the directory membership check tests the normalized path for any of
docs/.claude/.github/commands/skills/benchmarks/templates anywhere in the path).
🪄 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: 974f6759-28dd-4ab9-81e9-490445371ebc

📥 Commits

Reviewing files that changed from the base of the PR and between cc60bf6 and cab0a49.

📒 Files selected for processing (1)
  • hooks/hooks.json

Copy link
Copy Markdown
Author

@Zandereins Zandereins left a comment

Choose a reason for hiding this comment

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

Self-Review: Critical regression found

All three review bots (Greptile, Cubic, CodeRabbit) independently identified the same bug — they're right.

Bug: Relative paths bypass the structured-path exemption

The exemption regex \/(docs|\.claude|\.github|commands|skills|benchmarks|templates)\/ requires a leading / before the directory name. Relative paths (the common case in Claude Code) start with docs/, not /docs/, so they never match the exemption and get incorrectly blocked.

Trace for docs/specs/NOTES.md:

  1. \.(md|txt)$ → ✅ match
  2. split('/').pop()NOTES.md → ✅ denylist match
  3. \/(docs|...)\/ on "docs/specs/NOTES.md" → ❌ no leading / → exemption fails
  4. Result: BLOCKED (should be allowed)

Required fixes before merge

  1. Regex: Replace \/ with (^|\/) so both absolute and relative paths match
  2. Cleanup: Remove unused require('fs')
  3. Nice-to-have: Normalize Windows backslashes before regex checks

Will push a fix commit.

…hook

- Fix structured-path exemption regex to match relative paths (docs/specs/NOTES.md)
  by using (^|\/) instead of \/ which required a leading slash
- Normalize Windows backslashes before regex checks for cross-platform safety
- Keep denylist case-sensitive (only block UPPERCASE ad-hoc names like NOTES.md,
  not intentional lowercase notes.md)
- Remove unused require('fs')

Addresses review feedback from Greptile, Cubic, and CodeRabbit on PR affaan-m#962.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@affaan-m
Copy link
Copy Markdown
Owner

Closing as stale. The policy idea is still valid, but this PR edits obsolete hook wiring and is not mergeable against the current hook runtime. Tracked cleanly in follow-up issue #988, which ports the denylist idea to scripts/hooks/doc-file-warning.js and current tests.

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