~/skills/sleuth

/sleuth

Debugging

A Claude Code skill for systematic, evidence-first bug diagnosis — five structured phases that prevent confidently-wrong root-cause guesses.

Claude Code

The problem

When something breaks, an AI agent does what you’d expect: it pattern-matches the symptom to the most familiar cause and proposes a fix. And the fix is often well-reasoned, even plausible. But it has a structural flaw — it was produced by an agent that pattern-matched instead of investigated.

The training data for any large model is full of bugs with known causes. That makes the model extremely fluent at recognizing the shape of a bug and producing a confident, well-articulated explanation for it. That fluency is the trap: the explanation arrives before the evidence, and confidence fills the gap where investigation should have been.

The result isn’t usually a random wrong guess. It’s a wrong guess that looks right — one that fits the symptoms, uses the right vocabulary, and survives casual scrutiny. You implement the fix. The tests pass. The same bug surfaces three days later under slightly different conditions, and now you have two problems: the original one and a patch built on a false premise.

sleuth intercepts this. When invoked, it refuses to hypothesize until it has reproduced the failure, verified it’s working with the right code, and read the actual failure point. Candidates compete in a tournament — eliminated by the cheapest test that splits them, not by ranked likelihood. A fix only ships after a test fails for the predicted reason, and only after an explicit go-ahead with the full diagnostic picture on the table.

How the phases work

Phase 1 (Reproduce) classifies the failure into one of three tiers. Tier A: the bug reproduces on demand from a command. Tier B: a deterministic artifact — stack trace, log, error output — makes the failure visible. Tier C: fingerprint only; the bug can’t be triggered or observed in this session. The tier propagates forward: a Tier C bug caps every downstream phase at “hypothesis, never confirmed,” and the fix ships with that caveat explicit.

The preflight gate runs before any hypothesis. Two mandatory checks: confirm the code being reasoned about is the code actually running (git status, build freshness, correct branch and config), and read and quote the entry point where the symptom manifests. No hypothesis until both are done. If the preflight finds a stale build or uncommitted changes, it re-runs the repro and stops if the symptom vanishes — because that was the bug.

Phase 2 (Localize) traces backward from the symptom to a specific location — a function, a line, a boundary. Direction matters: start at the observed failure and trace toward the cause, never forward from a guessed bug class. The phase exits only when a named location is reached and a falsifiable claim is made about it — what observation there would prove it’s the wrong location.

Phase 3 (Diagnose) opens with a breadth pass across different layers before narrowing: the call site, the data feeding it, the environment, dependencies, the caller’s contract, and whether the symptom is actually correct behavior. Then a tournament: find the cheapest test that splits the candidate set, run it, eliminate losers, repeat. If three rounds leave more than two live candidates with no clean splitter, the skill escalates to the user rather than thrashing. If no candidate survives to a confirmable mechanism, it declares diagnostic failure and stops — an honest “here’s what I ruled out and what to try next” is the correct output, not a speculative fix.

Phase 4 (Fix) authors a test that fails for the predicted reason before touching production code. If the test doesn’t fail the way the diagnosis predicted, the diagnosis is wrong — back to Diagnose. Once the test fails correctly, the skill halts and presents the full picture for approval: the causal mechanism, the failing test output, the proposed diff, and an explicit caveat of what was examined and what was not. The fix applies only on explicit go-ahead, and only within the debug scope.

Phase 5 (Verify) requires the new test to pass and the pre-existing suite to still pass. A fix that resolves the symptom but breaks something else is not done. For intermittent bugs, Verify is statistical — enough runs to establish confidence, with the count stated explicitly.

the skill

Save as ~/.claude/skills/sleuth.md and invoke with /sleuth at the start of any relevant task.

sleuth.md
---
name: sleuth
description: Systematic bug diagnosis for code under active development. Walks a five-phase, evidence-first process — reproduce, localize, diagnose, fix, verify — built to prevent confidently-wrong root-cause guesses. Use this whenever the user is troubleshooting a software bug: when they say something is "broken," "failing," "not working," "throwing an error," or "behaving weirdly"; when they paste a stack trace or error output; when they ask "why is this happening" or "why does this break"; or any time the task is to find and fix the cause of unexpected software behavior — even if they never say the word "debug." Prefer this over ad-hoc debugging whenever the cause isn't immediately and verifiably obvious.
---

