ES · Leer en español Explore for free

Recipe · Intermediate · 15 min

Debug a bug with Claude Code: from the error to a regression test

A six-step debugging flow for Claude Code: reproduce, hand over the evidence, hypotheses without edits, a failing test first and a minimal fix.

Cover: Debug a bug with Claude Code: from the error to a regression test

Tested with Claude Code 2.1.316 · reviewed

See the files on GitHub Free · MIT license

The problem

Asking Claude Code to "fix it" usually ends in a patch for the symptom, a test tweaked until it passes, or a bug that comes back a week later. With a bare "fix it" Claude only knows something is wrong and has to guess what, where and how to check that it's fixed. This recipe gives you a six-step flow that ends with a regression test that keeps the bug from coming back, with a real example: installments that add up to 99.99 instead of 100.00.

The solution

The official best practices guide explains it with an example: instead of "fix the login bug", write "users report that login fails after session timeout; check the auth flow in src/auth/, especially token refresh; write a failing test that reproduces the issue, then fix it". The difference is information Claude can't read in the code: the observed symptom, where to look and what "fixed" means. The official tips for bugs are three: give it the command that reproduces it and the stack trace, tell it the steps to reproduce it, and say whether the failure is consistent or intermittent. The steps below turn those tips into a repeatable flow.

1. Reproduce it before touching anything

A bug you can't reproduce is a bug you can't call fixed. Before opening Claude, get a command that triggers it: a test that already fails, a curl against your local API, a five-line script or the exact click sequence. Write down the expected result and the one you get, with concrete data ("100.00 in 3 installments adds up to 99.99"), not "it's wrong".

If it's intermittent, run it several times and note the ratio ("fails 1 in 10"): it completely changes the hypotheses (races, cache, time, test order). If you know since when it happens, give the commit or the version; git log and git bisect are clues Claude uses very well. If you still have no way to reproduce it, that is the first job: "help me write a script that reproduces this, without fixing anything".

Our example, cuotas.py (Spanish for "installments"), rounds each installment with float:

def split_into_installments(total: str, n: int) -> list[float]:
    installment = round(float(total) / n, 2)
    return [installment] * n

split_into_installments("100.00", 3) returns three installments of 33.33, which add up to 99.99.

2. Hand over the evidence, not your theory

Give Claude what you already have instead of describing it. For a file, type @ and the path (@logs/error.log) and its content goes into the message. For an error on screen, paste the screenshot into the prompt. For a command's output, the official docs suggest piping it from the shell:

cat logs/error.log | claude -p "what's going on here? give me hypotheses, don't edit anything"

Paste the full stack trace, not the last line: the paths and line numbers in the middle are the ones that point to the origin. Avoid leading with your suspicion ("I think it's the cache"): if you say it, Claude tends to look where you point. Better to describe the symptom and let it form hypotheses from the evidence; if you have a suspicion, add it at the end as one more hypothesis. One bug per conversation: when you're done, /clear so leftovers from one bug don't pollute the next.

This template puts the whole flow into a single message. Copy it and fill in what's between < >:

There's a bug: <observable symptom, with concrete data>.
It reproduces with: <exact command> (always fails | fails 1 in N times).
Output/error: @logs/error.log  (or paste it as is, with the full stack trace)
Expected: <correct result>. Got: <current result>.
Since when: <commit, version or suspicious change, if you know>.

How I want you to work:
1. Reproduce it yourself first with the command and show me the output.
2. Give me 2-3 hypotheses ordered by likelihood and how you'd rule out each one. Don't edit anything yet.
3. If needed, add temporary logs marked with DEBUG-CLAUDE and run it again.
4. Write a regression test that fails because of this bug and show me it fails.
5. Make the minimal change that turns it green. Don't modify or loosen the test.
6. Run the whole suite, remove the DEBUG-CLAUDE logs and summarize the root cause in 3 lines.

3. Hypotheses in plan mode, no edits

Before it changes a line, ask it to think. /plan enters plan mode from the prompt and accepts a description, for example /plan the installment sum doesn't add up, see bug #42. In plan mode Claude reads and proposes, but doesn't edit your code until you approve the plan. Ask for two or three hypotheses ordered by likelihood and, for each one, which observation would confirm or rule it out. That way you see whether it understood the problem before spending time on the wrong fix.

If the bug is in code you didn't write, in a flaky test or in a schema change with many readers, the official guide recommends raising the effort with /effort high or /effort xhigh: Claude checks more edge cases and verifies its work more, at the cost of more time and tokens. For a simple bug, the default effort is enough.

4. Temporary logs that are easy to remove

When reading the code isn't enough, the quick way to rule out hypotheses is to observe: print intermediate values, timings or the order of calls. Ask for every temporary log to carry a fixed marker, for example a DEBUG-CLAUDE comment or prefix, so you can find and remove them at the end with grep -rn "DEBUG-CLAUDE" ..

Let Claude run the reproduction command and read the output itself: it's more reliable than copying and pasting by hand. If the output is huge (thousands of log lines or a slow suite), delegate that part to a subagent: it reads everything in its own context and only returns the summary, so the main conversation doesn't fill up. By default Claude asks permission to run commands; with auto mode or an allow rule for your test command (Bash(python3 -m unittest *), see permissions) the loop is much smoother.

