Appearance
find-vulns
find-vulns
findreadhands-off
Use this when: you want a security sweep of existing code
Problem it solves — A diff audit never sees the vulnerability already sitting in main. This hunts existing code boundary-by-boundary against the threat model, dedupes findings to root cause, and verifies each through the blind reviewer before it claims anything.
Find vulnerabilities
Divergent discovery: find ways an attacker reaches the assets in existing code that no open issue currently tracks. Not a diff audit — a hunting expedition framed by the threat model.
Before hunting — load the threat model and the lens
Read docs/security/threat-model.md in full. Extract:
- The assets (crown jewels: encrypted credentials, Anthropic key, session, task-store integrity).
- The trust boundaries B1–B8 and their attacker-controlled inputs.
- The won't-flag corpus — consult it before every finding; a corpus match is excluded, not a finding.
- The consciously accepted risks — tracked deferrals, not gaps.
This is the denominator. Every finding is relative to it. Judge every finding by the shared protocol in docs/security/review-lens.md — evidence before severity, the exclusion gate, the severity tiers.
Scope
- Default (working-set): recently-changed files (
git diff --name-only main) plus any area the user names ("find vulns inapp/Http"). --full: the whole codebase. Use deliberately — a full sweep costs more and is meant for pre-go-live or epic-boundary cadence reviews.
Declare the scope at the top of the report.
The hunt — boundary-by-boundary
This is the skill. The goal: find paths where an attacker controlling an input at a trust boundary reaches an asset. Work through each in-scope boundary from the threat model and ask: given what this attacker controls at B-n, what could go wrong in the code that handles it?
Frame as goal + context, not a checklist. The boundary list is the context; the attacker's goal (reach an asset) is the filter. Vuln classes below are illustrative only — anything that connects attacker-controlled input to an asset counts:
- B1 (webhook): HMAC check bypassed, fail-open on error, replay via non-idempotent handler
- B2 (credentials):
$hiddenabsent, encrypted attr serialising into a response/log/push - B3 (LLM I/O): external content interpolated without
@untrustedfence, LLM output trusted for control flow - B4 (MCP tools): write tool reachable without gating, tool args from LLM output not validated
- B5 (
v-html): unfiltered markdown/task text piped throughv-htmlor{!! !!}without DOMPurify - B6 (auth/session): route outside the
auth/verifiedgroup, state-changing action with no server-side check - B7 (Google): OAuth
state/redirect URI, Google-sourced string reaching a prompt unfenced - B8 (supply chain): committed secret, known-CVE dependency — the deterministic CI gate's domain; confirm the gate is green rather than re-derive its checks
Orchestration (opt-in, --full only)
A --full sweep's boundaries are disjoint, so it may fan out per docs/agents/orchestration.md: one cheap-model read-only scout per boundary (its attacker-controlled inputs + the assets + the won't-flag corpus as the brief), candidate lists merged per docs/agents/deterministic-merge.md. Root-cause grouping, severity and the blind-reviewer verify stay in-thread — a scout's candidate is a lead, never a finding. Working-set scope stays in-thread; a handful of files doesn't earn the overhead.
Dedupe by root cause
Ten unfenced interpolations of the same untrusted origin = ONE finding with ten locations, not ten findings. Group by the root-cause gap, not by call-site. This prevents a single systemic gap from monopolising severity budget and misleading triage.
Verify via blind-reviewer
Each candidate finding goes to blind-reviewer in the stricter independence mode docs/security/review-lens.md describes (location + category + code only, your reasoning withheld). Consume its verdicts:
- PASS — confirmed; include in the findings table.
- FAIL — false positive; drop, but note the count in the report ("N candidates dropped after blind review").
- CANNOT_VERIFY — its own bucket, surfaced to the human, never dropped.
Contract
| Item | Value |
|---|---|
| Task class | divergent discovery (find-) |
| Tools | Read, Grep, Glob, Bash, codegraph, blind-reviewer agent |
| Direct vs fork | forks — dispatches blind-reviewer for each candidate finding; --full may fan out per-boundary scouts (above) |
| Verifiable artifact | A verified-findings report (see Output) |
Output
Verified-findings table (one row per root cause):
| Root cause | Boundary | Locations | Reachability / attacker-control / blast-radius | Severity | Verifier verdict | Suggested fix |
|---|
CANNOT_VERIFY bucket (findings blind-reviewer couldn't confirm — surfaced, never dropped):
List each with attempted verification + structural reason it couldn't be tested.
Summary line: N confirmed · M CANNOT_VERIFY · P excluded by corpus · Q dropped (false positive)
Pure-read: this skill reports and proposes. Filing issues or applying fixes happens only after the user confirms (surface-then-confirm; the REPORT itself carries no confirm tax — it's what you see, free of charge).
Severity tiers and the evidence-before-severity discipline: docs/security/review-lens.md.
When it MUST run
Mandatorily before the DigitalOcean go-live (deploy epic #116 / #66) — the threat model's review_cadence requires it. Also at each epic boundary as part of that cadence. Default scope at go-live = --full.
How this gets triggered
Description-driven auto-invocation covers the manual trigger. No hook registration needed — this is a deliberate sweep, not a boundary event.