CommunityCoding & Developmentgithub.com

adamstallard/bug-hunter

Catch bugs in code that was just written, before it is committed. Run this before every `git commit` that contains code you or the user just wrote — "commit this", "commit my changes", "let's commit", "wrap this up and commit" all mean run this first. Also use when the user says "bug check this", "prevent bugs", or "find bugs before I commit". If another skill makes your commits and runs this one before each of them, let it. Finds candidate bugs in the staged change, proves each one with a regression test that actually fails, refutes the weak ones, fixes what survives, and re-verifies. Docs, specs, tests and work-in-progress commits skip the hunt, not this skill — they still commit through its script, carrying a `Bug-hunter: skipped` trailer. When a caller's brief says "caller commits", run in full but do not commit: leave the change staged and return the trailer lines and any open decisions for the caller to use. Do not use for reviewing someone else's PR or reviewer comments — use a code review for that.

What is bug-hunter?

bug-hunter is a Claude Code agent skill that catch bugs in code that was just written, before it is committed. Run this before every `git commit` that contains code you or the user just wrote — "commit this", "commit my changes", "let's commit", "wrap this up and commit" all mean run this first. Also use when the user says "bug check this", "prevent bugs", or "find bugs before I commit". If another skill makes your commits and runs this one before each of them, let it. Finds candidate bugs in the staged change, proves each one with a regression test that actually fails, refutes the weak ones, fixes what survives, and re-verifies. Docs, specs, tests and work-in-progress commits skip the hunt, not this skill — they still commit through its script, carrying a `Bug-hunter: skipped` trailer. When a caller's brief says "caller commits", run in full but do not commit: leave the change staged and return the trailer lines and any open decisions for the caller to use. Do not use for reviewing someone else's PR or reviewer comments — use a code review for that.

Works with✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/adamstallard/shared-skills/tree/HEAD/.agents/skills/bug-hunter

Installed? Explore more Coding & Development skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Bug Hunter

A claim with no failing test is a guess. Everything this skill reports is proven by a test observed red before the fix and green after; everything it cannot prove is dropped. That is the whole idea, and the reason its output is worth reading.

It edits the working tree, adds test files, and by default commits.

  • How to do each step — reference.md. Read it before Steps 4–7, before writing a report, and before committing.
  • Mutation testing, Step 3 to Step 9 — mutation.md. Read it before starting any mutant.

The contract

  1. Every fixed bug has a regression test that was observed red on the unfixed code and green after the fix.
  2. Every candidate that cannot be driven red, or that a second opinion refutes, is dropped — and its test is deleted, never left failing.
  3. Anything real but out of scope or untestable is reported, not fixed.
  4. The full test suite ends no worse than it started.

Workflow checklist

