Lesson 17 of 32
Debugging With Claude
Paste the real error, say where it probably lives, ask for a failing test before the fix, and refuse a suppressed symptom as a solution.
Debugging is where an agent is at its most useful, and the reason is structural. A bug has a definite right answer. Something in the system is behaving one way and should behave another, and there is almost always a command that tells you which of those two states you are in. That is exactly the shape of problem a loop of act-and-check is good at.
Most of the value in this lesson is in how you open. Debugging goes wrong at the first message far more often than it goes wrong later.
Two bug reports
Consider the same bug, described twice.
The first: "the login is broken, can you fix it".
The second: "Users report that login fails after their session times out. The
auth flow is in src/auth/, and I suspect token refresh. Here is the error from
the server log: [pasted]. Write a failing test that reproduces this first, then
fix it. Address the root cause rather than catching the error."
Both are one message. The second contains four things the first does not, and each one removes a way for the work to go wrong.
The symptom, in real terms
Not 'broken' but what happens, to whom, and when. 'Fails after session timeout' is a condition Claude can go and look for. 'Broken' is a mood.
The actual error output
Pasted whole, including the stack trace, plus the command that produced it. This is the highest-value thing in the message: it names files, lines and a call path, and it costs you a copy and a paste.
Where you think it lives
A directory, a module, a suspicion. Wrong guesses cost little because Claude will read and disagree. Absent guesses cost a search across the whole repository, paid for in context.
What counts as fixed
A failing test first, then the fix, then the test passing. This is the difference between 'I believe this is fixed' and something that returned a pass.
Two details worth adding when they apply. Say whether the failure is consistent or intermittent, because an intermittent bug changes the whole approach — a single passing run proves nothing about it. And say when it started, if you know, because "this worked before Tuesday" makes recent history a place to look rather than a thing to guess at.
A session
You typed: Uploads over about 10 MB fail intermittently in production, never locally. This is the error from the worker log — the full trace is in /tmp/upload-error.log. The upload path is src/uploads/. Write a failing test that reproduces it before you change anything, and find the root cause rather than catching the exception.
Claude ran: Read /tmp/upload-error.log
Claude ran: Read src/uploads/stream.ts
Claude ran: Read src/uploads/chunker.ts
Claude replied: The trace ends in chunker.ts at the point where it awaits the last chunk. The chunk size is read from an environment variable with a fallback of 5 MB, and the timeout is fixed at 2 seconds regardless of chunk size. On a slower connection a large final chunk exceeds it. That would be intermittent, and it would never happen locally.
WhyTwo facts you supplied did the heavy lifting here: intermittent, and never locally. Together they point at timing and environment, which is what turned a stack trace into a hypothesis.
Claude ran: Write src/uploads/chunker.test.ts
Claude ran: Run: npm test chunker
Output: FAIL chunker.test.ts ✕ retries a chunk that exceeds the write timeout Timeout of 2000ms exceededWhyThis failure is the good news. A test that fails for the reason you predicted is proof the bug is understood. Had it failed for some other reason, or passed, the diagnosis was wrong and nothing has been wasted yet.
Claude replied: Confirmed. Two options. I can scale the timeout with the chunk size, which is the narrow fix. Or the timeout can move into the config next to the chunk size, so the two cannot drift apart again. The second is a slightly larger change.
You typed: The second. They belong together.
Claude ran: Edit src/uploads/chunker.ts
Claude ran: Edit src/uploads/config.ts
Claude ran: Run: npm test uploads
Output: PASS chunker.test.ts (11 tests) PASS stream.test.ts (6 tests)WhyIt ran the whole upload suite rather than only the new test. That is the check that the fix did not break a neighbour, and it is worth asking for by name when it does not happen on its own.
Feeding it the evidence
Errors are text, and text has several routes in. Pasting is the obvious one and usually the right one — paste the whole trace, not the last line, because the lines above the message are the ones that say where it came from.
For anything longer, put the file where Claude can read it and say so, or pipe it
in directly: piping the contents of a log file into claude sends it as the
prompt. For anything visual — a broken layout, a stack trace in a browser
console, an error dialog — paste or drag the screenshot into the prompt. Claude
reads images, and a screenshot of a rendering bug carries more than a paragraph
describing it.
And name the command. "It fails when I run the integration suite" lets Claude reproduce the failure for itself, which is worth more than any description of it, because then the loop can close without you.
Root cause, not silence
This is the habit that separates debugging from making an error message go away.
Every bug has a cheap fix available that removes the symptom. Catch the exception and carry on. Add a null check and return early. Widen the type. Raise the timeout. Delete the assertion. Each of those makes the red text stop, and none of them are answers to the question of why the value was missing, why the type was wrong, or why it was slow.
Ask for the cause explicitly — "address the root cause, do not suppress the error" is a phrase worth keeping — and then read the diff with that in mind. When you see a guard appear, ask why the thing it guards against was happening. Sometimes the honest answer is that the input is legitimately optional and the guard is correct. Sometimes something upstream is broken and you have just moved the failure somewhere it will be harder to find.
When it goes round in circles
It will sometimes fix the wrong thing twice. There is a specific point at which you should stop correcting it, and it is earlier than instinct suggests.
If you have corrected the same misunderstanding twice, the conversation is now mostly wrong turns, and all of them are still in the context window influencing the next attempt. Clear the context and start again, with a prompt that includes what the failed attempts taught you: the cause it is not, the file that turned out to be irrelevant, the constraint you had not mentioned. A fresh session with a better-informed prompt reliably beats a long session dragging three dead hypotheses behind it.
The same instinct applies to scale. If the investigation needs a lot of reading — tracing something across dozens of files — ask for a subagent to do the exploring, so the findings come back to you without the four thousand lines behind them.
Two kinds of problem
One distinction to keep in mind, because it saves you debugging the wrong system.
Most of what goes wrong is a bug in your project, and everything above applies. Occasionally the problem is Claude Code itself: searches that come back empty when you know the files are there, garbled output in your terminal, memory climbing until things crawl. Those are not your code, and the fix is not a better prompt.
Claude Code has diagnostics for this. /doctor runs a checkup on your
installation, settings and extensions from inside a session, and proposes fixes
it can apply; claude doctor does it from your shell when Claude Code will not
start. If something started misbehaving after you added a plugin, a hook or an
MCP server, there is a safe mode that starts a session with all customisations
disabled, so you can find out whether one of them is responsible. There is a
whole lesson on this later, when you have configuration worth breaking.
What to take away
Debugging suits an agent because a bug has a right answer and usually a command that reveals it, and most of the outcome is decided by your first message. Give the real error output rather than a description of it, say what the symptom is in concrete terms, name where you think it lives, and say whether it is consistent or intermittent. Ask for a failing test before the fix, so that the diagnosis is proved and the repair has something to satisfy other than your judgement. Insist on a cause rather than a silenced symptom, and when a guard appears in the diff, ask why it is needed. If you have corrected the same misunderstanding twice, stop correcting and start again with what you now know.
Next: CLAUDE.md — writing down the things you have been repeating in every
prompt, so Claude reads them at the start of every session instead.
Check yourself
5 questions · pass 4/5 to unlock CLAUDE.md
1.What is the single most useful thing to include in a bug report to Claude?
2.Why ask for a failing test before the fix?
3.Claude reports the crash is fixed. Looking at the diff, it added a check that returns early when the value is missing. What should you ask?
4.You have corrected Claude three times on the same bug and it is still wrong. What is the best move?
5.Which of these is a problem with Claude Code itself rather than a bug in your project?
5 left to answer