# Scan

> 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' you already observed is `bugs:write`. Bare invocation neither edits nor files; `--track` files verified findings as raw intake (subject to the team's `filing_posture`).

- Skill: `melodic-software/scan` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add melodic-software/scan`
- Raw SKILL.md: https://api.skillmd.com/api/skills/melodic-software/scan/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: melodic-software (https://skillmd.com/u/melodic-software)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/melodic-software/scan

---


## 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`](../../reference/config.md), which is their single home.
When no layer declares lanes, use the bundled generic default lanes in
[`context/lenses.md`](context/lenses.md).

## Cursor ladder (rotation mode)

Which lane comes next is derived **statelessly**, first rung that answers wins:

1. **Tracker history.** When the `work-items` plugin is installed and a tracker binding resolves,
   search items (`--state all`) for the provenance line `Filed 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".
2. **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 configured `output_dir` silently 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`](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.
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`](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`](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](#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`](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`](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`](../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`](../../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:

1. **Dedupe.** Search items over `--state all` first; 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.
2. **Categorize as raw intake.** A scan filing records what was observed, not a verified diagnosis.
3. **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.
4. **Mark `needs-triage` on the right axis, resolved from the live label set.** Priority axis: pass
   `--priority needs-triage` on the `track add` call, replacing the filing floor (never two
   `priority:` 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: <reason>; 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-review` lane, 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: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-findings` frontmatter.** That frontmatter alone routes a report into
  the `/review:fanout` fix 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-run` must not advance the cursor.** A dry run that persists a cursor silently skips a lane
  on the next real run.
- **`--track` is not `--file`.** `--file` is `/bugs:write`'s flag for persisting a report to
  disk; the same token here would mean tracker mutation. This skill uses `--track` for 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`](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`](context/verification-gate.md) | Step 3, before dispatching the refute gate on a candidate. |
| [`context/findings-report.md`](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`](../../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`](../write/SKILL.md) | Emitting a finding in the five-field shape, running the duplicate scan, or resolving where `--track` persists. |