# Sleuth

You are diagnosing a software bug. Your single most dangerous failure mode is **confidently committing to a plausible-but-wrong root cause** and marching the user down it. Everything here exists to make that failure structurally hard.

The core discipline: **work from evidence, not from what a bug like this is "usually" caused by.** Your training makes you fluent at pattern-matching symptoms to familiar causes. That fluency is exactly the trap — it produces fast, confident, well-articulated wrong answers. Slow down and let observations, not priors, drive every step.

## Operating principles

These hold across all five phases.

- **Falsifiability.** You may not leave a phase until you can state what observation would prove its conclusion *wrong*. A real prediction names a specific, bug-specific signature — not "if I'm right, it'll work." "The test passes" is never a prediction. "The `user_id` will be `null` at line 40 because the session deserializer drops it" is.
- **Symptom-up.** In Localize and Diagnose, start at the observed failure and trace *backward* toward the cause. Do not start from "what class of bug is this?" and work forward — that's the pattern-matching trap.
- **No exit ramps.** Even an obvious-looking bug runs the full loop. You may *compress* — move fast through gates when evidence is clear — but you may not *skip* a gate. The moment a bug looks trivially obvious is the moment your guess is most likely to be a confident pattern-match. Compression is the speed valve; skipping is not allowed.
- **Evidence over reasoning.** Prefer a cheap measurement (run it, print it, diff it, bisect it) over an argument about what the code does. When you can look instead of reason, look.

## Maintain a status block

Keep a compact running log, updated as you move, and show it to the user. It is both your defense against losing track on long bugs and the user's interface for steering you.

```
SLEUTH STATUS
Phase: <Reproduce | Localize | Diagnose | Fix | Verify>
Repro tier: <A | B | C | →>
Candidates:
  - <candidate>  [live | test-eliminated | user-eliminated, untested]
Current prediction: <the falsifiable thing you're about to check>
```

Keep it one or two lines for a trivial bug; expand it for a hard one. **The user may prune or redirect at any time** ("it's not the cache, skip it" / "stop looking client-side"). Honor it immediately — but record a user-pruned candidate as `user-eliminated, untested`, distinct from one you eliminated with a test. If the search later stalls, **resurface those untested prunes**: "you ruled out the cache without a test — want to verify it now?"

**Escalate the log to a file** (`.sleuth/<short-bug-name>.md`, gitignored) when the investigation backtracks to an earlier phase or runs more than ~3 elimination rounds. That's when context gets long and an in-chat log starts to rot. Re-read the file rather than trusting memory.

---

## Phase 1 — Reproduce

Get a deterministic observation of the failure. Do **not** write the repro test here — that comes in Fix, once you know what to assert.

Classify what you achieved into a tier and record it; the tier governs how much you trust later phases. Do not *choose* a tier — report the strongest one the bug actually allows, and note what you attempted.

- **Tier A** — the bug reproduces by running a script/command under your control.
- **Tier B** — the failure is visible in a deterministic artifact captured from outside you: a saved stack trace, log excerpt, error output, recording.
- **Tier C** — fingerprint only: exact symptom, environment, version, and repro steps documented, but you cannot trigger or observe it in this session.

An intermittent (flaky) bug caps at **Tier B** — you have artifacts from the failing runs, but not on-demand reproduction.

**Exit criterion:** you have a tiered observation *and* a falsifier — what symptom-change would mean your reproduction is actually of a different problem.

**Tier propagation:** Tier A — diagnose normally, Verify re-runs the script. Tier B — stay careful, Verify must explain why the artifact no longer shows the failure. Tier C — Diagnose is capped at "hypothesis," never "confirmed"; Verify requires user confirmation; flag openly that any fix is speculative.

---

## Preflight gate (between Reproduce and Localize)

Before any hypothesis, two required checks. These catch the bugs that aren't really in the code and the hypotheses that aren't really grounded.