5. Regression test in red, minimal fix in green

This is the step that keeps the bug from coming back. First ask for a test that reproduces the failure exactly and for Claude to show you that it fails (red). If the test passes with the broken code, it proves nothing. Then the minimal change that turns it green, and the full suite to check nothing else broke. Say it explicitly in the prompt: "don't modify or loosen the test". A common agent failure is to loosen the assertion or rewrite the test until it passes.

The example's regression test, test_cuotas.py, compares the sum with Decimal("100.00"). Name the test after the bug number so it makes sense in the future:

import unittest
from decimal import Decimal

from cuotas import split_into_installments


class TestInstallments(unittest.TestCase):
    def test_regression_bug_42_exact_sum(self):
        # Bug #42: 100.00 in 3 installments added up to 99.99
        installments = split_into_installments("100.00", 3)
        self.assertEqual(sum(Decimal(str(c)) for c in installments), Decimal("100.00"))

    def test_exact_division_unchanged(self):
        installments = split_into_installments("90.00", 3)
        self.assertEqual([Decimal(str(c)) for c in installments], [Decimal("30.00")] * 3)


if __name__ == "__main__":
    unittest.main()

The fix works with Decimal, rounds down and adds the leftover cents to the first installment (33.34 + 33.33 + 33.33):

from decimal import Decimal, ROUND_DOWN


def split_into_installments(total: str, n: int) -> list[Decimal]:
    """Splits an amount into n 2-decimal installments that add up exactly to the total."""
    if n < 1:
        raise ValueError("n must be >= 1")
    total_d = Decimal(total)
    base = (total_d / n).quantize(Decimal("0.01"), rounding=ROUND_DOWN)
    installments = [base] * n
    # The remainder (cents lost when rounding) goes to the first installment
    installments[0] += total_d - base * n
    return installments

6. Review, clean up and leave a record

Before the commit, review the git diff yourself: the change should be small, no DEBUG-CLAUDE logs should remain and the new test should be included. /code-review (a bundled skill) reviews the current diff for correctness bugs if you want a second opinion. Ask Claude for a three-line root-cause summary and use it in the commit or PR message.

Watch out for /rewind: it undoes the changes Claude made with its editing tools, but a file changed only by Bash commands isn't tracked and stays as it is. That's why it pays to commit (or at least git stash) before you start debugging: git is your real safety net. If the same kind of bug shows up twice, turn it into a rule: a line in CLAUDE.md ("amounts are always Decimal") or a hook that runs the tests after every edit.

Check it

With the buggy cuotas.py, the regression test must fail; with the fix, it must pass:

# Red: with the buggy code, the regression test fails
python3 -m unittest -q
# AssertionError: Decimal('99.99') != Decimal('100.00')
# FAILED (failures=1)

# Green: after the fix
python3 -m unittest -q
# OK

# Before the commit: no temporary logs left
git diff
grep -rn "DEBUG-CLAUDE" . && echo "Temporary logs left"

We tested it on 1 Nov 2026 on Linux with Python 3.12.3 and Claude Code 2.1.316: the test gives exactly that AssertionError with the buggy code and OK with the fix. The Claude Code features we mention (/plan with a description, /effort, /code-review, /debug, the limits of /rewind and cat … | claude -p) come from the official documentation; we haven't yet recorded a full interactive session with the prompt template. The example files are in the ctrlia-depurar-bug repository (English version in en/; run ./en/ejemplo/rojo-verde.sh).

Why it works

Claude does much better when it can check its own work: a command that reproduces the failure and a test that goes from red to green give it an objective signal of "fixed" instead of its own opinion. Separating the hypotheses (plan mode, no edits) from the fix stops it from jumping at the first patch, and the regression test stays in the suite so the bug can't come back unnoticed. More in the best practices, the common workflows, the command list and checkpointing.

Common mistakes

FAQ

Does the /debug command debug my code?

No. /debug is a bundled skill that turns on the session's debug logging and analyzes that log to diagnose problems with Claude Code itself (a tool that fails, a setting that doesn't load). For a bug in your program, use the flow in this recipe.

What do I do if Claude changes the test so it passes?

Forbid it in the prompt ("don't modify or loosen the test"), ask it to show you the test in red before fixing and review the diff of the test file. If it keeps happening, add it to your CLAUDE.md as a project rule.

What if the bug is intermittent?

Say so and give the ratio ("fails 1 in 10"). Ask for hypotheses about concurrency, test order, cache or timing, and for a script that runs it in a loop (for example 50 times) to measure before and after the fix.

Can I give it a huge log?

Yes, but preferably with @path or piped with cat log | claude -p, asking it to filter. If it's very large, let a subagent read it and return only what matters, so the main context doesn't fill up.

Does /rewind undo everything Claude did while debugging?

Not everything. It undoes the edits made with its editing tools; files changed only by Bash commands and external changes aren't tracked. Commit before you start.

Do I have to approve every test run?

In Manual mode, yes. Add an allow rule for your test command or use auto mode, which approves routine actions and blocks risky ones (see the auto mode reference and the permissions recipe).

The whole library