/diagnose — the feedback loop is the skill
A discipline for hard bugs. Skip a phase only with an explicit reason.
Before exploring, read the project CLAUDE.md and any CONTEXT.md for the module vocabulary, and check ADRs in the area you're touching.
Phase 1 — Build a feedback loop
This is the skill. Everything else is mechanical. With a tight pass/fail signal that goes red on this bug, you will find the cause — bisection, hypothesis-testing, and instrumentation all just consume it. Without one, no amount of staring at code saves you.
Spend disproportionate effort here. Be aggressive, be creative, refuse to give up.
Ways to construct one — roughly in this order
- Failing test at whatever seam reaches the bug — unit, integration, e2e.
- Curl / HTTP script against a running dev server.
- CLI invocation with a fixture input, diffing stdout against a known-good snapshot.
- Headless browser script (Playwright MCP or a script) — drives the UI, asserts on DOM/console/network.
- Replay a captured trace. Save a real request / payload / event log to disk; replay it through the code path in isolation.
- Throwaway harness. A minimal subset of the system (one service, mocked deps) exercising the bug path in a single function call.
- Property / fuzz loop. For "sometimes wrong output": 1000 random inputs, look for the failure mode.
- Bisection harness. If it appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so
git bisect runcan eat it. - Differential loop. Same input through old vs new version (or two configs), diff the outputs.
- HITL bash script. Last resort — if a human must click, drive them with
scripts/hitl-loop.template.shso the loop stays structured and its output feeds back to you.
Servers and log streams run in the background (run_in_background: true) so the loop and the work proceed together.
Tighten the loop
Treat the loop as a product. Once you have a loop, tighten it:
- Faster? Cache setup, skip unrelated init, narrow the test scope.
- Sharper signal? Assert the specific symptom, not "didn't crash".
- More deterministic? Pin time, seed RNG, isolate filesystem, freeze network.
A 30-second flaky loop is barely better than none; a 2-second deterministic one is a superpower.
Non-deterministic bugs
The goal is not a clean repro but a higher reproduction rate. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not — keep raising the rate until it is.
Completion criterion — a tight loop that goes red
Phase 1 is done when you can name one command — a script path, a test invocation, a curl — that you have already run at least once (paste the invocation and its output), and that is:
- Red-capable — drives the actual bug path and asserts the user's exact symptom, so it can go red now and green once fixed. Not "runs without erroring".
- Deterministic — same verdict every run (flaky bugs: a pinned, high reproduction rate).
- Fast — seconds, not minutes.
- Agent-runnable — runs unattended; a human enters only via
scripts/hitl-loop.template.sh.
If you catch yourself reading code to build a theory before this command exists, stop — jumping straight to a hypothesis is the exact failure this skill prevents. No red-capable command, no Phase 2.
When you genuinely cannot build a loop
Say so explicitly and list what you tried. Ask for one of: access to an environment that reproduces it, a captured artifact (HAR, log dump, core dump, screen recording with timestamps), or permission to add temporary production instrumentation. Do not proceed to hypothesise without a loop.
Phase 2 — Reproduce + minimise
Run the loop. Watch it go red.
- The failure is the one the user described — not a different failure that happens to live nearby. Wrong bug = wrong fix.
- It reproduces across runs (or at a high enough rate to debug against).
- The exact symptom is captured (error message, wrong output, timing) so later phases can verify the fix addresses it.
Then minimise: shrink to the smallest scenario that still goes red. Cut inputs, callers, config, data, and steps one at a time, re-running after each cut. Done when every remaining element is load-bearing — removing any one makes it go green.
A minimal repro shrinks the hypothesis space in Phase 3 and becomes the clean regression test in Phase 5. Do not proceed until you have reproduced and minimised.
Phase 3 — Hypothesise
Generate 3–5 ranked hypotheses before testing any of them — single-hypothesis generation anchors on the first plausible idea.
Each must be falsifiable, stating its prediction:
"If X is the cause, then changing Y makes the bug disappear / changing Z makes it worse."
Can't state the prediction? It's a vibe — discard or sharpen it.
Show the ranked list to the user before testing. They re-rank instantly with domain knowledge ("we just deployed a change to #3") or name what they've already ruled out. Cheap checkpoint, big saving. Don't block on it — proceed with your ranking if they're AFK.
Phase 4 — Instrument
Each probe maps to a specific prediction from Phase 3. Change one variable at a time.
- Debugger / REPL if the env supports it. One breakpoint beats ten logs.
- Targeted logs at the boundaries that distinguish hypotheses.
- Never "log everything and grep".
Tag every debug log with a unique prefix — [DEBUG-a4f2]. Cleanup becomes a single grep; untagged logs live forever.
Perf branch. For performance regressions logs are usually wrong. Establish a baseline measurement (timing harness, performance.now(), profiler, query plan), then bisect. Measure first, fix second.
Phase 5 — Fix + regression test
Write the regression test before the fix — but only if a correct seam exists: one where the test exercises the real bug pattern as it occurs at the call site. A seam too shallow (single-caller test when the bug needs multiple callers) gives false confidence.
If no correct seam exists, that itself is the finding. Note it — the architecture is preventing the bug from being locked down.
With a correct seam: turn the minimised repro into a failing test there → watch it fail → apply the fix → watch it pass → re-run the Phase 1 loop against the original un-minimised scenario.
Test shape and seam agreement: ${CLAUDE_PLUGIN_ROOT}/skills/build/references/tdd-seams.md.
Phase 6 — Cleanup + post-mortem
- Original repro no longer reproduces (re-run the Phase 1 loop)
- Regression test passes, or the absence of a seam is documented
- All
[DEBUG-...]instrumentation removed (grep the prefix) - Throwaway harnesses deleted or moved somewhere clearly marked
- The hypothesis that turned out correct is stated in the commit message — so the next debugger learns
Then ask: what would have prevented this bug? If the answer is architectural (no good test seam, tangled callers, hidden coupling), hand off with specifics — ${CLAUDE_PLUGIN_ROOT}/skills/review/references/codebase-design.md has the vocabulary. Make that recommendation after the fix lands, when you know more than you did at the start.
Gotchas
- A green loop is not a fixed bug. If the loop never went red before the fix, it proves nothing — it may not reach the bug path at all.
- Minimising after hypothesising wastes the minimisation. The point is to shrink the hypothesis space before generating hypotheses.
- Untagged debug logs ship. The
[DEBUG-xxxx]prefix makes cleanup mechanical instead of a memory test. - HITL is a loop, not a chat. Driving the user through
scripts/hitl-loop.template.shkeeps their answers structured and parseable; asking freeform questions loses the loop.
Don't
- Read code to form a theory before a red-capable command exists — that's the failure this skill prevents.
- Fix a nearby failure the loop happens to catch instead of the symptom the user reported.
Adapted from diagnosing-bugs by Matt Pocock (MIT, Copyright (c) 2026 Matt Pocock). See THIRD-PARTY.md.