HookStack
Back to catalogue
ValidationPostToolUse· Write|Edit

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

#validation#code-quality#debug#console-log#advisory

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
// @hookstack post-write-debug-statement-guard
// Signale les instructions de debug oubliées après une écriture (PostToolUse Write|Edit)
import { existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node: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);
}

Learn more

Related hooks