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"
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.
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. 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. 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 4 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 (seecontext/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. Rank by evidence strength and drop the tail rather than widening the wave.
- 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.
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 surface as step 2 describes | 2 | 3 verified findings |
The candidate cap of 10 per wave 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. 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.
Done when you can name the exact file list the hunters will read, and the order they read it in.
Step 2. Dispatch hunters (recall stage)
Dispatch one subagent per lens over the resolved scope, each with the four-part contract:
objective, output format, tool/source guidance, and task boundaries, spelled out in
context/lenses.md. Size the fan-out to the surface: a single small file may
warrant one or two lenses; a full lane warrants all four. Whatever the surface suggests, the
Effort row is the ceiling. Every hunter is read-only, must attach a
verbatim evidence quote to every candidate, and is explicitly told that returning no candidate is a
valid and expected outcome.
Done when every dispatched lens has returned, and the merged candidate list is capped at 10.
Step 3. Verification gate (precision stage)
Dispatch a separate fresh-context subagent per candidate 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.
Done when every candidate carries a verdict and a label or a refuting argument.
Step 4. 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 and lens id, then the refuted tail and, on a rotation run, the cursor metadata block; 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. Under --dry-run, skip persistence entirely.
Done when the report is emitted, and (outside --dry-run) persisted with its path stated.
Step 5. --track filing (explicit only)
Posture gate first. Resolve filing_posture from the config cascade. See
${CLAUDE_PLUGIN_ROOT}/reference/config.md, whose bundled default is
manual-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 presence-gated on work-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-triageon the right axis, resolved from the live label set. Priority axis: pass--priority needs-triageon thetrack addcall, replacing the filing floor (never twopriority:labels). Status axis: apply the status marker as a separate label after creation. Create no labels. If the marker does not exist in the live set, 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 6. Hand off
Recommend, do not auto-invoke:
- A verified finding worth root-causing or fixing →
/debugging:debug. - A finding tagged security-relevant → the
/review:security-reviewlane, which owns that surface. - Nothing verified → say so plainly and name the lane and rung, so the next run rotates on.
What this skill does NOT do
- Does not fix anything. No edits, no patches, no PRs. Even for an obviously trivial defect.
- 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:auditdimensions 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 3 into one agent, precision collapses with them. The gate is a separate fresh-context dispatch, per candidate.
- 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.
Reference index. Load on demand
| File | Load when |
|---|---|
context/lenses.md |
Step 2, sizing the fan-out and writing each hunter's dispatch prompt; also holds the bundled default lanes. |
context/verification-gate.md |
Step 3, before dispatching the refute gate on a candidate. |
context/findings-report.md |
Step 4, 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. |