Why AI Coding Agents Leave Debug Code Behind
AI coding agents debug by inserting print statements and logging to check their own work, then rarely clean them up once a task looks done. Here's why, and a grep pattern plus an AGENTS.md instruction that catches it before it ships.
AI coding agents leave debug code behind because they debug the way a person would sitting at a terminal: they add a print statement, run the code, read the output, and decide what to do next. That loop is how they figure out whether their own change worked. The problem is that nothing in the loop tells them to go back and remove the print statement once the answer comes back clean. The task in their head is "make the test pass" or "fix the bug," not "leave the codebase exactly as it was plus the fix." Cleanup is a separate step that has to be asked for explicitly, and most setups never ask.
Why this keeps happening
An agent working through a bug doesn't have a debugger attached to a UI it can step through visually. Its main way of inspecting state at runtime is to insert something that prints to stdout, run the command, and read the result back in its own context window. That's a console.log in a React component, a print() in a Python script, a debugger statement, a temporary logger.info call, or a commented-out block of the old implementation kept "just in case" while a new version is tried.
This is a reasonable strategy. It's roughly what a competent engineer does when reproducing an issue locally. The difference is that a human engineer has a felt sense of "I'm about to open a PR, let me check my diff for junk," built from years of getting yelled at in code review. An agent's loop typically ends the moment its test or acceptance check passes. There is no step after that unless you put one there, so the print statements it used to get to a passing state just ride along into the commit.
It compounds on multi-step tasks. An agent debugging a flaky test might add three or four temporary log lines across different files while narrowing down the cause, fix the root issue in one of them, and then never revisit the other three because its attention has already moved to the next part of the task. Each one looks trivial in isolation. A handful of them across a real PR is what makes a reviewer stop trusting the diff.
What it looks like in a real diff
A console.log or console.debug left in a request handler, often logging a full payload or user object.
A bare print() in Python, sometimes printing a variable name and value pair used only to confirm a fix.
A debugger or breakpoint() statement that was never triggered again after the bug was found.
A commented-out block of the previous implementation, left instead of deleted because deleting felt riskier.
A TODO: remove this or TODO: cleanup comment that documents the intent to clean up without doing it.
An extra temporary variable or flag added purely to inspect a value mid-function, unused by the actual fix.
None of these break the build. That's exactly why they survive: CI is green, the tests pass, and the feature works. The only thing that catches them is either a human actually reading the full diff line by line, or a check that runs before the human ever sees it.
A grep pattern that catches it before it merges
You don't need a linter plugin for this. A single grep command run against the staged diff catches most of it, and it's cheap enough to run on every commit. Here's one that covers the common cases across JavaScript, TypeScript, and Python:
grep -rnE "console\.(log|debug|warn)\(|debugger;|print\(|breakpoint\(\)|TODO:\s*remove|FIXME:\s*remove" \
--include=\*.{js,jsx,ts,tsx,py} src/Point it at your actual staged files instead of the whole tree so it only flags what's about to be committed:
git diff --cached --name-only --diff-filter=ACM | grep -E "\.(js|jsx|ts|tsx|py)$" \
| xargs grep -nE "console\.(log|debug|warn)\(|debugger;|print\(|breakpoint\(\)" 2>/dev/nullWire that second command into a pre-commit hook (Husky, pre-commit, or a plain .git/hooks/pre-commit script) and it fails the commit with the exact file and line number before the code ever reaches a PR. It will occasionally flag a legitimate print() call or an intentional console.log behind a debug flag, that's fine, false positives here cost a few seconds to dismiss, while a missed one costs a reviewer's attention and sometimes a production log full of user data.
Teach the agent to check its own diff
The grep hook is the backstop. The cheaper fix is upstream of that: tell the agent, explicitly, that finishing the task includes checking for what it added along the way. Most AGENTS.md or system-prompt files describe what a good implementation looks like and skip what a clean commit looks like. Add both. A few lines that work well in practice:
Before declaring a task complete, run a self-check for debug artifacts: search your changes for console.log, print(, debugger, breakpoint(), and commented-out code blocks, and remove any that aren't part of the intended change.
If you added a temporary log statement to verify a fix while working, remove it once the fix is confirmed. Do not leave verification code in the final diff.
Treat any TODO or FIXME comment you write as something to resolve before finishing, not something to hand off. If it genuinely can't be resolved now, say so explicitly in your summary instead of leaving the comment behind.
Run git diff on your own changes as a final step and read it as a reviewer would, specifically looking for anything that wouldn't survive a code review.
That last instruction matters more than it looks. Asking an agent to re-read its own diff before calling the task done reproduces, roughly, the habit a senior engineer has built over years: never submit a PR you haven't looked at yourself. Agents don't have that habit by default. You have to write it down.
Where this fits among other agent failure modes
This is a narrow problem and worth keeping narrow. Leftover debug statements are not the same failure as an agent rewriting a test to make it pass, and they're not the same as an agent introducing a security vulnerability or wandering into files outside the task. Those are worth separate defenses. This one is specifically about the print statements and commented blocks an agent leaves behind on its way to a working solution, and it's cheap enough to solve with a grep pattern and one paragraph of instructions that there's no reason to leave it unaddressed.
If you're also dealing with an agent that games its own tests to get to green, that's a related but distinct problem worth reading separately.
Pairing the pre-commit check above with using an AI coding agent to review a pull request gives you two independent passes before anything reaches a human. It also helps to get an agent to write smaller pull requests, since a five-file diff is where a stray console.log actually gets read, unlike a forty-file one.
The self-audit instruction above works best alongside the broader habit of making an AI coding agent follow your code style: once an agent is already checking its output against a written standard, adding a line about debug artifacts is a small extension of a check it's already running. More general guidance on setting up agents for a codebase lives on our AI coding tools page.
How did this land?
About the author

Staff Engineer, Platform
Carlo works on the platform that turns prompts into running apps. He writes the engineering deep dives and the changelog notes worth reading.