1. **Check the plug.** Confirm the code you're about to reason about is the code that's actually running. Run the checks and **put the real command output in the log** — do not self-report "I checked." Cover at least: source matches what's running (`git status`, recent diffs), build is current (timestamps / rebuild), correct branch, correct env/config, clean cache. Generate the stack-specific checks from these seeds.
   - If a check fails (e.g., stale build, uncommitted changes): correct it, **re-run the reproduction**, and continue only if the symptom persists. If it vanishes, that was the bug — you're done.
2. **Read before you hypothesize.** Read and **quote in the log** the entry point where the symptom manifests. You may not name a hypothesis until you've looked at the actual code at the failure point. If the symptom-up trace later shows you read the wrong place, record it as a false start and re-read.

---

## Phase 2 — Localize

Working backward from the symptom, narrow to a specific location — a function, a line, a boundary between components.

**Exit criterion:** a named location, *arrived at by tracing the symptom backward* (not by guessing the bug class), plus a falsifiable claim — what observation at that location would prove it's the *wrong* location.

---

## Phase 3 — Diagnose

This is where false confidence is most dangerous, so it has the most structure.

**First, a breadth pass.** Before narrowing, enumerate candidate origins across *different layers* — not just the one you instinctively suspect:
- the call site / local logic
- the data or input feeding it
- the environment / configuration
- a dependency or external service
- the caller's contract or assumptions
- **the possibility that the symptom is actually correct behavior** and the bug is in the expectation

The breadth pass is real only if it sometimes yields a candidate from a *different layer* than your first instinct. If every breadth pass concludes "the obvious place is the only place," you are rubber-stamping, not searching — force at least one cross-layer candidate and say why it's plausible or not.

**Then run the tournament.** Don't rank candidates by guessed likelihood — that hands the work to your weakest skill. Instead:
1. Find the **cheapest test that splits the candidate set** — one that eliminates one or more candidates regardless of which is true.
2. Run it. Eliminate the losers. Update the log.
3. Repeat on what survives.

If after ~3 rounds you still have more than two live candidates and no test eliminates more than one at a time, **stop and escalate to the user** with the live set rather than thrashing.

**Before declaring a winner, check completeness:** "what else could produce this *exact* symptom — is the set complete?" Then the winning diagnosis must be a **causal mechanism that predicts the observed symptom**, with a falsifier.

**Carry a search-breadth caveat forward:** record what you examined and what you did *not*. This is not bookkeeping — it's the thing that lets the user catch a narrow search at the Fix gate.

**If no candidate survives** to a confirmable mechanism: declare **diagnostic failure**. Output the eliminated candidates, the residual uncertainty, and a recommended next human action. Do **not** apply a speculative fix. An honest "I don't know yet, here's what I ruled out and what to try" is a success, not a failure — it's the whole point of this skill.

---

## Phase 4 — Fix

1. **Author a test that confirms the diagnosis** — one that fails *for the predicted reason*. Run it and confirm it fails with the predicted signature. This is the final, empirical confirmation of the diagnosis; if the test doesn't fail the way you predicted, the diagnosis is wrong — return to Diagnose.
2. **Halt and present** for approval — cheap to approve, loud when it matters:
   - the diagnosis (the causal mechanism)
   - the failing test and its output
   - the proposed diff
   - the **examined / did-not-examine caveat** from Diagnose
3. **Apply only on explicit go-ahead.** Keep edits within the debug scope (files touched during Localize + the test file); flag any edit outside it.

If the user declines the fix, leave the failing test in place — it documents the confirmed bug.

The caveat in step 2 is doing real work: it's the user's cue to extend a too-narrow search ("you never looked at the serialization layer — check there"). Make it specific, not a formality.

---

## Phase 5 — Verify

**Exit criterion:** the new test passes **and** the pre-existing suite still passes — a fix that resolves the symptom but breaks something else is not done. For an intermittent bug, Verify is statistical: run it enough times to be confident the failure is gone, and say how many.

If you didn't actually observe it pass, it isn't fixed.

---

## A note on speed

This process is meant to *feel* fast on easy bugs — a Tier-A bug with one obvious candidate blows through every gate in seconds, because each gate's evidence is immediately available. The structure costs you nothing when the bug is genuinely simple. It only slows you down precisely when slowing down is warranted: when the evidence isn't there yet. Don't shortcut the gates to feel faster; let clear evidence carry you through them quickly.