- [ ] Step 0: Scope the change — staged diff only
- [ ] Step 1: Triage — is this commit worth reviewing at all?
- [ ] Step 2: Baseline — run the suite before touching anything
- [ ] Step 3: Find candidates (strong model, reachability required); mutation subagents beside it where the changed functions have tests
- [ ] Step 4: Prove — write the test, run it, demand a matching red
- [ ] Step 5: Refute — a second opinion tries to kill each proven bug, and checks each survivor test and dead-code claim
- [ ] Step 6: Fix (prevention question, scope-confined)
- [ ] Step 7: Verify — new tests green; the full suite once, before Step 9
- [ ] Step 8: Iterate — back to back, up to 3, without asking; never a 4th
- [ ] Step 9: Report once at the end; ask the user each open decision (picker — written is not asked); then commit (with prose's trailer when prose is installed) or leave it for review — or, under "caller commits", end with `scripts/caller-result.sh`'s block

Never run this from inside a git pre-commit hook. It writes files, adds tests, and commits — all of which fight git's index lock. It runs before the commit is created, and the commit happens at Step 9.

A companion hook watches for commits that never came through here: after each command it asks git whether HEAD moved and whether the new commit carries a Bug-hunter: trailer. So the trailer is the only marker that matters — no environment variable, nothing to remember, and the record lives in the commit itself where git log can audit it.

The hook also asks a second question of any trailer that is not itself a skip note — 0 bugs found and aborted ... included, not only ones reporting a fix: does a Bug-hunter-Tree trailer match the tree that actually landed? That binding is what scripts/mint-trailer.sh produces when run against the real staged tree, right before the commit — see Step 9 for how. A Bug-hunter: trailer can be a real git trailer with exactly the right wording and still be a sentence recalled from having read this file, rather than one produced by running any part of it — that is the one shape the string-presence check cannot catch, and the one this binding is for.

A commit that lands with no Bug-hunter: trailer, or with one whose Bug-hunter-Tree binding does not match, gets named in a report, with the repository it is in. The report sends you to the section below and gives no command to fix the commit: the hook cannot see where you are standing, and the commit it names is often not your HEAD.


If you are an agent about to commit

If another skill makes your commits and runs this skill before each of them, let that skill start from "commit this"; still run for "bug check this". If this turn already stated a commit subject, use that subject unchanged. Only add trailers.

Every commit you create goes through this skill and carries a Bug-hunter: trailer. Not when asked for it by name — every time. A commit with code gets the hunt. A commit with only docs, specs, tests or formatting skips the hunt, not the skill. "Commit this", "commit my changes", "wrap up and commit", or your own decision to commit after finishing a task all mean: stage the change, run this skill, then commit. Triage (Step 1) is what decides whether a given commit is worth reviewing, so running it always is cheap when it does not apply.

Run it yourself, in your own session. The agent that wrote the change runs this skill. Do not hand the whole run to a subagent that then starts finders, refuters and mutation agents of its own: the run needs an orchestrator that stays alive to collect survivors and resume agents, and a nested delegate keeps ending its turn while its children run. Delegating the single passes, as the steps say, is how it is meant to run.

Do not evade this skill

One heading, not five rules, because an agent looking for a way out of a check reliably finds the seam between separately-stated rules. Every line here was added after an agent got around the rule it closes.

1. Say which one you are doing, out loud, every time. One line in the reply the user can see: "running bug-hunter on N files", or "skipping bug-hunter — ". Not in your reasoning — in the reply. Skipping while saying nothing looks exactly like running it and finding nothing. A stated skip can be overruled in four words; an unstated one is a decision you made for them. If you skip, the commit carries the trailer too — commit-with-trailer.sh -F msg.txt "skipped — <reason>", as in Step 9. Both, not either. Under caller commits, the skip goes to the result script instead (When the caller commits).

2. Never ask permission to run it. The trigger is a property of the code, not a moment: if code has been written since the last pass that came back clean, a pass is owed. Before a commit, between iterations, after a fix the user asked for, after a refactor, after a commit that already landed — not separate cases, all "code was written". A list of moments invites hunting for the moment that is not on it.

3. Announcing only counts if you have already started. "That's running next unless you want to stop here" · "I'll kick that off if you're happy with it" · "the fixes need their own pass — want me to run it?" Each ends the turn with the check not running, which hands back the decision as plainly as asking would. If you are describing a run in the future tense, you have not started it, and that is the bug.

4. Iterations run back to back, and the report is owed. Up to three, no check-in between them, and no interim report — a report between iterations invites stopping early and makes an unfinished run look finished. See Steps 8 and 9.

Three is the budget, not an opening offer. There is no fourth iteration. "want me to run one more pass?" · "iteration 3's own fixes never got reviewed — shall I do a 4th?" Both hand back a decision the budget already made, at the moment the user is most likely to say no. A run that caps out ends in the root-causes report (Step 9) and one choice per cause, where the first choice is a new run at iteration 1 scoped to that cause. That is not rule 2 suspended: a pass over iteration 3's fixes really is owed, and it is still owed — what ran out is the budget for taking it inside this run, which is why it becomes a new run and why which cause gets it is the user's call. Nor is it rule 3 reversed: inside the budget a pass is owed and you start it without asking; at the cap the run is over, and what is left is a decision, offered as options rather than as an open question.

5. These are not reasons. Every one has been used, by a capable model, to skip this skill on a commit that then shipped a bug:

  • the change is small — one-line changes are wrong all the time
  • I am confident in this code — so was every author of every bug this has found
  • the user is waiting / we are moving fast — that is the moment quality gets traded away, which is the moment this exists for
  • the session is long and this is expensive — a run that finds nothing is cheap, triage stops most commits before the expensive part, and the long passes run in the background anyway (Step 3)
  • I already ran it on related code earlier — you did not run it on this
  • this is the skill's own machinery, not real code — it is real code, and the worst bugs in this skill's own history have been in exactly that machinery
  • the user is clearly watching and would object if they minded — they are watching the output, not the absence of it

Creating bugs and moving on because checking was inconvenient is the specific behaviour this skill exists to stop. Doing it to this skill is not irony, it is just the bug.

Whose call the cost is. A full run is minutes; a bad one is half an hour. Whoever installed this already weighed a slower commit against a bug reaching main. An agent that quietly decides the check is too slow today has not made an engineering judgement — it has overridden that decision on behalf of someone not in the room, in the direction that happens to be less work. If the cost genuinely matters right now, say so in one sentence and let them choose.

Whose decision it is

Two short lists, because both failures live at the boundary between them: asking about the first trains the user to skim, and deciding the second for them is choice 3 taken silently.

Yours, never asked. Whether to run and when; whether a candidate is proven, refuted or dropped; how to fix a proven bug inside the change; whether to iterate, up to the cap; running the suite; committing a run that ended clean with nothing open — except under caller commits, where the commit is the caller's.

The user's, always asked. What to act on: each root cause and each recommendation (fix now / write it down / skip), each unresolved entry of an abort, whether a commit goes ahead when something reported-not-fixed changes what it means (report-only mode), and the Step 0 stop — which of a file's diverging staged and unstaged edits to keep.

When you do ask, use the host's interactive picker (in Claude Code, the AskUserQuestion tool): any question with discrete options goes through it, one question per decision, in severity order, never buried in prose at the end of a long report. Plain text is the fallback for hosts without a picker, not the default. The picker is also the one thing here that is structural rather than advisory — a turn with a picker open cannot end until the user answers — which is why the next rule matters.

Asked means the user saw it. A question is asked when it is in a reply the user reads or a picker they answer — not when an agent wrote it down. If this skill runs in an agent that reports to another agent, its Step 9 report and its questions reach that agent, and nothing has been asked yet: the hop moves the picker, it does not discharge the question. So:

  • If an agent you spawned ran this on its own change, its report is your Step 9 input, not your Step 9 output. Every open decision in that report goes in front of the user in your next reply, before anything else — through your picker, one question per decision, or the report's numbered block verbatim where there is none. "The run aborted" or "it found three root causes" with no options under it is relaying about the report instead of delivering it; if you are writing that sentence, that is the bug. Holding the decision across a turn while other work finishes is choice 3, made for them — the other work waits, or the reply carries both.
  • If you are running this in an agent that reports to another agent, you have no picker that reaches the user. Print the whole report in its format — not a summary of it — with the hand-off footer (reference.md) that tells whoever holds it what is still theirs to ask. Say plainly which decisions are open and that you could not ask them. Under caller commits, the result block's decisions list replaces the footer (When the caller commits).

A run has not ended until its questions are in front of the user, whichever agent gets them there.

Step 0 — Scope

The change under review is the staged diff:

git diff --cached --stat
git diff --cached

Read the whole file for each changed file, not just the hunks — a bug is usually in the interaction between new code and the code around it.

If any file appears in both the staged and the unstaged diff, stop. You would be reasoning about the index while testing the working tree, and they disagree. Tell the user which files diverge and ask them to stage or revert those edits. Never git stash to work around this — the user has uncommitted work and this skill is about to write files on top of it.

Unstaged changes in other files are fine. Note them in the report.

Do not hunt for bugs in specs, documentation, or existing tests. Changes to those files are still context worth reading.

Step 1 — Triage

Triage decides from the diff, not from the commit message. For a diff of up to about 300 changed lines, triage it yourself, in the loop you are already in: a sub-agent costs more to start than reading a diff that size. Delegate a larger diff to a single fast pass on a small, cheap model. Either way, the pass gets the file list, the diff, and whether the user asked for a checkpoint (below). Ask for one of REVIEW or SKIP plus a one-line reason. Do not ask for a commit message first, and do not read a subject prefix as a signal — this skill imposes no commit convention (reference.md).

SKIP when the diff shows:

  • Nothing outside docs, specs, tests, lockfiles, generated or vendored files changed.
  • The change is purely formatting, renames, moves, or comments — no behavior is different.
  • It is git's own revert or merge.

Work in progress is the one skip a diff can't show. SKIP it only when the user asked, in this conversation, for a checkpoint or a work-in-progress save — or said the same thing in other words (tmp, save point, squash me). A checkpoint defers the check, never exempts the code: its behavior changes are reviewed when they land in a commit that is not a checkpoint.

REVIEW for everything else. When unsure, review. A small diff is not a reason to skip; plenty of one-line changes are wrong. "Looks simple" is never a valid skip reason.

If the user explicitly asked for this skill, run it regardless of triage.

On SKIP, print the report (Step 9) with the skip reason and stop hunting. SKIP is a value for the trailer, not an exemption from it — the commit still goes through commit-with-trailer.sh with skipped at triage (<reason>) as its third argument (Step 9). Skipping the hunt is the cheap half; skipping the commit script is what puts an unchecked commit in front of the hook. Under caller commits, the same value goes to the result script instead (When the caller commits).

A hook report is an input to triage, not an instruction to run

The companion hook reports commits that landed without a trailer. Look at what actually landed — this is what keeps a false alarm cheap:

what the reported commit isresponse
docs, formatting, a rename, a revert — any skip categorygit commit --amend --no-edit --trailer "Bug-hunter: skipped at triage (<reason>)"
not new work at all — the hook misfired on a branch switch, a replay, a commit that predates the hookthe same amend, with Bug-hunter: not new work (<what it actually was>), and say the hook misfired
real codeget the change back in front of you and run the full loop, then record the trailer

The report gives you the sha and the repository and stops, deliberately, because it cannot see where you are standing. Check where that sha sits relative to your HEAD before acting on it. The amends above assume it is your HEAD, which is often false: git commit then git checkout main is the case this hook exists for, and there they land on the wrong commit.

Step 2 — Baseline

Run the project's test suite, typecheck, or build — whatever it already has — and record the result. If something is already failing, note it as the baseline. You are not responsible for pre-existing failures, but you must not add to them, and you must not mistake one for a bug you found. This is running a command and writing down what it printed: run it yourself. Never start a sub-agent to run a command; starting one costs more than reading its output. Keep only the summary lines of a long output.

Step 3 — Find candidates

Delegate to a subagent using the strongest available model. Its job is to find plausible scenarios the code does not appear to handle.

Every candidate must come with:

  • The scenario — what situation the code does not handle.
  • Reachability — a real caller, entry point, input, or state that gets there. Trace it. A candidate whose trigger cannot be named is not a candidate.
  • Concrete input — the specific value, sequence, or condition.
  • The wrong outcome — what actually happens, and what should have.

Observe it before reporting it. Run the thing — call the function, execute the script, curl the endpoint, whatever is cheapest — with the concrete input, and record what came back. Reading code is how you find a candidate; running it is how you know. Report the command and the result next to each candidate. This is the single highest-leverage habit in this skill: a finder that runs its own candidates reports almost no false positives, and one that only reasons reports plenty. If a candidate genuinely cannot be observed without building something, say so and let Step 4 decide.

Where an invariant is stated, search for a counterexample with a property. When a spec's MUST or SHALL, a comment, or a name promises something the code maintains — a cursor that never moves past an item it has not processed, an ordering, a state machine's legal transitions, a parse/print round trip — the finder may generate random inputs or operation sequences, assert the invariant, and shrink a failure to a minimal input: the library does that, and with a hand-written loop the finder does it by hand. A shrunk failure is observed by construction: a candidate with its concrete input and the wrong outcome it produced. It still needs its reachability. A generator builds states no real caller reaches, so trace the shrunk input back to a real entry point, or drop it. Use the project's property library if it has one, otherwise a short seeded loop in a scratch file that is never staged; never install a dependency. Bound each property to a fixed number of runs or a few seconds, inside this pass. No invariant stated, no property — this is not for fishing. The procedure is in reference.md.

Where the changed functions have tests, mutants check that the tests pin them. A mutant is one small deliberate change to the code, such as a < made <=. A mutant the tests still pass on has survived, and marks behaviour no test checks. Read mutation.md before starting any mutant, and follow it: mutation is never improvised. The rules you must not get wrong, which it also carries:

  • When: once per run, within one cap. Iteration 1 mutates the functions the change touches that have tests. A later iteration mutates only when the last iteration's fixes touched functions that have tests, and then only those, within what the run's cap has left.
  • Who and where: fresh mutation subagents, in the background beside the finder, never the code's author, each given mutation-brief.md as written. Never mutate source in the real tree: mutants live only in throwaway copies.
  • Cheap: each function's focused command runs only the tests that exercise it, never a whole test file when the runner can select single tests in it, and each mutant's run has a time budget; one over it counts as killed by timeout.
  • A survivor is a lead, never a finding. The finder answers each one, and a candidate it reports is proven like any other.
  • Mutants never hold up the hunt. Steps 4–7 go ahead on the finder's own candidates while the mutants run. A survivor that arrives later is judged against the code as it is then. Only the commit and the Step 9 report wait until every survivor is answered.

Not candidates. Style, naming, formatting, preferences, "consider extracting this", missing comments, defensive checks for conditions that cannot occur, or hypotheticals with no caller. Report none of these. The value of this skill is that everything it prints is real.

Run this pass in the background and keep working

This is the long step. Launch it in the background and get on with something else — the find pass and the refute pass only read, so nothing stops you doing other work while they run. Waiting at an idle prompt is where most of the "this is too expensive" feeling comes from, and it is recoverable.

Safe meanwhile: unrelated files, documentation, answering the user, planning. Not safe:

  • Do not touch the files under review. Findings against a version that no longer exists are worthless. If the other work needs an edit there, the run is invalid — wait, or restart it against the new state. Running mutation subagents do not stop Step 6: its fixes may edit these files while they run, since each survivor is judged against the code as it is when it arrives (mutation.md).
  • Do not commit. The commit waits for the whole run, always. Backgrounding is for spending the wait well, not for shipping while the check is in flight.
  • Do not guess at the result. You know nothing until the pass reports back. Never write up findings you have not received, and if the user asks in the meantime, say it is still running.

Steps 4, 6 and 7 write files, so they run in the foreground. It is the reading passes that parallelise, and the mutation subagents, which write only in their own copies.

When the pass reports back, the run is the next thing. Its return is Step 4 starting — or, after the last iteration, Step 9 — not a notification to file while the other work finishes. Finish the edit you are in the middle of, then pick the run back up. If what came back ends the run, the report and every question in it go out in that same turn's reply (Whose decision it is). A result that arrives during other work and sits in your context is how a run quietly never ends.

Steps 4–7 — Prove, Refute, Fix, Verify

Read reference.md before doing any of these. It carries the procedure; below is only the part you cannot get wrong.

Step 4 — Prove. Write the regression test, run it against the unfixed code, and require it to fail for the predicted reason — not an import error, not a missing fixture. A candidate that cannot be driven red is dropped and its test deleted. Never leave a failing or discarded test in the tree. This is the whole contract: without an observed red, a finding is a guess. Do not delegate this — Step 3 hands over the concrete input and the expected wrong outcome, so writing one test in the project's existing framework is translation, not investigation; write and run it in the loop you are already in, and a small, cheap model is sufficient. The red is the check, and it is empirical.

A survivor the finder called a gap gets a test, not a fix, proven green on the real code and red on the mutant, by mutation.md.

A property's counterexample is committed as an example, not as the property. The regression test is the shrunk input written as an ordinary deterministic test in the project's framework. A property checks many inputs, so even with a fixed seed its red can come from a different counterexample than the predicted one; and committing one usually means a new dependency. Keeping the property as a guard is a recommendation for Step 9.

Step 5 — Refute. A separate agent that has not seen the finder's reasoning tries to kill each proven bug — unreachable, guarded upstream, intentional, or asserting something the code never promised. Only survivors get fixed; refuted findings are dropped and their tests deleted. Use a capable model — this is judgment, not mechanics, so do not tier it down the way Steps 2, 4 and 7 are; it does not need the strongest available either, which Steps 3 and 6 do.

Refuters run concurrently. Each one judges its own finding and must not see the others' reasoning, so there is nothing to serialize — launch them all at once rather than one per bug in turn. The one question that waits is the cross-finding one below, which needs the surviving set.

A refuter judges findings, never proposals. It is told to default to skepticism, which is right for "is this bug real" and wrong for anything else: pointed at a proposed design change, its disposition defends whatever exists today (defending one design once cost eight iterations). So: findings to the refuter, design decisions to the user via Step 9's picker. The one question about fixes that belongs here is factual rather than evaluative — do any of these fixes fight each other? Give it the specs and decision records Step 6 reads too, so it can say whether a finding re-treads ground already recorded as rejected.

Fresh refuters, one per item, also review every survivor test and every judged-dead claim, beside the findings (mutation.md). Each is a factual claim, which is why it belongs here and not with the user: this test asserts what the code should do, and this code is never reached.

Tell the refuter which spec text predates the change. A refuter cannot see when a document was written, so it reads spec, design and task text as pre-existing intent — and documentation authored in the same session as the code is not that. It is the implementer describing what they just wrote, and it has killed a reproduced finding as "a deliberate, documented design choice". So the brief names the committed spec separately from the in-flight text, and the refuter checks provenance — git log -S "<phrase>" -- <file>, git diff, git blame — before treating any passage as a decision. A sentence that exists in no commit refutes nothing; the procedure is in reference.md.

Step 6 — Fix. Write the fix yourself, resume this iteration's Step 3 finder via SendMessage when that saves tokens, since it already read the files and ran the candidates, or start a fresh agent on the strongest available model; any of the three is fine. Whoever writes it gets the surviving findings, the refuter verdicts and Step 5's fix-conflict notes (reference.md).

Read the codebase's specs and decision records for this code first, if it has them: the OpenSpec spec of the capability that covers it, that capability's decision record (openspec/specs/<capability>/decisions.md), and the Decisions section of the design.md of any open OpenSpec change that covers it — the same places Step 9 writes to. Never read a free-form DECISIONS.md (upper case, outside openspec/) or an ADR: those are written for people, not agents. Check two things: whether this approach is already recorded as rejected, and — for any recorded lesson you are about to reuse, such as an earlier fix or rule — whether its recorded scope covers your case. A lesson applied outside its scope is a fresh bug wearing the authority of an old fix.

Then answer: is there a better approach that would have prevented bugs like this from ever happening? Prefer simpler code, simpler architecture, fewer moving parts. Confine the fix to the change under review; if the real cause sits outside it, that is a root cause for Step 9, not something to fix in passing.

The fixer also removes dead code that a survivor exposed, never a check at a trust boundary, and never exported code without the user's yes (mutation.md).

Step 7 — Verify. Every new test passes. The full suite runs once per run, not once per iteration — at Step 2 for the baseline, and again immediately before the Step 9 report, which nothing ships without. In between, run the new regression tests plus whatever existing tests cover the files the fix touched; that is what catches a fix breaking its neighbours while it is still cheap to attribute. Never weaken, delete or skip an existing test, or change its assertions, to reach green. If a pre-existing test now fails, the fix broke it — fix the fix, or revert it and report it unresolved. Running tests and comparing counts to the baseline is running commands: do it yourself, as in Step 2.

Step 8 — Iterate

A run and an iteration are different things, and the counter resets.

  • An iteration is one Step 3 → Step 7 pass over the current state of the change, including the fixes just written.
  • A run is up to three iterations. It ends when an iteration comes back clean, or at the abort — three iterations that each found something, followed by the root-causes report and one decision per cause (Step 9), never a fourth iteration. The full suite belongs to the run, not to the iteration: whichever ending applies, run it before the report and hold the report until it is no worse than the Step 2 baseline.
  • Whatever the user then asks for starts a new run at iteration 1. Applying a root cause, taking a recommendation, building something they picked: that is new code, and new code gets its own three-iteration budget.

So a long session is a sequence of short runs, not one endless run. Count and report within the run — Bug-hunter: 2 iterations, 3 bugs fixed means this run, not this week.

An iteration that fixed something is never the last one. Stop only after a pass that finds nothing. An iteration whose finder found nothing is clean only once every mutation subagent running has reported and every survivor sent to it is answered with no bug proven; it waits for them, since there is nothing else to do. A survivor proven a bug counts as that iteration having found something. An iteration that found something does not wait: the next one starts while the mutants still run (mutation.md). A fix is new code written minutes ago by someone who was just thinking about a different problem, so stopping straight after writing one stops where the change is least reviewed — "I fixed four bugs" and "this code is clean" are different claims, and only the second is what anyone wanted.

If an iteration still yields bugs, before writing more fixes, ask why the previous pass missed them. Is the finding approach looking in the wrong place? Is the code's design the reason bugs keep appearing here? Rethink the approach, then continue. If the answer is a design change larger than the change under review, take it to Step 9 as a root cause and let the user decide.

The abort is an ending, not a request for more budget

When the third iteration finishes and something is still unresolved, the run is over. Do not run a fourth iteration and do not offer one. Everything still open becomes a numbered root cause in the Step 9 report, each with its three choices attached — and choice 1 is a new run at iteration 1, scoped to that one cause. That is not a fourth iteration renamed: the counter resets for new work, so a fresh finder reading the fixed code gets a fresh three-iteration budget.

The commonest unresolved cause is the run's own remainder. Iteration 3 fixed something, so by this step's own rule it cannot be the last iteration — and it is, because the budget is spent. Iteration 3's fixes are code written minutes ago that no finder pass has ever read. Report it as a root cause in those words: iteration 3's fixes never got a finder pass — the budget ended before one could run, naming what those fixes touched.

Mutation leftovers at the cap are budget causes too: survivors still unanswered, and those answered as candidates but not yet proven, each an entry (mutation.md). Never a fourth iteration for them, and never drop one silently.

That is a different kind of cause from Step 6's, and the report says which. Step 6's root cause is about place — the real cause sits outside the change under review, so fixing it there would exceed the scope. The remainder is about budget — the cause sits squarely inside the change under review, and the pass that would have found it does not exist. Do not conflate them; the user is weighing a different thing in each case. Both get the same three choices, because the shape of the decision is the same either way. The shape of the entry is in reference.md.

Step 9 — Report and commit

Every run ends with the report. Always, unasked, in the reply the user can see. A run ends when an iteration comes back clean or when the third one aborts — those are the only two endings, and both print it. Never ask whether the user wants one, and never defer one to "next time": that is the same evasion as asking permission to run the check. If a run happened, its report is owed.

Prose is not the report. Print the format — headline counts, root causes, per-iteration findings, result — and put any commentary around it, not instead of it. One report per run, covering every pass. No interim reports (Step 8).

Anything still open goes in this report, with its question attached. A decision the user has not been given cannot be made, and a run that ends with an unasked question has not ended.

A run that aborted reports one root cause per unresolved thing, and asks about each. Step 6's out-of-scope causes and the run's own unreviewed remainder are both entries, each labelled as the kind it is (Step 8), each carrying the three choices below through the picker. Describing the gap accurately and stopping there is not the report — an accurate description of something nobody was asked to decide is choice 3 taken on the user's behalf.

The full report format, with a worked example and the rules for each section, is in reference.md. Read it before writing one. When mutants ran, the result block carries one line for them, such as mutation 14 mutants of 4 changed functions — 11 killed (1 by timeout), 2 survivors killed by new or strengthened tests, 1 dead code removed, 0 stale, 0 ignored, and the report carries a per-operator table. Each removal of dead code is listed in its iteration, and exported or public code that a search found no caller for is a recommendation, asked like any other. The commit itself — what to stage, the trailer, splitting — is in reference.md.

Commit through the script. Never assemble git commit -m … --trailer … by hand — not for a fix, not for a skip, not for a one-line docs change. The one exception, an amend that adds a missing trailer, is described below.

If the subjects in git log start with a TYPE(Scope): prefix, such as FIX(parser):, use one too, and reuse a scope (the part in parentheses) that git log already shows, such as parser, written exactly as it is there, lowercase included, so each scope keeps one spelling across the log. When none fits and you make up a new scope, capitalise its first letter, as in FIX(Parser):. The mix of cases this gives is intended: existing scopes keep their spelling, and new ones start with a capital.

In the commands below, <base directory> is this skill's base directory: the directory that holds this SKILL.md. Claude Code names it on a "Base directory for this skill:" line, which it prints just before this file's text when the skill loads. In the commands, replace <base directory>, angle brackets and all, with the path written out in full, as that line gives it; don't swap in a home-directory install path, which may be missing or hold a different copy. The script's path is absolute because it lives in this skill, not in your project. Run the command from inside your project, though: the git commands the script runs act on the repository you run it from.

First stage everything the commit holds. Then write the message to msg.txt in the directory you will run the command from. It is not part of the commit: never stage it, and delete it once the commit is made, so that a later git add cannot pick it up.

Next, check whether the prose skill is installed: invoke the manage-skills skill and ask it to list the skills (its list action); prose is installed when prose's row in that list says installed. If it is, use the command under When prose is installed, further down, not the one here. If it is not, standing in the project, run both lines below in one tool call, since a shell variable set in one tool call is gone by the next:

BH="<base directory>/scripts/commit-with-trailer.sh"
"$BH" -F msg.txt "1 iteration, 1 bug fixed" "<Co-Authored-By value from your attribution instructions>"

1 iteration, 1 bug fixed is a sample: pass this run's own result in the same form, such as 0 bugs found or aborted after iteration 2 (<reason>). The arguments, in order:

  1. -F msg.txt: the file holding the commit message, which is the subject, a blank line, then the body. Never put a trailer in it. With a file, the message never has to be quoted on the command line. The script also accepts the message as two shell arguments, "<subject>" "<body>", in place of -F msg.txt, but the file is the form to use.
  2. The Bug-hunter: value, without the Bug-hunter: prefix.
  3. Optionally, the Co-Authored-By value: the text after Co-Authored-By: , without that prefix. Your host may give you a co-author line for commits; in Claude Code it is in the attribution instructions, and names the current model. Leave the argument out if you have none. In the command, the placeholder <Co-Authored-By value from your attribution instructions> stands for the whole value: replace it, angle brackets and all. The value itself has angle brackets around the email, as in "Name <[email protected]>", and keeps them.

A skip commits through the script too, with its reason in the Bug-hunter: value, argument 2. Like the command above, this is the form without prose:

BH="<base directory>/scripts/commit-with-trailer.sh"
"$BH" -F msg.txt "skipped at triage (docs only)" "<Co-Authored-By value from your attribution instructions>"

The script passes the Bug-hunter: and Co-Authored-By values to git as --trailer options, and git places each in the message's trailer block, the last paragraph, where git reads trailers. You never place a trailer line yourself.

Just before committing, the script does two things, in order:

  1. It runs the repository's pre-commit hook, if there is one, so a formatter there cannot change the files after step 2 ties the trailers to them. If the hook fails, nothing is committed. A hook may rewrite files and still fail, as some formatters do, so review what it changed, stage what belongs, and run the script again. With prose installed, first run prose's two check calls again (see When prose is installed, below), since the staged files have changed.
  2. It ties the trailers to the staged files. For a Bug-hunter: value that does not start with skipped or Skipped, it runs mint-trailer.sh, a script beside it in scripts/, which prints a Bug-hunter-Tree: <hash> line, where the hash identifies the git tree object for the staged files; commit-with-trailer.sh adds that line as a trailer. For another skill's value, such as prose's (see When prose is installed, below), it runs that skill's verifier, which confirms the value was made from those files and, for prose, from the message. This tie is what lets a check made later, such as bug-hunter's companion hook (below), tell whether the committed files are the ones the trailers were made from.

When every value passed starts with skipped or Skipped (the Bug-hunter: value, and each other skill's value if any was passed), no value needs tying, and the script skips both steps. (The Co-Authored-By value is never tied.) git then runs the pre-commit hook during the commit as usual. A Prose: value never starts with either, so with prose installed both steps run, even for a skip.

bug-hunter's companion hook, installed with this skill, is not a git hook: your agent host runs it after each shell command you run. It reports to you each commit that landed without a valid Bug-hunter: trailer. It counts a non-skip value as valid only with a Bug-hunter-Tree line beside it whose hash matches the committed files. So a hand-typed git commit with a non-skip value, which leaves out the mint, gets reported, and so does a commit whose files changed after the mint.

After committing, check that the trailer landed: git log -1 --format='%(trailers:key=Bug-hunter)' prints the Bug-hunter: line. If it prints nothing, the commit has no Bug-hunter: trailer, usually because it was made by hand. While the commit is still local, add the trailer with an amend. Amending is the one case where you type git commit … --trailer … by hand: the script makes new commits and cannot amend one. Once the commit is pushed, ask the user before amending it: that rewrites published history. Stage nothing new before the amend: anything staged would go into the commit.

For a skip value, which needs no mint:

git commit --amend --no-edit --trailer "Bug-hunter: <value>"

For a value that is not a skip, use this form instead. It adds a second --trailer, carrying the Bug-hunter-Tree line that mint-trailer.sh prints, and --no-verify:

git commit --amend --no-edit --no-verify \
  --trailer "Bug-hunter: <value>" \
  --trailer "$("<base directory>/scripts/mint-trailer.sh")"

With nothing new staged, the staged files are the commit's own, since git leaves the index matching the commit it just made. The mint hashes those files and the amend commits them, so the Bug-hunter-Tree hash matches the amended commit. --no-verify skips the pre-commit and commit-msg hooks for this amend. The pre-commit hook must not run here, because it would run after the mint, and a hook that changed the files would leave the hash naming files the commit does not hold. --no-verify cannot skip one hook without the other, so neither hook runs on this amend, and the commit-msg hook never sees the trailer lines the amend adds. That is the cost of adding them this way. In the skip form the hooks run as usual, since there is no mint for a hook's change to break. A commit made by hand may lack other trailers too. Add a missing Co-Authored-By with its own --trailer in the same amend; to add a missing Prose: trailer, follow prose's SKILL.md, Committing, which covers an amend.

The mechanics the script wraps, and what the binding does and does not attest to: reference.md.

When prose is installed, the same commit carries its Prose: trailer. The commit library that commit-with-trailer.sh calls is shared with other skills, and it puts every installed skill's trailer on one commit, not one commit per skill. So you pass prose's value to commit-with-trailer.sh, which hands it on. Below, $S is prose's scripts directory: scripts/ in prose's base directory, which Claude Code names on the "Base directory for this skill:" line when the prose skill loads; if prose is not loaded yet, invoke the prose skill to see that line. The commands below set S with S="<prose's scripts directory>": replace that placeholder with the path written out in full, as with <base directory>, and leave each $S as it is. Keep the S= in the same tool call as the command that uses it: a variable set in an earlier tool call is gone by the next one.

When prose is installed, every commit, a skip included, goes through prose's two calls. With the commit staged and msg.txt written, as above:

  1. Run prose's first call. It lists the prose in the change and prints the rules and a pass token: S="<prose's scripts directory>"; python3 "$S/prose.py" check -F msg.txt --goals "review the fix; check it is safe to merge". --goals names what the commit message's reader wants from it; replace the sample goals with your change's. Add --goals-for <path or pattern> '<goals>' for each changed doc or spec, naming its own reader's goals; prose's SKILL.md says how.
  2. Rewrite the listed comments, docs and message by those rules; restage.
  3. Run the second call, which prints the Prose: trailer: S="<prose's scripts directory>"; python3 "$S/prose.py" check -F msg.txt --pass <token>. If it exits 5, it prints no trailer yet: it asks for a cold read before it will sign. See If prose asks for a cold read, after the command below. When the second call succeeds after a cold read, the Prose: value it prints ends with the cold read's counts (how many findings there were, and how many were fixed or kept); pass the whole value as printed.
  4. Commit once, passing prose's value in a family before --. A family is four shell arguments: --verified-value, the verifier's path ($S/verify-staged.sh), the key Prose, and the value, which is the text after Prose: that the second call printed. Keep the value quoted: it holds a space, and unquoted it would split into two arguments. After -- come the same arguments as without prose: -F msg.txt, the Bug-hunter: value, and the optional Co-Authored-By value. Pass -F the msg.txt as it stood at the last run of the second call, the run that printed the value: the Prose: value is tied to that exact text, so any edit after that call makes the commit refuse. Set BH and S in the same tool call that runs the command, as the block below does: a variable set in an earlier tool call is gone by now. The Prose value below is a sample: pass the one your second call printed. For a skip, pass the skip value, such as skipped at triage (docs only), in place of 1 iteration, 1 bug fixed.
BH="<base directory>/scripts/commit-with-trailer.sh"
S="<prose's scripts directory>"
"$BH" --verified-value "$S/verify-staged.sh" Prose "✓ 4d593e935186:9138830a72a2" -- \
  -F msg.txt "1 iteration, 1 bug fixed" "<Co-Authored-By value from your attribution instructions>"

If prose asks for a cold read (the second call exits 5), it prints a cold-read brief. Follow prose's SKILL.md, The cold read, which gives the form of the report file and the dispositions file. In order: you start a fresh subagent and give it the brief; it writes a report file; you answer the report in a dispositions file; then you run the second call again with --cold-read <report file> and --dispositions <dispositions file>. If you cannot start a subagent, prose's reference.md, Cold read: counts, refusals, reuse and hand-back, says what to do: hand the cold read back to whoever reads your result, or, when nothing does, read the brief yourself. The cold reader's report holds findings, each a place it stopped, and one understood line per item, saying what it took the item to mean. The dispositions file answers each finding with fixed or kept, and each understood line with matches, fixed or kept. When the rerun succeeds, run the commit command shown in step 4, putting in the Prose value the rerun prints. The one exception is in the next paragraph: when you would keep a finding but cannot ask a person, you don't commit at all.

To keep a finding or understood line is to leave the passage it is about unchanged; for an understood line, that means accepting that a reader may take a meaning other than yours. Before you keep one, a person must agree, through your host's interactive picker, and you write kept (asked): <reason> against it in the dispositions file. Don't commit if you would keep a finding or line but cannot ask a person: for example, because you are a subagent with no picker, or because you run under caller commits (a caller's brief that leaves the commit to the caller; see When the caller commits). Leave the change staged, in the dispositions file write kept: <reason> against each finding or line you would keep, and run the second call anyway, to get its refusal: it refuses any kept without (asked), and its message names each one. Put that message in your bug-hunter report, with one open decision ("keep as is · fix it") for each finding or line it names.

A commit that changes openspec/ carries spec-check's Spec-check: value too, when spec-check is installed (the manage-skills list used above to check for prose says so): one more --verified-value family before --, as spec-check's SKILL.md (Commit) shows.

Stage and edit nothing between the second call that printed the value and the commit: just before committing, the script runs prose's verifier, which refuses the commit if the staged files or the message changed since that call. If it refuses, run prose's two calls again and commit with the new value.

A prose pass that touches only comments, docs or the message needs no new hunt. The hunt covered the code, and such a pass leaves the code as it was. The Bug-hunter-Tree binding still matches: the script mints it when it commits, over the files staged then, rewrite included. If the pass changes code, code was written since the last clean hunt, and a hunt is owed.

Three things are required every run, and they are the ones agents drop first:

  • The headline counts. Iterations, fixed, refuted, reported — and, whenever anything is open, how many decisions the user owes.
  • Root causes above the findings, numbered so the user can say "do ①". A cause buried under a dozen symptoms is a cause nobody acts on.
  • A why not on every finding that was not fixed. "Reported" with no reason is a shrug.

A skipped run is one line, and it is worth printing.

🔍 bug-hunter · skipped at triage — docs only, no executable code

That is the whole report — it is not the whole job. Knowing the check ran and why it stopped is useful, and at one line it costs nothing to skim. The commit is still made the same way as any other (under caller commits, handed back the same way — When the caller commits), and the same sentence is the third argument to the script above: "$BH" -F msg.txt "skipped at triage (docs only, no executable code)", with msg.txt holding Fix a typo in the README. Keep no-op output proportional — the expensive thing is never a short line saying nothing happened, it is a long report demanding work that does not need doing.

Write down what was rejected, before you commit

A run that reverted a fix, refuted a finding, or chose one approach over another has produced knowledge that does not survive in the diff — the diff shows what the code is, never what it stopped being. That knowledge is about the code under review, and it goes where the next agent to change that code will read it — never in a DECISIONS.md or an ADR, which agents don't read:

  1. A rejection about one site — one fix, one function — goes in a comment at that site, in the codebase's own idiom: present tense, a warning rather than a history. That is where the next person tempted back to it will be standing.
  2. A rejection about a capability goes in its decision record, openspec/specs/<capability>/decisions.md; while an open OpenSpec change covers this code, in the Decisions section of that change's design.md instead. A record holds only evidence a test can't express: the failure, the measurement or the outside fact that rules the approach out.
  3. With no OpenSpec spec for this code, the comment at the site carries it, and the commit message names it. Create no new decision file.

An entry says three things: what was tried and rejected, the evidence against it, and where the lesson stops applying. That last field is not optional; a lesson recorded without its boundary gets applied outside it. A comment at the site carries the same in a sentence — the alternative, and what breaks when it is taken. The shape of each: reference.md.

The records of three reverts in this skill's own history survive only because they were written down within minutes of happening. Nothing else would have kept them.

The commit waits for the run, exactly as the report does

One commit at the end, after the last iteration — not one per iteration. The fixes from an unfinished run are unverified by definition; a commit says "this is done", and mid-run it isn't. Work in progress lives in the working tree between iterations, which is what the abort clause already assumes.

There is no exception, and an earlier draft invented one. What an invented exception provides is a documented reason to do the thing the rule forbids, which is how every other rule here has been walked around.

What does need saying, because it is a real distinction rather than an escape: the rule is about the change under review. A commit that touches any file the run is reviewing waits for the run. A genuinely separate change — different files, its own triage, its own trailer — is its own commit and is not covered. That test is checkable; "it felt unrelated" is not.

When the caller commits

If the brief says "caller commits", the run is the same; only the commit moves. A caller that builds its own commits — through an API, in a pipeline, anywhere this skill does not run git commit — says so in the brief: caller commits: return the result, do not commit. Steps 0–8 run as written, and every rule under Do not evade this skill still applies. Step 9 ends differently:

  1. Do not commit. Leave the change staged: everything the run reviewed, every fix and test it added, and every record it wrote under Write down what was rejected.
  2. Print the report, as in any run.
  3. Run the result script last, after the final git add. It mints the binding over what is staged and prints the whole block. Its arguments are the Bug-hunter: value — what the commit script's third argument would have been — and the open decisions, one per line, without numbers. Its path starts with this skill's base directory, written out in full as the commit step says:
BR="<base directory>/scripts/caller-result.sh"
"$BR" "1 iteration, 1 bug fixed" --decisions - <<'EOF'
refund state is rebuilt on every read — 1 fix it now · 2 write it down · 3 skip
EOF

With nothing open, drop --decisions and the heredoc. A skip passes its skipped at triage (<reason>) value and mints nothing. When prose is installed and a cold-read finding or understood line would be kept, prose cannot sign until a person answers it: add one decision line per kept one, such as prose: keep «<quote>» in <file>? — 1 keep as is · 2 fix it. Put prose's refusal in the bug-hunter report, with the paths of the cold-read report and the dispositions file, and prose's pass token. 4. End the reply with the script's output, whole and unedited. The caller parses it and passes the trailer lines through unchanged:

=== bug-hunter result ===
Bug-hunter: 1 iteration, 1 bug fixed
Bug-hunter-Tree: <the hash the script minted>
=== bug-hunter open decisions: 1 ===
① refund state is rebuilt on every read — 1 fix it now · 2 write it down · 3 skip
=== end bug-hunter ===

Never type the block. The script is the only way to make it, for the same reason commits go through commit-with-trailer.sh: hand-assembled trailers went wrong until a script took the assembly away. The binding signs the snapshot the run worked on; the caller commits exactly that tree, or the binding is false. So nothing is staged after the script runs. If something must be, run the script again.

Open decisions go to the caller, in that block, never through a picker. Nobody can answer a picker in a headless run. The block replaces the hand-off footer: the caller acts on it — asks a person, lists the decisions in its pull request, or holds the commit. Choosing for the user is still not yours: an open decision is never answered 2 or 3 because nobody could be asked. Format detail and a worked example: reference.md.

Then ask, one recommendation at a time

Whenever the report carries a recommendation — a root cause, a finding too large for the change under review, anything an abort left unresolved, a Step 3 property worth keeping as a permanent guard, or exported code that a search found no caller for — stop and ask the user what to do with it. Each one separately. Do not apply them silently, and do not decide on their behalf that they are out of scope. A recommendation is where a small change prevents a class of bugs rather than one instance of one.

Use the host's interactive picker if it has one, one question per recommendation, in severity order. Offer only the options that honestly apply: "implement it now" belongs there when you have a fix you would defend, and when you do not, say so and offer the other two. Padding the menu with an option you have no confidence in is how a run ends with a hopeful patch on a problem nobody understood. A worked example is in reference.md.

Exported code with no caller found has its own three options: remove it, keep it and add a test of its contract, or skip. Keeping it is the safe default, and removal needs the user's yes. The wording is in reference.md.

If the report reached you from an agent you spawned, the asking is yours. Its picker could not reach the user and its text asked nobody; see Whose decision it is — next reply, before anything else.

Take the answers per item — "1 for ①, 2 for ②" is a normal reply, and so is "all 1". If the user answers once for everything, that applies to everything.

1 — fix it now. Apply the change, then start the loop over it immediately, without asking again. The user already said yes; asking "shall I now check the fix?" is asking the same question twice and invites a no the second time. A root-cause fix is the code most in need of checking, not least: written in one pass, minutes after the analysis, usually touching more than the original change did — that breadth is what made it a root cause. Every time this skill has skipped that pass, the next run found something. Say what the fix will touch before starting it, then apply, loop, and report once at the end.

For a cause the abort left unreviewed rather than unfixed, there is nothing to apply and the pass itself is option 1: a new run at iteration 1, scoped to that cause — the fixes iteration 3 wrote, and the files they touched. Say 1 — new run rather than fix it now when that is what the option is, and never count it as a fourth iteration, in the report or in the trailer.

2 — write it down. Record it as a known, accepted gap where this codebase already lists work it knows about and has not done. An AGENTS.md, CLAUDE.md or CONTRIBUTING.md that names the place settles it; otherwise, in this order: a spec's Open Questions or Roads Not Taken section, an existing OpenSpec change, a docs/ entry, then the closest README — never a DECISIONS.md or an ADR. Only if none of those exist, create docs/open-questions.md. One entry: the cause, which findings it produced, the recommendation, the odds that the recommendation works, and the date. Commit it with the rest of the change. A recommendation that lives only in a terminal report is gone when the tab closes, which is choice 3 with extra steps.

3 — skip. Nothing happens. Still name it in the report so the run's record is complete, and do not raise it again in this session.

What is not on the list is deciding for them. Reporting a cause, moving on, and letting it evaporate — when the user would have said "fix it" — is the same failure as never running this skill at all. Relaying that a cause exists, with no options under it, is the same thing one hop later.

Never

  • Run inside a git pre-commit hook.
  • git stash, amend, or rebase.
  • Weaken, skip or delete an existing test, or change its assertions. The one change allowed is adding assertions to cover a mutation survivor, proven green on the real code and red on the mutant (Step 4). A test that asserts nothing still stays: it catches crashes.
  • Mutate source in the real tree. Mutants live only in a throwaway copy.
  • Leave a failing or discarded test in the tree.
  • Report a bug with no failing test, or a style opinion of any kind.
  • Apply a refactor larger than the change under review.
  • Write to this skill's own files unless they are the change under review.
  • Read or write a DECISIONS.md or an ADR, in this skill or in the codebase. They are for people; what an agent needs is in the specs and decision records.
  • End a reply holding a decision from the user's list. Written is not asked; relayed to you is not relayed to them.

Related Skills