| name | vuln-scan |
| description | Static source-code vulnerability scan. Reads a target directory (and its threat model — <repo>-threat-model.md, or legacy THREAT_MODEL.md — if present), spawns parallel review subagents per focus area, and writes <repo>-vuln-findings.{json,md} with campaign finding IDs ({REPO_SLUG}-{SHORTSHA}-{NNN}) and the shared severity enum, for /triage to consume. When a <repo>-security-audit.json baseline exists, the scan supplements it: known findings are deduped against the baseline and new verified findings flow back into it via triage and the disposition ledger — /secure-code-audit remains the preferred baseline and /verify-remediation the way to update it after fixes. Read-only — no building, running, or network. Use when asked to "scan for vulns", "review this code for security issues", "find bugs in <dir>", "re-sweep an audited repo", or as the step between /threat-model and /triage. |
| argument-hint | <target-dir> [--baseline <audit.json>] [--focus <area>] [--single] [--extra <file>] [--no-score] |
| user-invocable | true |
| allowed-tools | ["Read","Glob","Grep","Write","Task","Bash(rg:*)","Bash(grep:*)","Bash(ls:*)","Bash(wc:*)","Bash(head:*)","Bash(file:*)","Bash(git:*)","Bash(jq:*)","Bash(python3 *validate_report.py:*)"] |
/vuln-scan
Static vulnerability review of a source tree. Produces
<repo>-vuln-findings.json (+ a human-readable .md) that /triage
ingests directly.
Positioning — how this skill complements the other scanners
/secure-code-audit is the preferred baseline. It is the
comprehensive assessment: multi-framework mappings (ASVS, K8s Top 10,
CIS, STIG, SLSA, Scorecard), severity criteria, LoC accounting, and the
full schema/report.schema.json contract. When no
<repo>-security-audit.json exists for the target and campaign-grade
coverage is wanted, recommend running it instead of (or after) this
skill.
/verify-remediation updates the baseline after fixes. Checking
whether previously reported findings are resolved in patched code is its
job, not this skill's — never re-litigate existing baseline findings
here.
/vuln-scan supplements the baseline between full audits. It is
deliberately lighter and less comprehensive than /secure-code-audit:
a fast sweep for new candidate findings (new code, new release, a
focus area the audit under-covered). When a baseline exists, this skill
dedupes against it and its output is the feeder for updating the
original *-security-audit report — via /triage verification and the
disposition ledger, never by hand (see Step 5).
This skill does not execute code. It reads source and reasons about it.
For execution-verified findings (ASAN crashes, reproducing PoCs), point the
user at the external vuln-pipeline CLI (vuln-pipeline run <target>) —
it is not part of this harness and must be installed separately.
Paths: <skill-base> is this skill's base directory (injected by the
runtime as "Base directory for this skill"; it is
ai-security-harness/harnessing/vuln-scan). <harness> is the
ai-security-harness repo root, i.e. <skill-base>/../... Resolve both to
absolute paths once at startup. Bash git is permitted ONLY for read-only
provenance queries (git -C <harness> rev-parse --short HEAD,
git -C <target-dir> rev-parse --short HEAD,
git -C <target-dir> remote get-url origin) — never to modify anything.
Tool fallbacks. Prefer the dedicated Glob and Grep tools. Some sessions
do not provision them — allowed-tools is a permission filter, not a loader,
so listing them here does not make them appear. When Glob/Grep are
unavailable, fall back to the read-only Bash commands whitelisted above:
rg --files <scope> / ls -R for enumeration, rg -n / grep -rn for
search, wc / head / file for sniffing, jq for reading the baseline
JSON. These are the ONLY permitted Bash commands; do not write helper
scripts or pipe target content into a shell interpreter.
Arguments
<target-dir> (required) — directory to scan. Relative or absolute.
--baseline <audit.json> — explicit path to the target's existing
<repo>-security-audit.json (overrides auto-discovery, Step 1.3).
--focus <area> — scan only this focus area (repeatable). Skips recon.
--single — no subagent fan-out; one sequential pass. Use on tiny targets
or when debugging the prompt.
--extra <file> — append the contents of <file> to the review brief
(after the category list). Use to add org-specific vulnerability classes,
compliance checks, or stack-specific patterns. Plain text; same shape as
the category blocks below.
--no-score — skip the Step 3b confidence pass (saves a round of
subagents). Findings keep the scanner's self-reported confidence only.
Step 1 — Scope
- Resolve
<target-dir>. If it doesn't exist or has no source files, stop
with an error. Derive identity for IDs and filenames:
<repo>: repo name from git -C <target-dir> remote get-url origin
(basename, .git stripped), falling back to <target-dir>'s
directory basename — the same rule /threat-model uses.
REPO_SLUG: <repo> uppercased, every character outside [A-Z0-9]
replaced with _, truncated to 24 chars.
SHORTSHA: git -C <target-dir> rev-parse --short=7 HEAD; if the
target is not a git checkout, use 0000000 and say so in the output
metadata.
- Look for the target's threat model, resolving per the
/threat-model
skill's naming rule: exactly one *-threat-model.md in <target-dir>,
else the legacy <target-dir>/THREAT_MODEL.md. If present, parse its
section 3 "Entry points & trust boundaries" table and section 4 "Threats"
table for focus areas and threat classes. This is the preferred scoping
input.
- Locate the baseline audit, in order: the
--baseline argument; then
<target-dir>/<repo>-security-audit.json; then (when the harness runs
inside the campaign workspace)
analysis-results/findings/**/<repo>/<repo>-security-audit.json. If
found, Read its findings[] (id, title, locations, cwes/category,
severity) into a baseline table for Step 3 dedupe, and record the
baseline path + its metadata.harness_version for the output metadata.
If none is found, note that the target has no audit baseline and
recommend /secure-code-audit for campaign-grade coverage in the Step 5
hand-back.
- If no threat model and no
--focus: do a quick recon — list the
source tree, read entry points and dispatch code, and propose 3-10 focus
areas using the pattern <subsystem> (<function/file>) — <key operations>.
- If
--focus was given, use exactly those.
Tell the user the focus areas you'll scan, the source-file count, and
whether a baseline audit was found (path + finding count) before fanning
out.
Step 2 — Fan out
Unless --single, spawn one Task subagent per focus area in parallel.
Cap at 10 concurrent. Each subagent gets the review brief below with its
focus area filled in. On tiny targets (<15 source files), fall through to
--single automatically.
Review brief (per subagent)
You are conducting authorized static security review of source code. Your
focus area: **{focus_area}**. Other agents cover other areas; duplication
is wasted effort.
TARGET: {target_dir}
TRUST BOUNDARY: {from the threat model's section 3, or "untrusted input → process memory"}
TASK: read the source in your focus area and identify candidate
vulnerabilities. This is static review — do NOT build, run, or probe
anything. Reason from the code.
REPORTING BAR: report anything with a plausible exploit path. Skip style
concerns, best-practice gaps, and purely theoretical issues with no attack
story at all — but if you're unsure whether something is real, REPORT IT
with a low confidence score rather than dropping it. A downstream triage
step does the rigorous verification; your job is to not miss things.
WHAT TO LOOK FOR:
MEMORY SAFETY (C/C++ and unsafe/FFI blocks) — HIGH VALUE:
- heap-buffer-overflow / stack-buffer-overflow / global-buffer-overflow
- heap-use-after-free / double-free
- integer overflow feeding an allocation or index
- format-string bugs
- unbounded recursion or allocation driven by untrusted size fields
INJECTION & CODE EXECUTION — HIGH VALUE:
- SQL / command / LDAP / XPath / NoSQL / template injection
- path traversal in file operations
- unsafe deserialization (pickle, YAML, native), eval injection
- XSS (reflected, stored, DOM-based) — but see React/Angular note below
AUTH, CRYPTO, DATA — HIGH VALUE:
- authentication or authorization bypass, privilege escalation
- TOCTOU on a security check
- hardcoded secrets, weak crypto, broken cert validation
- sensitive data (secrets, PII) in logs or error responses
LOW VALUE — note briefly, keep looking:
- null-pointer deref at small fixed offsets with no attacker control
- assertion failures / clean error returns (correct handling, not a bug)
DO NOT REPORT (common false positives — skip even if technically present):
- volumetric DoS / rate-limiting / resource-exhaustion — BUT unbounded
recursion, algorithmic-complexity blowup, or ReDoS driven by untrusted
input ARE reportable
- memory-safety findings in memory-safe languages outside unsafe/FFI
- XSS in React/Angular/Vue unless via dangerouslySetInnerHTML,
bypassSecurityTrustHtml, v-html, or equivalent raw-HTML escape hatch
- findings in test files, fixtures, build scripts, docs, or .ipynb
- missing hardening / best-practice gaps with no concrete exploit
- env vars and CLI flags as the attack vector (operator-controlled)
- regex injection, log spoofing, open redirect, missing audit logs
- outdated third-party dependency versions
{if --extra <file> was given: append its contents here verbatim}
{if a baseline audit was found: "ALREADY IN THE BASELINE AUDIT (do not
re-report; finding something ADJACENT to one of these is reportable, the
same issue at the same site is not):" followed by one line per baseline
finding in your focus area: "{id}: {title} ({file}:{lines})"}
For each finding you DO report, trace: where does the untrusted input
enter, what path reaches the sink, and what condition triggers it.
OUTPUT — one block per finding, nothing else:
<finding>
<id>F-{focus_idx:02d}-{n:02d}</id>
<file>{relative/path}</file>
<line>{line_number}</line>
<category>{heap-buffer-overflow | use-after-free | integer-overflow | sql-injection | command-injection | path-traversal | deserialization | xss | auth-bypass | hardcoded-secret | ...}</category>
<cwe>{primary CWE for the category, e.g. CWE-122; omit only if genuinely none fits}</cwe>
<severity>{critical | high | medium | low | informational}</severity>
<confidence>{0.0-1.0}</confidence>
<title>{one line}</title>
<description>{root cause, attacker control, trigger condition, data flow from entry to sink. Cite line numbers.}</description>
<exploit_scenario>{concrete attack: what input, from where, causing what outcome}</exploit_scenario>
<recommendation>{specific fix: parameterize the query, bounds-check before memcpy, etc.}</recommendation>
</finding>
SEVERITY — use the harness's shared five-level scale (same enum as
secure-code-audit and triage):
critical = exploitable by an unauthenticated or low-privilege
attacker with severe impact: RCE, cluster/host
compromise, secrets disclosure, cross-tenant access
high = directly exploitable with significant impact, or
critical impact gated by one realistic precondition
(authenticated user, non-default-but-common config)
medium = exploitable only under specific conditions, or
significant impact requiring privileged position
low = defense-in-depth gap or limited-impact issue with a
concrete but weak attack story
informational = no direct security impact; worth recording (e.g.
dangerous pattern currently unreachable)
If you find nothing reportable in your area after a thorough read, emit a
single <finding> with category=none and a one-line note of what you covered.
Step 3 — Collate
- Collect
<finding> blocks from all subagents. Drop category=none
placeholders.
- Baseline dedupe (when a baseline audit was found) — a candidate that
matches a baseline finding (same file and same weakness class, at or
near the same lines) is known, not new: remove it from the findings
list and record it as
{candidate title, matched baseline id} in a
known_findings list. The baseline audit already owns that finding;
re-reporting it would fork its history. When in doubt (same file,
related but distinct flaw), keep the candidate and note the nearest
baseline id in its description.
- Light dedupe within the scan — if two candidates cite the same
file:line with the same category, keep the one with the longer
description and note the duplicate. (Heavy dedupe is /triage's job;
don't over-engineer here.)
- Assign campaign IDs
{REPO_SLUG}-{SHORTSHA}-{NNN} (the same canonical
scheme as secure-code-audit; see docs/disposition-ledger.md
"Finding IDs"): NNN is 001-based in (severity desc, file, line) order.
SHORTSHA is the scanned commit, so IDs from different sweeps never
collide with each other or with the baseline. Keep each subagent's
working id (F-xx-yy) in a scanner_ref field for traceability.
Step 3b — Confidence pass (skip if --no-score)
A cheap second-opinion read that ranks findings by signal quality.
Nothing is dropped — this pass calibrates confidence so humans and
/triage see high-signal findings first. Spawn one Task subagent per
finding in parallel with the brief below. Shallow: re-read and score, not
a full reachability trace.
Scoring brief (per finding)
You are giving ONE candidate security finding an independent confidence
score. You are NOT deciding whether to keep it — every finding is kept.
You are deciding how likely it is to survive rigorous triage.
FINDING:
{the full <finding> block}
TARGET: {target_dir} (you may Read/Grep inside it; do NOT execute)
STEP 1 — Re-read the cited code. Open {file} around line {line}. Does the
code actually do what the description claims?
STEP 2 — Check against common false-positive patterns (volumetric DoS,
memory-safe language, test/fixture/doc file, framework auto-escape, env-var
vector, missing-hardening-only, regex/log injection, outdated dep). A match
lowers confidence sharply but does not auto-zero it.
STEP 3 — Score 1-10 that this is a real, actionable vulnerability:
1-3 likely false positive or noise
4-5 plausible but speculative
6-7 credible, needs investigation
8-10 high confidence, clear pattern
OUTPUT (exactly this, nothing else):
CONFIDENCE: <1-10>
REASON: <one line>
Resolve: overwrite each finding's confidence with the score
(normalized to 0.0-1.0) and attach confidence_reason. Re-sort findings
by (confidence desc, severity desc, file, line) and reassign the
NNN sequence in that order so -001 is the highest-signal finding.
Compute low_confidence_count = findings with confidence < 0.4, for the
summary line.
Step 4 — Write output
Write both files to <target-dir>/, named after the target (same
convention as <repo>-security-audit.{json,md} and
<repo>-threat-model.md):
<repo>-vuln-findings.json — the /triage ingest shape:
{
"target": "<target-dir>",
"scanned_at": "<iso8601>",
"focus_areas": ["..."],
"metadata": {
"repo": "<repo>",
"repo_slug": "<REPO_SLUG>",
"scanned_ref": "<SHORTSHA or '0000000 (not a git checkout)'>",
"harness_version": "<VERSION file semver>-<harness git short SHA>",
"baseline": "<path to the matched *-security-audit.json, or null>",
"baseline_findings": 0
},
"findings": [
{
"id": "EXAMPLE_REPO-abc1234-001",
"scanner_ref": "F-01-02",
"file": "relative/path.c",
"line": 123,
"category": "heap-buffer-overflow",
"cwe": "CWE-122",
"severity": "high",
"confidence": 0.9,
"title": "...",
"description": "...",
"exploit_scenario": "...",
"recommendation": "...",
"confidence_reason": "..."
}
],
"known_findings": [
{"title": "...", "matches_baseline_id": "EXAMPLE_REPO-9cb7556-004"}
],
"summary": {
"total": 0,
"critical": 0, "high": 0, "medium": 0, "low": 0, "informational": 0,
"known": 0, "low_confidence": 0
}
}
Findings are sorted by confidence desc (then severity, file, line), so
the top of the file is the highest-signal material.
Validation gate. The scan is not complete until the deterministic
validator passes (schema/vuln-findings.schema.json is auto-detected from
the filename; it machine-enforces id derivation from
repo_slug/scanned_ref, summary-count reconciliation, and baseline
bookkeeping):
python3 <harness>/scripts/validate_report.py <target-dir>/<repo>-vuln-findings.json
Fix every ERROR it reports and re-run until it passes with 0 errors —
the same discipline as validate_report.py for audit and triage
artifacts.
<repo>-vuln-findings.md — human-readable: a header naming the target,
scanned ref, harness version, and baseline (or "no baseline — run
/secure-code-audit for campaign-grade coverage"); a summary table
(id | severity | category | file:line | title); one ### <id> section per
finding with the full description; and, when a baseline was found, an
"Already in baseline" table of the known_findings.
Step 5 — Hand back
Tell the user:
- Counts: N new candidates (per-severity split, X low-confidence),
K known findings deduped against the baseline, across F focus areas
and M source files.
- Top 3 by confidence, one line each.
- Next steps — the update path for the baseline:
- Verify:
> /triage <target-dir>/<repo>-vuln-findings.json --repo <target-dir>
- Fold verified findings into the baseline: triage-confirmed true
positives are appended to the original
<repo>-security-audit.json
as new findings, transcribed to the schema/report.schema.json
finding shape — keep this scan's campaign id; cwe → cwes[]
(CWE-\d{1,5}, at least one entry); file/line →
locations: [{path, lines}]; recommendation → remediation;
validation_status: not_verified (a machine triage verdict never
sets confirmed — that takes execution evidence or a human
reviewer, and the verdict itself flows through the ledger); set
source_findings to the scan report path. Re-run
scripts/validate_report.py on the amended report until it passes —
the same follow-up-finding flow /fuzz-harnesses uses. Status
changes and refutations flow through the disposition ledger
(scripts/emit_triage_ledger_events.py → /track-findings) —
never hand-edit validation_status on existing baseline
findings.
- No baseline? Recommend
/secure-code-audit <target> to establish
one; this scan's report then serves as a leads file for it.
- Remind: to check whether existing baseline findings are fixed,
use
/verify-remediation — that is the tool for updating the
baseline after remediation, not a re-scan.
- Remind: these are static candidates (
claimed, in
findings-lifecycle terms), not verified. For execution-verified
crashes, the external vuln-pipeline run <target> (separate install;
not part of this harness).
Constraints
- Never execute target code. No builds, no
docker, no network, no
Bash against <target-dir> beyond the read-only commands whitelisted
above. If the user asks you to "reproduce" or "confirm with a PoC,"
decline and point at the harness's validate-findings skill (live
authorized targets) or the external vuln-pipeline CLI (offline ASAN
reproduction).
- Never modify the baseline audit. This skill reads
<repo>-security-audit.json for dedupe only. Appending verified
findings to it happens after /triage, and disposition changes go
through the append-only ledger — a scan must not rewrite the
portfolio's memory.
- Don't fabricate line numbers. Every
file:line you emit must be
something you Read or Grep'd. If unsure of the exact line, cite the
function and say so in the description.
- Stay in
<target-dir> (plus the read-only baseline path). Don't
follow symlinks or .. out of it.
- Findings are candidates for
/triage, not final verdicts. This skill
never drops a finding — Step 3b only ranks and Step 3 dedupe only
reroutes known ones into known_findings. /triage does the rigorous
N-vote verification and is where false positives actually get removed.
Provenance
Derivative work of the vuln-scan skill in
defending-code-reference-harness (Copyright 2026 Anthropic PBC,
Apache License 2.0 — see the harness root NOTICE and
LICENSES/Apache-2.0.txt), ported into this harness at v0.37.0 and
modified per CHANGELOG.md (campaign IDs, shared severity enum, baseline
supplement flow, and <repo>-vuln-findings.* naming added at v0.38.0).
The focus-area recon pattern and memory-safety quality tiers originate in
that project's harness/prompts/find_prompt.py and recon_prompt.py; the
broader category menu, DO-NOT-REPORT exclusions, per-finding confidence
pass, and exploit_scenario/recommendation output fields are adapted
from
anthropics/claude-code-security-review's
/security-review command (MIT — see
LICENSES/MIT-claude-code-security-review.txt).