"There's a bug in the report, can you fix it?" The agent reads some files, announces the cause with total confidence, patches three lines, and the symptom disappears. Two weeks later it is back, wearing a different shirt, because the patch suppressed the symptom without touching the cause, and now there are two things wrong: the original defect, and a wrong "fix" woven around it that any future diagnosis has to see through first. Nobody, at any point, could have stated what was actually broken. That is the failure mode this lesson exists to kill: fixes without diagnoses. Debugging is the skill that still separates seniors, and agents raise its value, because they amplify whichever you have more of, method or thrash.
The loop: symptom, hypothesis, cheapest discriminating test
Debugging is applied science, run as a loop:
- State the symptom precisely. Not "the report is wrong" but "Dana's streak reads 5; her events cover 3 days." The precision matters because a sloppy symptom statement admits too many hypotheses, and you will pay for each one.
- Reproduce it. Step zero, before any theory: a command you can run that shows the bug every time (or a documented rate, if it is intermittent). The docs' bug workflow starts exactly here, telling Claude the command to reproduce and the exact error. No reproduction means no way to know when you have fixed it, which by 3.2's rule means the fix is not even dispatchable.
- Generate hypotheses. Plural, always. A single hypothesis is a conclusion wearing a lab coat, and 1.5 taught you what happens when one confident belief anchors everything after it.
- Pick the cheapest test that discriminates between them. Not the test that would confirm your favorite; the test whose outcome splits the hypothesis space. Running the report for a single user, adding one log line, checking whether two objects are the same reference: seconds each, and each kills half the field.
- Repeat until the cause is stated, then fix the cause, and prove it: the reproduction from step 2 now passes, ideally captured as a permanent test (the failing-test-first pattern the docs recommend), and nothing else broke.
Two classic instruments deserve their own lines. Stack traces: read them as a crime scene, not a verdict. The top frame is where execution died, which is routinely not where the defect lives; walk down the frames to the boundary between your code and everything else, and treat the trace's line numbers as coordinates for questions ("what was undefined here, and who let it be undefined?"), not as the answer. Bisection: when the hypothesis space is a timeline or a pipeline, halve it. git bisect halves history ("which commit broke this?"); logical bisection halves the dataflow ("is the value already wrong when it leaves the loader? print it there"). Halving beats staring at any hypothesis space bigger than a few candidates.
The agent's seat, and yours
An agent is a genuinely excellent diagnostic partner in three specific roles: hypothesis breadth (it will name candidate causes you would not have, including whole classes you forgot exist), instrumentation speed (it can lace a module with targeted logging in seconds and strip it after), and log stamina (it reads five hundred lines of output without its attention flagging, which yours will). Use all three shamelessly.
What you do not hand over is the loop itself: which hypothesis to test next, what a result rules out, when the cause is actually established. The mechanics from 1.5 explain each boundary. Announce your pet theory first and sycophancy means the agent adopts it against evidence (exhibit C was precisely a debugging session). Let failed fix attempts pile up in the session and context poisoning means iteration 20 reasons from the debris of iterations 3 through 19; clear and restate clean instead, carrying forward only the facts you have established. And the headline failure, letting the agent fix what neither of you understands: a symptom-suppressing patch is indistinguishable from a real fix at "the error went away" resolution. The question that separates them costs one sentence: "state the cause before proposing the fix." If the cause cannot be stated, you do not have a fix, you have a coincidence with a commit message. This is 3.2's doctrine reappearing: "the symptom stopped" is not a verifiable definition of done; "the stated cause is corrected and the reproduction passes" is.
The report script prints wrong streaks: reproduce with node report.js,
Dana's streak reads 5, her events in events.json cover 3 days.
Do NOT edit any file yet. Work the loop with me:
- List 3 to 5 candidate causes, each with the observation that would confirm or kill it.
- For the most likely two, propose the cheapest discriminating test (a command, a single log line, a one-user run). Wait for my go-ahead before running anything.
- After each result, restate: ruled out, still standing.
We fix only after we can both state the cause in one sentence.
That prompt structure is doing deliberate work: the agent brings breadth and speed, the hypothesis gating stays with you, and no edit happens before a stated cause. Notice also what it does not do: it does not say "I think it's the streak function", because the fastest way to waste an agent's breadth is to hand it your anchor first. Ask for the hypothesis list before you reveal your own suspicion, then compare.
Where it breaks
The confident wrong diagnosis. Agents state hypotheses in the same register as facts. "The issue is that the tally is shared across users" sounds identical whether it is verified or the first plausible guess. Your defense is the discipline above: every causal claim gets asked "what did we run that establishes that?", and a claim with no observation behind it is a hypothesis, whatever its tone.
Fix-thrashing. Symptom, patch, new symptom, patch, and twenty minutes later the module is a quilt of reverted half-fixes and the original state is unrecoverable. The loop's structure prevents this (no edits during diagnosis), and git makes it cheap to enforce: diagnosis happens on a clean tree, and any instrumentation gets stripped or stashed before the real fix lands as one coherent diff.
Intermittence used as an excuse. "Can't reproduce it" too often means "did not try to characterize it". An intermittent bug still has structure: under what load, which inputs, what time of day, does it survive a restart? Characterizing the conditions IS the reproduction work, and agents are good at building the harness that hammers a flaky path five hundred times to turn "sometimes" into a rate you can watch move.
The bug in the harness. Sometimes the defect is in your test, your fixture, or your reproduction script, and the code is innocent. If evidence keeps contradicting itself, add "my instrument is lying" to the hypothesis list. It discriminates cheaply: run the instrument against a case you know by hand.
Lablab-3-5Two staged bugs, two chairs
Goal: Fix a crash by driving the diagnosis yourself with the agent assisting, then fix a wrong-output bug with the agent driving and you gating every hypothesis.
Prereqs: Node 18+, the practice repo (make a debug-lab/ directory in it). Create these three files exactly as shown. Both bugs are real and were reproduced from these exact contents before this lesson shipped; the expected outputs below are from those runs.
[
{ "user": "dana", "ts": "2026-06-29T14:02:11Z", "kind": "commit" },
{ "user": "dana", "ts": "2026-06-30T09:15:40Z", "kind": "review" },
{ "user": "dana", "ts": "2026-07-01T16:44:03Z", "kind": "commit" },
{ "user": "priya", "ts": "2026-06-29T11:20:55Z", "kind": "commit" },
{ "user": "priya", "ts": "2026-07-02T10:05:19Z", "kind": "commit" },
{ "user": "priya", "ts": "2026-07-03T13:37:00Z", "kind": "review" },
{ "user": "tom", "ts": "2026-07-01T08:12:30Z", "kind": "commit" },
{ "user": "tom", "kind": "deploy" },
{ "user": "tom", "ts": "2026-07-03T17:59:59Z", "kind": "commit" }
]
// Per-user activity tallies for the standup report.
const EMPTY_TALLY = { days: {}, total: 0 };
// Fresh tally for a user who has no events recorded yet.
function newTally() {
return EMPTY_TALLY;
}
// Record one event on a tally under its YYYY-MM-DD day key.
function record(tally, day) {
tally.days[day] = (tally.days[day] || 0) + 1;
tally.total += 1;
}
// Longest run of consecutive active days in the tally.
function longestStreak(tally) {
const days = Object.keys(tally.days).sort();
let best = days.length > 0 ? 1 : 0;
let run = 1;
for (let i = 1; i < days.length; i++) {
const gap = Date.parse(days[i]) - Date.parse(days[i - 1]);
if (gap === 86400000) {
run += 1;
if (run > best) best = run;
} else {
run = 1;
}
}
return best;
}
module.exports = { newTally, record, longestStreak };
// Weekly standup report: active days and longest streak per user.
// Usage: node report.js [user]
const fs = require("fs");
const path = require("path");
const { newTally, record, longestStreak } = require("./tally");
function dayOf(event) {
return event.ts.slice(0, 10);
}
function buildTallies(events) {
const tallies = {};
for (const event of events) {
if (!tallies[event.user]) {
tallies[event.user] = newTally();
}
record(tallies[event.user], dayOf(event));
}
return tallies;
}
function main() {
const raw = fs.readFileSync(path.join(__dirname, "events.json"), "utf8");
let events = JSON.parse(raw);
const onlyUser = process.argv[2];
if (onlyUser) {
events = events.filter((e) => e.user === onlyUser);
}
const tallies = buildTallies(events);
for (const [user, tally] of Object.entries(tallies)) {
const activeDays = Object.keys(tally.days).length;
console.log(
user + ": " + tally.total + " events, " + activeDays +
" active days, longest streak " + longestStreak(tally)
);
}
}
main();
Bug A: you drive, agent assists.
- Run
node debug-lab/report.js. It crashes:TypeError: Cannot read properties of undefined (reading 'slice')atdayOf(report.js line 9), withbuildTalliesandmainbelow it in the trace. - Read the trace yourself before opening a session. The top frame says where it died. Form the question the trace poses (what was undefined, and which input produced it?) and answer it with one cheap test of your own choosing (a log line, or inspecting events.json).
- Use the agent for the parts it is good at: once YOU have named the offending record, ask for the fix options (validate and skip with a warning? fail loudly on malformed input? default the field?) and make the call yourself. Decide what the right behavior is for a reporting tool fed dirty data, implement, and re-run.
Bug B: agent drives, you gate.
- With bug A fixed, run
node debug-lab/report.jsagain. It completes, and the numbers are wrong: every user shows the same inflated figures (with the data above: 8 events, 5 active days, streak 5, for all three users, when Dana should be 3/3/3, Priya 3 events over 3 days with streak 2, Tom 2/2/1). - Open a session with the dispatch-a-diagnosis PromptCard above, adapted to this symptom. The agent proposes hypotheses and tests; you approve or veto each test on one criterion: does its outcome discriminate? ("Run it for a single user" is a good early one:
node debug-lab/report.js danaprints correct numbers, which rules out the math and implicates aggregation across users.) - Do not accept any edit until the agent states the cause in one sentence and you agree the evidence establishes it. Then let it fix, re-run both the full report and the single-user runs, and compare against the correct outputs above.
- Close out: have the reproduction become a permanent artifact (a tiny test asserting Dana's line from this fixture), commit, log the session's shape in STATUS.md.
Verify
- Bug A:
node debug-lab/report.jsno longer crashes, and you can state which record caused the crash and why your chosen handling (skip, fail, default) is right for this tool. - Bug B: the full report prints dana 3 events / 3 active days / streak 3, priya 3 / 3 / 2, tom 2 / 2 / 1, and single-user runs agree with the full run.
- You can state bug B's cause in one sentence, and it names the mechanism (what was shared that should have been fresh), not just the fix.
- The fixture test from step 7 exists and passes, and it fails if you temporarily reintroduce the bug (revert the fix, run, watch it go red, restore).
>Troubleshooting
- Bug A does not crash for you: check events.json survived copy-paste intact; the malformed record (tom's
deployevent with nots) is the trigger, and a "helpful" editor or formatter may have dropped it. - The agent proposes rewriting both files wholesale at step 5: that is a fix without a diagnosis, exactly the failure this lesson names. Veto it, restate the ground rule (no edits before a stated cause), and if the session has accumulated fix-attempt debris, clear and restart from the facts you have established.
- Bug B "disappears" when you add a console.log of the tallies object: you are logging a live reference that the console displays after further mutation, or your log changed evaluation order. Log
JSON.stringify(tallies)for a point-in-time snapshot, and add "my instrument is lying" to the hypothesis list; it is on the list for a reason. - The agent fixates on
longestStreakdate math (UTC parsing, the 86400000 constant): a fair hypothesis, and the single-user run kills it in five seconds, because the same math prints correct streaks when one user's data flows through. This is what a discriminating test is for; make the agent run it before it dives into date arcana.
Knowledge check
Knowledge check
Sources
- Common workflows (fix bugs efficiently: share the exact error and reproduction command, note intermittence; resume sessions; delegate research): https://code.claude.com/docs/en/common-workflows (fetched July 2026)
- Best practices for Claude Code (describe the symptom with likely location and what fixed looks like, write a failing test that reproduces the issue then fix it, course-correct early, show evidence over assertions): https://code.claude.com/docs/en/best-practices (fetched July 2026)
- Both staged bugs were reproduced from the exact file contents above (crash output, inflated shared-tally output, single-user discriminating runs, and post-fix outputs) before this lesson shipped, July 2026.