"Proactively hunt unobserved bugs in resting code: a read-only two-stage scan, recall-biased per-lens hunter subagents, then a separate fresh-context default-refute gate, over a target path/feature/diff or a rotated lane, emitting only verified 5-field findings. Use when: 'find a bug', 'bug hunt', 'scan for bugs', 'hunt for bugs in <X>'. Skip when: reviewing a diff (`review:code-review`); security auditing (`review:security-review`); root-causing an observed failure (`debugging:debug`); doc/config/code/arch claim drift, all dimensions (`codebase-health:audit`); structural tidying (`code-tidying:tidy`); comment markers (`work-items:scan-todos`); coverage gaps (`testing:audit`, `mutation-testing:audit`). Disambiguation: 'scan repo for issues' is the upstream known-issue registry (`claude-ops:known-issues`); 'file a bug' the user observed is `bugs:write`. Bare invocation neither edits nor files; `--track` files verified findings as raw intake (subject to the team's `filing_posture`)."
Resources
2Install
npx skillscat add melodic-software/claude-code-plugins/plugins-bugs-skills-scan Install via the SkillsCat registry.
Repository context. Gather first
Collect these with individual Bash calls, one command per call, never combined into a single
invocation:
- Current branch,
git branch --show-current - Recent commits,
git log --oneline -10 - Shallow clone,
git rev-parse --is-shallow-repository
Treat a failure (not a repository, git unavailable) as an unknown value and carry on. Keep these as
separate body Bash calls rather than pre-compute lines: the harness runs a skill's whole pre-compute
block as one shell invocation, and a worktree-isolated session refuses a compound command that
contains git.
Pre-computed context
Lane config: !ls "${CLAUDE_PROJECT_DIR:-.}/.claude/bugs.md" 2>/dev/null || echo "absent — bundled default lanes apply"
Consumer gotchas:
!bash "${CLAUDE_PLUGIN_ROOT}/scripts/concat-gotchas.sh"
Variables
Arguments: $ARGUMENTS
Purpose
/bugs:scan is the front of the bug lifecycle: it hunts defects nobody has observed yet, in
code that is resting. Every other finding-producer in reach needs an oracle first, a diff, a failing
test, a stack trace, a factual claim, a comment marker. This one needs none of them.
One invocation is one bounded pass: hunt a target (or a rotated lane), verify each candidate
through a separate fresh-context gate, report what survives in the plugin's five-field shape, stop.
That makes it equally usable interactively ("find a bug in the billing module"), from a /loop, or as
a daily operator routine, the same bounded unit either way.
The design premise, taken from published practice: per-run recall is low and the raw false-positive
rate is high, so recall and precision are separated into two stages. Hunters are told to be
generous; the verification gate is told to refute. A finding that reaches the report survived an agent
whose job was to kill it.
Verb contract. Read-only toward the repository
Bare invocation is read-only: it never edits code, never branches, never pushes, never files a
tracker item. Filing sits behind --track, and model auto-invocation never supplies --track on its
own. A user (or a standing lane rule) must ask for it.
Reconciliation: persisting the findings report and its cursor block under ${CLAUDE_PLUGIN_DATA}
is plugin-owned state, not target-repo mutation. The scanned repository's working tree, index, and
history are untouched by a bare run. --dry-run goes further: it emits the findings to stdout and
persists nothing. No report file, no cursor advance, no filing.
Durable cursor state never lives in .work/. Anything a wrapping session leaves under .work/ is a
checkout-local cache that a fresh clone or a git clean erases; the ladder below must re-derive the
cursor from rungs 1–3 without it, and never treats a cache note as authority.
Durability on a cloud or scheduled run. ${CLAUDE_PLUGIN_DATA} is ephemeral on a cloud session
and unreachable from the next scheduled run on another host, so the persisted report and its cursor
vanish with the container. On such a run pass --track: a filed item is the only output that
survives, and it is also what rung 1 of the cursor ladder reads back. Without --track a cloud run
is a one-off whose findings live only in the transcript.
Modes
| Argument | Mode | Behavior |
|---|---|---|
<path> / <feature> / <diff-ish ref> |
Targeted | Hunt exactly that scope. No lane rotation, no cursor advance. |
--lane <name> |
Named lane | Hunt the named lane's globs. Advances the cursor to that lane. |
| (empty) | Rotation | Self-select the next lane via the cursor ladder, then hunt it. |
--track |
Filing | After reporting, file verified findings as raw intake (see below), subject to the team's filing_posture and the filing ladder in Step 7. Composable with any mode. |
--dry-run |
Plan-and-report only | Full hunt + verification, report to stdout, zero persistence and zero cursor advance. Composable with any mode; overrides --track. |
Lane definitions (lanes, filing_posture) resolve from .claude/bugs.md per the cascade
contract. Keys, layers, and merge semantics live in
`${CLAUDE_PLUGIN_ROOT}/reference/config.md`, which is their single home.
When no layer declares lanes, use the bundled generic default lanes in
`context/lenses.md`.
Cursor ladder (rotation mode)
Which lane comes next is derived statelessly, first rung that answers wins:
- Tracker history. When the
work-itemsplugin is installed and a tracker binding resolves,
search items (--state all) for the provenance lineFiled by /bugs:scan (lane: <name>).
The most recently filed lane is the previous lane; take the next one in declaration order. The
search must be an exact match on that line (the seam's search verb, or the provider's literal
body match with the string quoted): a semantic or fuzzy search cannot tell "no match" from
"missed", so it is not an answer. When only a fuzzy search is available, or no search ran against
a bound tracker, print why and fall through to rung 2; confirm the search actually ran against a
bound tracker before trusting an empty result. An unbound tracker returns nothing, which is not
the same answer as "no prior scan filings". - Persisted-report cursor. Otherwise resolve the report directory by the same precedence
persistence uses. Step 5 below defers to/bugs:write's Step 4, and so does this rung;
reading a directory reports no longer land in is how a configuredoutput_dirsilently strands the
cursor. Then search backward from the newest report for the newest one carrying a valid
rotation cursor block, one that names the rotation lane (see
`context/findings-report.md`), and use it the same way. Reports
without such a block are skipped, not read as a cursor:/bugs:write's reports share that
directory and carry none, and a targeted scan's report must never advance rotation. If no report
carries one, fall through to rung 3. - Date-derived floor. Otherwise pick deterministically: index the declared lane list by
(days since 1970-01-01 UTC) mod (lane count). Zero state, no history, still rotates daily.
State the rung you used in the report. An operator reading a rotation run should be able to tell
tracker-derived rotation from the zero-state floor.
Reset semantics. Deleting the plugin-data reports (or scanning from a fresh machine) drops the run
to rung 3, which is a valid state, not an error. <project-slug> is the kebab-cased basename of the
project root, so two checkouts sharing a basename (~/work/api and ~/oss/api) share one cursor
directory. Name the absolute project root in the report so a reader can spot the collision. Two
sessions started the same day on a zero-state checkout will select the same lane; that is accepted
(the dedupe steps below absorb the duplicate findings) rather than jittered, because a
deterministic floor is what makes daily coverage predictable.
Budget
- Stop condition: 3 verified findings, or the lane's budget-bounded sample is complete.
- Candidate cap: at most 10 candidates per hunt wave reach the verification gate (5 on a small
scope, per the sizing table). Rank by evidence strength and drop the tail rather than widening the
wave; the dropped tail is reported, never discarded. - Refill cap: if a whole wave is refuted, at most 2 refill waves. Then report the refuted set
and stop. An unbounded refill loop is a token sink against a ~1:50 signal-to-noise base rate. - "Lane exhausted" means the sample is complete, not that the lane is bug-free. The scan is
budget-bounded sampling; never claim exhaustive coverage of a lane in the report.
Zero verified findings is a clean, successful outcome. Do not invent a finding to justify the run.
Sizing. Model by stage, breadth by scope, effort as the ceiling
Cost follows what is scanned, and precision never pays for it. Three rules, applied in this order:
Model by stage. Each stage runs on the tier its job needs, passed through the Agent tool's
per-invocation model parameter, never on the session's top model. The tiers below are the
fleet's ordered capability tiers; which alias each one binds today is the loop-lane convention's
to state, not this skill's. Resolve it from the marketplace'sdocs/conventions/loop-lane/README.md "Capability tiers", whose alias binding is dated and
carries its own recheck trigger:
| Stage | Tier | Why |
|---|---|---|
| Hunters (Step 2) | fast | The recall stage is generous by design and every output is refuted downstream, so a cheaper reader costs little precision and most of the run's tokens live here. |
| Gates (Step 4) | strong | The precision stage; the reproduction it runs is what makes a finding credible. |
| Main thread | the session's model, nothing passed | Orchestration and triage only. |
Name the alias that actually resolved in the report's run metadata, so a reader can tell which
binding the run used. A dispatch the harness rejects means the binding moved: report it and
re-read the owner doc rather than substituting an alias here.
Breadth by scope. After Step 1 enumerates the files (test suites excluded), classify the scope
and size the recall stage from it:
| Scope | Definition (files, or lines after hotspot ranking) | Lenses | Gate cap per wave |
|---|---|---|---|
| small | 5 files or 1,000 lines or fewer | 1 to 2, the highest-ranked for the surface | 5 |
| medium | 20 files or 5,000 lines or fewer | 2 to 3 | 10 |
| large | above | 4 | 10, refill waves allowed |
A targeted run over one file is small; a full lane is usually large. The scope class, the lens
count, and the models used go in the report's run metadata.
Effort as the ceiling. The Effort row bounds lenses and refill waves; scope picks
within it, never above it. A low run over a large lane still dispatches one lens.
Effort
Caller effort for this run is ${CLAUDE_EFFORT}. If that reads as a literal placeholder rather than
one of low, medium, high, xhigh, or max, this body was read directly instead of
skill-loaded, so the substitution never ran: treat the run as high and use the full budget above.
Effort scales the recall stage only. The verification gate is the precision machinery and never
relaxes: every candidate still faces a separate fresh-context refuting subagent at every level, and
"if uncertain, it is NOT a finding" binds at low exactly as it does at max.
| Effort | Lenses dispatched (step 2) | Refill waves | Stop condition |
|---|---|---|---|
low |
1, the highest-ranked lens for the scope | 0 | first verified finding |
medium |
2 | 1 | 2 verified findings |
high, xhigh, max |
up to 4, sized to the scope per the sizing table | 2 | 3 verified findings |
The gate cap per wave (10, or 5 on a small scope) holds at every level. Name the effort level and
the lens count in the report's run metadata, so a low run reads as the narrower sample it is
rather than as a clean lane.
The scan pipeline
Process one unit at a time. One target, or one lane. A unit is closed when its verified findings are
reported and deduped (and filed, under --track when the team's filing_posture allows it); only
then does the cursor advance.
Step 1. Resolve scope
Resolve the mode, the lane globs, and filing_posture from the config cascade. Enumerate the concrete
file list and drop test suites from it (*.test.*, test_*, *.Tests.ps1): hunters read a test to
learn a unit's contract and never hunt the test itself. If the enumeration exceeds ~40 files, narrow
to the highest-signal subset (recently changed, highest fan-in, most branch-dense) and say in the
report that you sampled. Then compute the hotspot reading order described in
`context/lenses.md`, one git command per Bash call; on a shallow clone, print the
skip notice and continue unranked. Classify the scope as small, medium, or large per the
sizing table.
Done when you can name the exact file list the hunters will read, the order they read it in, and
the scope class that sizes the rest of the run.
Step 2. Dispatch hunters (recall stage)
Dispatch one subagent per lens over the resolved scope, on the hunter tier the sizing table
names, each with the four-part contract: objective, output format, tool/source guidance, and task boundaries, spelled out
in `context/lenses.md`. The scope class picks the lens count and the
Effort row is the ceiling. Every hunter is read-only, must attach a verbatim evidence quote
to every candidate, may follow one hop outside the scope (a direct caller or callee of a scoped file)
and tags such a candidate out-of-lane, and is explicitly told that returning no candidate is a
valid and expected outcome.
Done when every dispatched lens has returned.
Step 3. Triage the candidate list (main thread, before any gate)
Seed, merge, and cut in the main thread, so no gate is spent on a duplicate or on a candidate whose
own hunter says nothing observable breaks:
- Seed from the prior tail. On a rotation or named-lane run, read the newest persisted report
for this lane (the directory rung 2 reads, resolved by the same precedence) and add its
"Candidates not gated" rows to the list ahead of the hunters' output: each already carries apath:line, so at equal evidence strength it ranks first. A row whose quoted location no longer
exists goes to the side observations. A targeted run has no lane and seeds nothing. - Merge same-cause candidates. Two lenses often reach one fault by different routes, and a
seeded row often returns as a fresh candidate. Keep one candidate carrying both lens ids and the
stronger evidence quote; the gate sees it once. - Drop cosmetic-impact candidates. A candidate whose stated impact is a comment, a log string,
or a report field nothing consumes is a side observation, not a bug. Move it to the report's side
observations rather than gating it. - Rank by evidence strength and cut to the gate cap. Done when the remaining list is at or
under the cap. The tail above the cap is retained in the report's "Candidates not gated" section,
which is what item 1 reads back on the next run over this lane.
Triage never confirms anything: it merges, drops, and orders. Every candidate that remains still
faces the gate.
Done when the list is at or under the gate cap and every merged or dropped candidate is accounted
for in the report.
Step 4. Verification gate (precision stage)
Dispatch a separate fresh-context subagent per candidate, on the gate tier the sizing table
names, using the prompt contract in `context/verification-gate.md`. The hunter that found a
candidate never grades it. A model re-checking its own work rubber-stamps it. The gate's default
stance is refute: it must try to construct the concrete input path that triggers the claimed
fault, and if it cannot, the candidate dies. If uncertain, it is NOT a finding.
Survivors are labeled reproduced (a check was actually run) or verified-by-reading (the fault is
established from the source, with the reproduction argument stated). Refuted candidates are retained
with their refuting argument. A high-kill gate catches some true positives, so they are never
silently dropped. A gate's one-line side observation (a stale comment, a weaker sibling check) goes to
the report's side observations.
Done when every candidate carries a verdict and a label or a refuting argument.
Step 5. Assemble and dedupe the report
Format per `context/findings-report.md`: the five fields per finding
(from /bugs:write's shape), plus the evidence label, the lens id, and the scope tag, then the refuted
tail, the candidates not gated, the side observations, and, on a rotation run, the cursor metadata
block with the sampled scope list; a targeted run emits the no-cursor line in its place. Before
persisting, run the same duplicate scan /bugs:write performs over the output directory. See Step 2
("Survey before you write") in `${CLAUDE_PLUGIN_ROOT}/skills/write/SKILL.md`,
and drop or merge findings that restate a prior report.
Persist to the path /bugs:write --file resolves (its Step 4 owns that precedence:output_dir, then ${CLAUDE_PLUGIN_DATA}/bug-reports/<project-slug>/, then a project-local
fallback). Do not reinvent either mechanism here. Write the file with the Write tool: a shell
redirect or heredoc is what a repository's write guards block, and the plugin data directory is
outside any scratch exemption. Under --dry-run, skip persistence entirely.
Done when the report is emitted, and (outside --dry-run) persisted with its path stated.
Step 6. --track filing (explicit only)
Posture gate first. Resolve filing_posture from the config cascade. See
`${CLAUDE_PLUGIN_ROOT}/reference/config.md`, whose bundled default ismanual-only when no layer declares it. When it resolves to manual-only, file nothing: print one
notice, "filing skipped: filing_posture is manual-only (<layer, or bundled default>); report-only", and stop at the report, the same degrade shape as an absent work-items. Only allowed reaches the
beats below.
Then apply the filing ladder in Step 7: on an interactive run only the findings the ladder routes
to the tracker are filed; on an unattended run every verified finding is. Then, presence-gated onwork-items, follow the dogfood-filing beats by invoking /work-items:track add, never by pathing
into another plugin's files:
- Dedupe. Search items over
--state allfirst; sameness is judged by underlying cause, not
wording. An open match gets a comment instead of a second item; a closed match is reopened or
linked from a fresh item. - Categorize as raw intake. A scan filing records what was observed, not a verified diagnosis.
- File through
track add, which owns the body template and the argv-safe write. The body
carries the five fields, and a provenance line,Filed by /bugs:scan (lane: <name>), which
is what rung 1 of the cursor ladder later recognizes. - Mark
needs-triagefrom the live label set. Apply that exact name when it exists. Create no labels. If it does not exist, say so and file without it.
The filer does not self-triage.
Degrade: if work-items is absent, or no tracker binding resolves, do not improvise a filing path.
Print one notice, "filing skipped: ; report-only", and stop at the report.
Done when each verified finding is filed, matched to an existing item, or explicitly skipped with a
printed reason.
Step 7. Hand off through the filing ladder
The scan itself edits nothing. Where a verified finding goes next follows one ladder, so a small
fix does not become a tracker item and an unattended run never leaves work stranded in a transcript:
| Run | Finding | Route |
|---|---|---|
| interactive | local: its fix stays inside one plugin or module, changes no documented contract, and has an existing test file to extend | fix it now, in this session and branch, through /implementation:implement (or the project's own fix lane) |
| interactive | non-local, or needs a design decision | file it with --track, or ask the operator |
| any | security-relevant | file it, and route to the /review:security-review lane, which owns that surface |
unattended (a loop, a routine, a lane rule that passes --track) |
any | file it; nobody is present to fix it, and the tracker is the only durable output |
"Unattended" is declared by the caller, never inferred from the environment. Recommend the route,
do not auto-invoke it; a finding worth root-causing first goes to /debugging:debug. Nothing
verified: say so plainly and name the lane and rung, so the next run rotates on.
Next
- Verified local finding, interactive run:
/implementation:implement. - Verified non-local or security-relevant finding:
/work-items:track add, then/review:security-reviewfor the security-relevant ones. - A finding that needs root-causing before a fix:
/debugging:debug.
What this skill does NOT do
- Does not edit code itself. No patches, no PRs, from this skill. A verified finding leaves
through the Step 7 ladder, which in an interactive session routes a local fix to the implement lane
in the same session rather than to the tracker. - Does not file on bare invocation. Filing needs
--track. - Does not review a diff. That is
/review:code-review's lane; this reads resting code. - Does not verify factual claims in docs or config against code. All four
codebase-health:audit
dimensions own that. - Does not chase coverage gaps (
testing:audit,mutation-testing:audit), structural drift
(code-tidying:tidy), or comment markers (work-items:scan-todos). - Does not emit
type: review-findingsfrontmatter. That frontmatter alone routes a report into
the/review:fanoutfix relay; a scan report is intake, not a detector-findings artifact.
Gotchas
- The hunter never grades itself. If you collapse Steps 2 and 4 into one agent, precision collapses
with them. The gate is a separate fresh-context dispatch, per candidate. - A cheaper hunter is fine; a cheaper gate is not. The gate's reproduction is the whole precision
claim, so it stays on the gate tier whatever the hunters ran on, and a scope class never lowers
the gate's stance. - The ungated tail is a queue, not an archive. Step 3 reads the prior report's "Candidates not
gated" rows back before it merges; a run that skips the seed re-derives what the last run over
the lane already paid for. - Triage merges and drops, it never confirms. A candidate that survives triage still faces the
gate; a candidate dropped as cosmetic is written to the side observations, not silently gone. - No evidence quote, no finding. A candidate without a verbatim quote of the offending source is
retracted at the gate, not "investigated further". - Refuted is reported, not deleted. The refuted tail is a feature: it lets a human overturn a
wrong kill, and it stops the same dead candidate resurfacing next run. --dry-runmust not advance the cursor. A dry run that persists a cursor silently skips a lane
on the next real run.--trackis not--file.--fileis/bugs:write's flag for persisting a report to
disk; the same token here would mean tracker mutation. This skill uses--trackfor filing.- Shallow clones degrade, they do not fail. The hotspot ranking is skipped with a printed notice
when history is absent; the four lenses read the scope unranked. - A lane with no verified findings is a result. Report it, advance the cursor, do not refill past
the cap looking for something to say. - Consumer
## Gotchasconcatenate after these bullets. The pre-computed Consumer gotchas block
is the cascade## Gotchasfrom.claude/bugs.mdlayers (user-global, team, local overlay) in
layer order. Treat those lines as additional gotchas for this invocation. Missing layers are
absent, not an error. Generalizable lines belong in this shipped skill via an issue, not in the
consumer file.
Reference index. Load on demand
| File | Load when |
|---|---|
| `context/lenses.md` | Step 2, writing each hunter's dispatch prompt; also holds the bundled default lanes. |
| `context/verification-gate.md` | Step 4, before dispatching the refute gate on a candidate. |
| `context/findings-report.md` | Step 5, emitting the report, and the ladder's middle rung reading a prior cursor back. |
| `${CLAUDE_PLUGIN_ROOT}/reference/config.md` | A .claude/bugs.md key decides the run (lane, rotation, filing posture) and its layer is unclear. |
| `${CLAUDE_PLUGIN_ROOT}/skills/write/SKILL.md` | Emitting a finding in the five-field shape, running the duplicate scan, or resolving where --track persists. |