From cab0a491ce536725a7626c4a4e19cc255f02eb2e Mon Sep 17 00:00:00 2001 From: Zandereins Date: Fri, 27 Mar 2026 10:36:33 +0100 Subject: [PATCH 1/2] fix(hooks): replace Write hook allowlist with targeted denylist 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) --- hooks/hooks.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index d8c8c2d73..894c9f36e 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -37,10 +37,10 @@ "hooks": [ { "type": "command", - "command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/\\.(md|txt)$/.test(p)&&!/(README|CLAUDE|AGENTS|CONTRIBUTING)\\.md$/.test(p)&&!/\\.claude\\/plans\\//.test(p)){console.error('[Hook] BLOCKED: Unnecessary documentation file creation');console.error('[Hook] File: '+p);console.error('[Hook] Use README.md for documentation instead');process.exit(2)}}catch{}console.log(d)})\"" + "command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';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)){console.error('[Hook] BLOCKED: Ad-hoc documentation filename');console.error('[Hook] File: '+p);console.error('[Hook] Use structured paths: docs/specs/, docs/adr/, commands/, skills/');process.exit(2)}}catch{}console.log(d)})\"" } ], - "description": "Block creation of random .md files - keeps docs consolidated" + "description": "Block ad-hoc doc filenames (NOTES, TODO, SCRATCH, etc.) unless in structured paths" }, { "matcher": "Edit|Write", From 39fa93380add7b958624d5b15aa5285f6d7983d5 Mon Sep 17 00:00:00 2001 From: Zandereins Date: Fri, 27 Mar 2026 14:12:39 +0100 Subject: [PATCH 2/2] fix(hooks): handle relative paths and normalize backslashes in Write 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 #962. Co-Authored-By: Claude Opus 4.6 (1M context) --- hooks/hooks.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index 894c9f36e..6ce068169 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -37,7 +37,7 @@ "hooks": [ { "type": "command", - "command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';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)){console.error('[Hook] BLOCKED: Ad-hoc documentation filename');console.error('[Hook] File: '+p);console.error('[Hook] Use structured paths: docs/specs/, docs/adr/, commands/, skills/');process.exit(2)}}catch{}console.log(d)})\"" + "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';const n=p.replace(/\\\\\\\\/g,'/');const b=n.split('/').pop()||'';if(/\\.(md|txt)$/i.test(n)&&/^(NOTES|TODO|SCRATCH|TEMP|DRAFT|BRAINSTORM|SPIKE|DEBUG|WIP)\\.(md|txt)$/.test(b)&&!/(^|\\/)(docs|\\.claude|\\.github|commands|skills|benchmarks|templates)(\\/|$)/i.test(n)){console.error('[Hook] BLOCKED: Ad-hoc documentation filename');console.error('[Hook] File: '+p);console.error('[Hook] Use structured paths: docs/specs/, docs/adr/, commands/, skills/');process.exit(2)}}catch{}console.log(d)})\"" } ], "description": "Block ad-hoc doc filenames (NOTES, TODO, SCRATCH, etc.) unless in structured paths"