Leftover debug statement guard
No stray console.log or print() slips into a commit
After each write, scans the touched file for forgotten debug traces — console.log/debugger (JS/TS), print()/breakpoint()/pdb (Python), dbg! (Rust) — and flags them. Non-blocking and test-file aware, so it nudges without interrupting the flow.
What does the Leftover debug statement guard hook do?
Leftover debug statement guard is a Claude Code PostToolUse hook matching Write|Edit. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. No stray console.log or print() slips into a commit.
As a PostToolUse hook it runs after the action, reacting to what just happened rather than blocking it. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.
Use cases
- Clean commits
- Code review hygiene
- Catch debug leftovers
Tags
settings.json fragment
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/debug-statement-guard.mjs",
"type": "command"
}
],
"matcher": "Write|Edit"
}
]
}
}Script · .claude/hooks/debug-statement-guard.mjs
#!/usr/bin/env node
// Signale les instructions de debug oubliées après une écriture (PostToolUse Write|Edit)
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
// Par famille de fichiers : motif -> libellé. Non bloquant, purement informatif.
const RULES = [
{ ext: /\.(?:[mc]?[jt]sx?)$/, patterns: [
[/\bconsole\.(?:log|debug|trace)\s*\(/, 'console.log/debug'],
[/\bdebugger\b\s*;?/, 'debugger'],
] },
{ ext: /\.py$/, patterns: [
[/^\s*print\s*\(/m, 'print('],
[/\bbreakpoint\s*\(\s*\)/, 'breakpoint()'],
[/\b(?:import\s+pdb|pdb\.set_trace\s*\()/, 'pdb'],
] },
{ ext: /\.rs$/, patterns: [
[/\bdbg!\s*\(/, 'dbg!'],
] },
];
function isTestFile(p) {
return /(?:\.|_|\b)(?:test|spec)\.[mc]?[jt]sx?$/.test(p) || /(?:^|\/)test_|_test\.py$/.test(p);
}
export function run(input, { readFile = readFileSync, fileExists = existsSync } = {}) {
const filePath = input.tool_input?.file_path ?? '';
if (!filePath || !fileExists(filePath) || isTestFile(filePath)) return null;
const rule = RULES.find((r) => r.ext.test(filePath));
if (!rule) return null;
let content;
try { content = readFile(filePath, 'utf8'); } catch { return null; }
const found = rule.patterns.filter(([re]) => re.test(content)).map(([, label]) => label);
if (found.length === 0) return null;
return {
message:
`[debug-statement] ${filePath} contient des traces de debug oubliées : ${found.join(', ')}. ` +
'Retirez-les avant de commiter.\n',
};
}
/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const input = JSON.parse(readFileSync(0, 'utf8'));
const result = run(input);
if (result?.message) process.stderr.write(result.message);
}