diff --git a/.claude/hooks/block_dangerous_commands.py b/.claude/hooks/block_dangerous_commands.py index ab0d21e1..b7f2a426 100755 --- a/.claude/hooks/block_dangerous_commands.py +++ b/.claude/hooks/block_dangerous_commands.py @@ -21,9 +21,12 @@ # ── Dangerous patterns (regex) ────────────────────────────────── DANGEROUS_PATTERNS = [ # === Git destructive operations === - (r"git\s+push\s+.*--force", "git push --force (force push)"), - (r"git\s+push\s+.*-f\b", "git push -f (force push)"), - (r"git\s+push\s+.*--force-with-lease", "git push --force-with-lease"), + # Bound the wildcard to the push invocation's own args: [^|&;<>]* stops at a + # pipe/redirect/separator so a later `-f` in a chained command or a flattened + # heredoc body (newlines collapse to spaces) cannot trigger a false positive. + (r"git\s+push\s+[^|&;<>]*--force", "git push --force (force push)"), + (r"git\s+push\s+[^|&;<>]*-f\b", "git push -f (force push)"), + (r"git\s+push\s+[^|&;<>]*--force-with-lease", "git push --force-with-lease"), (r"git\s+push\s+\S+\s+\+", "git push origin +branch (force push)"), (r"git\s+clean\s+.*-f", "git clean -f (delete untracked files)"), (r"git\s+reflog\s+expire", "git reflog expire (destroy recovery data)"), diff --git a/test/hooks-integrity.test.js b/test/hooks-integrity.test.js index f404224d..1394eba7 100644 --- a/test/hooks-integrity.test.js +++ b/test/hooks-integrity.test.js @@ -106,11 +106,17 @@ describe('Claude Code Hooks integrity', () => { }); test('should block git force push (--force)', () => { - expect(content).toContain('git\\s+push\\s+.*--force'); + expect(content).toContain('git\\s+push\\s+[^|&;<>]*--force'); }); test('should block git force push (-f)', () => { - expect(content).toContain('git\\s+push\\s+.*-f\\b'); + expect(content).toContain('git\\s+push\\s+[^|&;<>]*-f\\b'); + }); + + test('should bound force-push wildcard to avoid chained-command false positives', () => { + // [^|&;<>]* stops at a pipe/redirect/separator so a later -f in a chained + // command or flattened heredoc body does not trigger a false positive. + expect(content).not.toContain('git\\s+push\\s+.*-f\\b'); }); test('should block git reset --hard', () => {