Security assessment
Four-phase security assessment pipeline: (1) Threat Model — interview or bootstrap mode to identify attack surfaces, threats, and mitigations; (2) Vulnerability Scan — static source-code scan spawning parallel subagents per focus area, producing VULN-FINDINGS.json; (3) Triage — state machine to drive findings through triage roles, create issues, prioritize; (4) Patch — generate candidate fixes consuming TRIAGE.json or VULN-FINDINGS.json. Incorporates former: threat-model, vuln-scan, triage, patch.From its SKILL.md
npx -y skills add asong56/skills --skill security-assessmentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 21 days oldThe repository was created 21 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
46.0 KB, ~11.8k tokens by cl100k_base, as published. Nobody here has run it
Security Assessment Pipeline
Sequential four-phase security workflow. Each phase produces the artifact the next phase consumes.
Phase 1 — Threat Model
threat-model
A threat model answers "what could go wrong with this system, who would do it, and what should we do about it?" independently of whether any specific bug has been found yet. It is the map; vulnerability discovery is the metal detector. A good threat model tells the pipeline where to look and tells triage which findings matter.
Litmus test: If patching one line of code makes an entry disappear, it was
a vulnerability, not a threat. A threat ("attacker achieves RCE via untrusted
media parsing") still stands after every known bug is fixed; a vulnerability
("dr_wav.h:412 doesn't bounds-check chunk_size") does not. This skill
produces threats. Vulnerabilities appear only as evidence that raises a
threat's likelihood score.
Invocation: /threat-model [bootstrap-then-interview|bootstrap|interview] <target-dir> [flags]
Step 0 — Safety preamble (always runs first)
This skill performs static analysis only. It reads source, git history,
and any vulnerability reports the user supplies, and writes a single output
file (<target-dir>/THREAT_MODEL.md). It does not build, execute, fuzz, or
modify the target, and does not make network requests against the target's
infrastructure.
Before proceeding, confirm and state in your first response:
- The target directory exists and is a local checkout you can read.
- You will not execute any code from the target directory.
- If
--vulnspoints at a URL or you are asked to "fetch CVEs", you will query only public advisory databases (NVD, GitHub Security Advisories, the project's own issue tracker) and never the target's live deployment.
If the user asks you to validate a threat by running an exploit, decline and
point them at the vuln-pipeline (README Step 2) instead.
Step 1 — Route to a mode
Parse $ARGUMENTS:
| First token | Route to |
|---|---|
interview | Read interview.md in this directory and follow it. |
bootstrap | Read bootstrap.md in this directory and follow it. |
bootstrap-then-interview | Bootstrap first, then interview seeded from the draft. See below. |
| anything else, or empty | Ask the user: "Is someone who owns or built this system available to answer questions in this session?" Yes and the codebase is checked out → recommend bootstrap-then-interview. Yes but no codebase → interview.md. No → bootstrap.md. |
All modes write the same artifact (THREAT_MODEL.md, schema in schema.md)
so downstream consumers (pipeline recon/judge, verifier agents) do not need
to know which mode produced it.
interview | bootstrap | |
|---|---|---|
| Needs | An application owner present in the session | A local checkout; optionally past vulns |
| Method | Four-question framework: conversational walk through what are we working on → what can go wrong → what are we going to do about it → did we do a good job | Five stages: parallel research swarm → synthesize sections 1-3 + vuln table → generalize vulns into threat classes → STRIDE gap-fill → emit |
| Best for | New systems, design reviews, systems where the risk lives in business logic the code doesn't show | Inherited systems, third-party code, OSS dependencies, anything with a CVE history |
| Provenance tag | interview | bootstrap |
Context durability. Interview mode is multi-turn; tool results from early reads may be evicted before you need them. To stay resilient:
- Do not read
interview.mdorbootstrap.mdin full up front. Read the mode file (or the relevant section of it) at the point you need it, one question or stage at a time. - If a re-read via the Read tool is refused as "file unchanged", the prior
result was evicted; reload with
cat <path>via Bash instead.
Interview backbone (so you can proceed even if interview.md is
unavailable mid-session):
| Q | Question | Fills schema sections |
|---|---|---|
| Q1 | What are we working on? | section 1 context, section 2 assets, section 3 entry points |
| Q2 | What can go wrong? | section 4 threat rows (id, threat, actor, surface, asset) |
| Q3 | What are we going to do about it? | section 4 impact/likelihood/status/controls; section 5 deprioritized; section 8 recommended mitigations |
| Q4 | Did we do a good job? | validate ranking, coverage check, section 6 open questions |
bootstrap-then-interview mode
When the owner is available and the codebase is checked out, this is the recommended path: the owner's time goes to refining a code-grounded draft instead of describing the system from scratch.
- Tell the owner: "I'll read the code first and come back with a draft
(about 5-10 min), then we'll walk it together. Want that, or would you
rather start cold?" Only proceed if they opt in; otherwise fall back to
interview.md. - Read
bootstrap.mdand follow it end-to-end. Write<target-dir>/THREAT_MODEL.md. - Immediately continue into interview mode: read
interview.mdand follow it with--seed <target-dir>/THREAT_MODEL.mdin effect. The section 6 open questions from bootstrap become your Q1-Q4 prompts; the owner confirms, corrects, and adds rather than starting from nothing. - Overwrite
<target-dir>/THREAT_MODEL.mdwith the refined model. Set provenancemode: bootstrap-then-interview.
The same flow is available manually: run bootstrap first, then
interview --seed <THREAT_MODEL.md> in a later session.
Step 2 — Shared output contract
All modes MUST emit <target-dir>/THREAT_MODEL.md conforming to schema.md
in this directory. Read schema.md immediately before you write the file,
not at routing time; in interview mode the gap between routing and emit can be
many turns, and an early read will be evicted before it's used.
After writing the file, print to the user:
- The path to
THREAT_MODEL.md. - The top 5 threats by likelihood × impact (id, one-line description, L×I).
- For
bootstrap: any open questions the code could not answer (these seed a laterinterviewpass). - For
interview: any owner statements that could not be verified in code (these seed follow-up code review).
References
- docs/security.md and docs/prompting.md for the engagement-context and authorization framing this skill inherits.
Phase 2 — Vulnerability Scan
/vuln-scan
Static vulnerability review of a source tree. Produces VULN-FINDINGS.json
(+ a human-readable .md) that /triage ingests directly.
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 vuln-pipeline run <target> — see README Step 2.
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. 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.--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. - Look for
<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. - If no THREAT_MODEL.md 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>. Same shape asharness/prompts/recon_prompt.py. - If
--focuswas given, use exactly those.
Tell the user the focus areas you'll scan and the source-file 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 THREAT_MODEL.md 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}
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>
<severity>{HIGH | MEDIUM | LOW}</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: HIGH = directly exploitable → RCE, data breach, auth bypass.
MEDIUM = significant impact under specific conditions. LOW = defense-in-
depth.
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. Dropcategory=noneplaceholders. - Light dedupe — if two findings cite the same
file:linewith the same category, keep the one with the longer description and note the duplicate id. (Heavy dedupe is/triage's job; don't over-engineer here.) - Assign stable ids
F-001,F-002, ... in (severity desc, file, line) order.
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 ids
F-001.. in that order. Compute low_confidence_count = findings with
confidence < 0.4, for the summary line.
Step 4 — Write output
Write both files to <target-dir>/:
VULN-FINDINGS.json — the /triage ingest shape:
{
"target": "<target-dir>",
"scanned_at": "<iso8601>",
"focus_areas": ["..."],
"findings": [
{
"id": "F-001",
"file": "relative/path.c",
"line": 123,
"category": "heap-buffer-overflow",
"severity": "HIGH",
"confidence": 0.9,
"title": "...",
"description": "...",
"exploit_scenario": "...",
"recommendation": "...",
"confidence_reason": "..."
}
],
"summary": {"total": 0, "high": 0, "medium": 0, "low": 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.
VULN-FINDINGS.md — human-readable: a summary table (id | severity |
category | file:line | title), then one ### F-NNN section per finding with
the full description.
Step 5 — Hand back
Tell the user:
- Counts: N findings (H/M/L split, X low-confidence), across K focus areas, from M source files.
- Top 3 by confidence, one line each.
- Next step:
> /triage <target-dir>/VULN-FINDINGS.json --repo <target-dir> - Remind: these are static candidates, not verified. For
execution-verified crashes,
vuln-pipeline run <target>(README Step 2).
Constraints
- Never execute target code. No Bash, no builds, no
docker, no network. If the user asks you to "reproduce" or "confirm with a PoC," decline and point atvuln-pipeline. - Don't fabricate line numbers. Every
file:lineyou 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>. 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./triagedoes the rigorous N-vote verification and is where false positives actually get removed.
Provenance
The focus-area recon pattern and memory-safety quality tiers are lifted
from this repo's own harness/prompts/find_prompt.py and
harness/prompts/recon_prompt.py — the same logic the autonomous pipeline
uses, applied statically. 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.
Phase 3 — Triage
Triage
Move issues on the project issue tracker through a small state machine of triage roles.
Every comment or issue posted to the issue tracker during triage must start with this disclaimer:
> *This was generated by AI during triage.*
Reference docs
- AGENT-BRIEF.md — how to write durable agent briefs
- OUT-OF-SCOPE.md — how the
.out-of-scope/knowledge base works
Roles
Two category roles:
bug— something is brokenenhancement— new feature or improvement
Five state roles:
needs-triage— maintainer needs to evaluateneeds-info— waiting on reporter for more informationready-for-agent— fully specified, ready for an AFK agentready-for-human— needs human implementationwontfix— will not be actioned
Every triaged issue should carry exactly one category role and one state role. If state roles conflict, flag it and ask the maintainer before doing anything else.
These are canonical role names — the actual label strings used in the issue tracker may differ. The mapping should have been provided to you - run /setup-matt-pocock-skills if not.
State transitions: an unlabeled issue normally goes to needs-triage first; from there it moves to needs-info, ready-for-agent, ready-for-human, or wontfix. needs-info returns to needs-triage once the reporter replies. The maintainer can override at any time — flag transitions that look unusual and ask before proceeding.
Invocation
The maintainer invokes /triage and describes what they want in natural language. Interpret the request and act. Examples:
- "Show me anything that needs my attention"
- "Let's look at #42"
- "Move #42 to ready-for-agent"
- "What's ready for agents to pick up?"
Show what needs attention
Query the issue tracker and present three buckets, oldest first:
- Unlabeled — never triaged.
needs-triage— evaluation in progress.needs-infowith reporter activity since the last triage notes — needs re-evaluation.
Show counts and a one-line summary per issue. Let the maintainer pick.
Triage a specific issue
-
Gather context. Read the full issue (body, comments, labels, reporter, dates). Parse any prior triage notes so you don't re-ask resolved questions. Explore the codebase using the project's domain glossary, respecting ADRs in the area. Read
.out-of-scope/*.mdand surface any prior rejection that resembles this issue. -
Recommend. Tell the maintainer your category and state recommendation with reasoning, plus a brief codebase summary relevant to the issue. Wait for direction.
-
Reproduce (bugs only). Before any grilling, attempt reproduction: read the reporter's steps, trace the relevant code, run tests or commands. Report what happened — successful repro with code path, failed repro, or insufficient detail (a strong
needs-infosignal). A confirmed repro makes a much stronger agent brief. -
Grill (if needed). If the issue needs fleshing out, run a
/grill-with-docssession. -
Apply the outcome:
ready-for-agent— post an agent brief comment (AGENT-BRIEF.md).ready-for-human— same structure as an agent brief, but note why it can't be delegated (judgment calls, external access, design decisions, manual testing).needs-info— post triage notes (template below).wontfix(bug) — polite explanation, then close.wontfix(enhancement) — write to.out-of-scope/, link to it from a comment, then close (OUT-OF-SCOPE.md).needs-triage— apply the role. Optional comment if there's partial progress.
Quick state override
If the maintainer says "move #42 to ready-for-agent", trust them and apply the role directly. Confirm what you're about to do (role changes, comment, close), then act. Skip grilling. If moving to ready-for-agent without a grilling session, ask whether they want to write an agent brief.
Needs-info template
## Triage Notes
**What we've established so far:**
- point 1
- point 2
**What we still need from you (@reporter):**
- question 1
- question 2
Capture everything resolved during grilling under "established so far" so the work isn't lost. Questions must be specific and actionable, not "please provide more info".
Resuming a previous session
If prior triage notes exist on the issue, read them, check whether the reporter has answered any outstanding questions, and present an updated picture before continuing. Don't re-ask resolved questions.
Phase 4 — Patch
patch
Third leg of the static pipeline (/vuln-scan → /triage → /patch).
Turns a ranked list of verified findings into candidate diffs.
The skill never applies a diff to the target repo. Output is inert text
in ./PATCHES/ for a human to review and apply out-of-band — see
docs/patching.md#reviewing-generated-patches. There is no --apply or
--approve flag by design: the capability isn't present, so it can't be
prompt-injected into use.
Invoke with /patch <findings-path> [--repo PATH] [--top N] [--id fNNN] [--model M] [--fresh].
Arguments (parse from $ARGUMENTS):
- findings path (first positional, required):
TRIAGE.json,VULN-FINDINGS.json, a pipelineresults/<target>/<ts>/directory, or any JSON the/triageingest table recognizes. --repo PATH: target codebase, read-only (default cwd). Required for static mode; the skill stops if cited files don't resolve under it.--top N: patch only the N highest-severity true positives (static mode).--id fNNN: patch only the finding with this id.--model M: passed through tovuln-pipeline patchin execution-verified mode. Ignored in static mode (subagents inherit the orchestrator's model).--fresh: ignore./.patch-state/checkpoint and start over.
Tools. Prefer Read, Glob, Grep, Write, Task. Some sessions do not
provision Glob or Grep; allowed-tools is a permission filter, not a loader.
When they are unavailable, fall back to the read-only Bash commands
whitelisted above: rg/grep for search, ls for enumeration,
head/file/wc for sniffing, jq for JSON ingest. Bash is otherwise
permitted only for python3 .claude/skills/_lib/checkpoint.py (state I/O)
and vuln-pipeline patch (execution-verified delegate). find is NOT
permitted.
Write scope. The Write tool may target ONLY paths under ./PATCHES/ and
./.patch-state/. Never write into --repo, never git apply, never
patch, never edit target source. If a step seems to require it, the step is
wrong.
Checkpointing (runs before Phase 0 and after every phase)
State persists to ./.patch-state/ so a fresh /patch session resumes
without re-spawning patch or reviewer subagents. All checkpoint I/O goes
through python3 .claude/skills/_lib/checkpoint.py (atomic, JSON-validated).
The Write→--from pattern keeps repo-derived bytes out of Bash argv; never
pass payload via heredoc or stdin.
State files: progress.json (single source of truth: {"status": "running"|"complete", "phase_done": N, "shards_done": [...]}),
phaseN.json, _chunk.tmp.
Start of run. Bash:
python3 .claude/skills/_lib/checkpoint.py load ./.patch-state
status == "absent"OR"complete", OR--freshin$ARGUMENTS→ fresh start. Bash:python3 .claude/skills/_lib/checkpoint.py reset ./.patch-state, proceed to Phase 0.status == "running"withphase_done == N→ resume. Readphase0.json..phaseN.jsonin order (and anyshard_*.jsonlisted inshards_done), merge into working state, printResuming from checkpoint: Phase N complete, skip to Phase N+1. Do not re-spawn any subagent whose output is already checkpointed.
End of every phase N. Write tool → ./.patch-state/_chunk.tmp with the
phase's JSON, then Bash:
python3 .claude/skills/_lib/checkpoint.py save ./.patch-state <N> <name> --from ./.patch-state/_chunk.tmp
End of run. After writing PATCHES.md and PATCHES.json, Bash:
python3 .claude/skills/_lib/checkpoint.py done ./.patch-state 4
Phase 0: Parse arguments and detect mode
0a. Parse $ARGUMENTS
Extract findings path (first positional), --repo (default .), --top,
--id, --model, --fresh. If no findings path, stop and ask.
0b. Detect mode
Inspect the findings path:
- execution-verified mode when the path is a directory containing
reports/manifest.jsonlORfound_bugs.jsonlORrun_*/result.json(pipeline output). The findings have PoC bytes + ASAN traces + reproduction commands; the pipeline's verification ladder applies. - static mode otherwise:
TRIAGE.json,VULN-FINDINGS.json, generic finding JSON, or markdown. No PoC; the oracle is a fresh-context reviewer.
Record mode in working state. The two modes share Phase 1 ingest then fork
at Phase 2.
Checkpoint: Write tool → ./.patch-state/_chunk.tmp:
{"phase": 0, "mode": "exec"|"static", "args": {repo, top, id, model, findings_path}}
Then Bash:
python3 .claude/skills/_lib/checkpoint.py save ./.patch-state 0 mode --from ./.patch-state/_chunk.tmp
Phase 1: Ingest and normalize
Same input contract as /triage Phase 1. Normalize every input format to a
flat findings[] of dicts. Pull what's present; never guess what's absent.
1a. Recognized containers (priority order)
TRIAGE.json— read.findings[]. Filter toverdict == "true_positive". This is the canonical input: already verified, deduped, ranked, owner-tagged.VULN-FINDINGS.json— read.findings[]. Unverified; printWarning: VULN-FINDINGS.json is unverified scanner output. Consider /triage first.and continue.- Pipeline results directory — one finding per
reports/bug_NN/. Mapreport.json→description,crash.crash_type→category, ASAN top-frame →file/line. Recordbug_id = NNfor the--bug Ndelegate flag. - Generic
*.jsonwith a top-level list or afindings/results/issues/vulnerabilitiesarray.
1b. Field aliases (canonical ← also-accept)
| Canonical | Also accept |
|---|---|
file | path, location.file, filename |
line | line_number, location.line, lineno |
category | type, cwe, rule_id, crash_type |
severity | severity_rating, level, priority |
title | name, summary, message |
description | details, report, body, evidence, rationale |
recommendation | fix, remediation, mitigation |
owner_hint | owner, component |
Attach id (f001, f002, ... in ingest order; preserve existing ids from
TRIAGE.json) and source (relative path of the file it came from).
1c. Filter and order
- If
--id fNNN: keep only that finding. - If
--top N(static mode): sort byseverityHIGH > MEDIUM > LOW thenconfidencedesc, keep the first N. - Drop findings with no
file(cannot patch what cannot be located). Record them asskippedwith reason"no source location".
1d. Locate the target codebase (static mode)
Resolve --repo. For the first 5 findings with a file, check the path
resolves under repo (try as-given, then with common prefixes stripped). If
none resolve, stop: tell the user the cited files aren't reachable and
suggest a --repo value.
Checkpoint: Write tool → ./.patch-state/_chunk.tmp:
{"phase": 1, "mode": ..., "findings": [...], "skipped": [...], "repo": ...}
Then Bash:
python3 .claude/skills/_lib/checkpoint.py save ./.patch-state 1 ingest --from ./.patch-state/_chunk.tmp
Phase 2: Generate patches
Forks on mode.
2A. Execution-verified mode — delegate to the pipeline
The pipeline already implements the build → reproduce → regress → re-attack ladder with executable oracles. Do not reimplement it.
For each finding (or once for the whole directory if no --id/--top
filter), Bash:
vuln-pipeline patch <findings_path> --model <--model arg> [--bug <bug_id>]
The pipeline writes <findings_path>/reports/bug_NN/{patch.diff, patch_result.json} itself. After it returns, Read each patch_result.json
and copy verdict + rationale into working state. Set
verified: "ladder_passed" when verdict.passed == true, else
verified: "ladder_failed".
If the CLI exits non-zero (no build_command, missing target config), record
the stderr as the finding's error and continue with remaining findings.
Skip Phase 3 (the ladder is the verifier). Proceed to Phase 4.
Checkpoint per finding: Write tool → ./.patch-state/_chunk.tmp =
{"id": ..., "verified": ..., "verdict": ..., "diff_path": ...}, then Bash:
python3 .claude/skills/_lib/checkpoint.py shard ./.patch-state <id> --from ./.patch-state/_chunk.tmp.
After all findings, write the consolidated phase payload to _chunk.tmp then:
python3 .claude/skills/_lib/checkpoint.py save ./.patch-state 2 generate --from ./.patch-state/_chunk.tmp
2B. Static mode — one patch subagent per finding
One Task per finding, all in a SINGLE assistant message for parallel
execution. subagent_type: "general-purpose". Never set
run_in_background — you need the diff text, not an async handle.
Each subagent has read-only access to --repo. It cannot modify the target;
it emits the diff as text in its response. The orchestrator writes that text
to PATCHES/bug_NN/patch.diff.
Patch subagent prompt (assemble once, reuse per finding)
You are conducting authorized security research as part of a defensive
assessment. Your task: write a candidate fix for ONE verified vulnerability
finding in a codebase you have read-only access to.
You may use Read, Glob, and Grep ONLY on paths inside {REPO_PATH}. You may
NOT build, run, install, edit files on disk, or reach the network. You will
emit the fix as a unified diff in your final response; you will NOT apply it.
────────────────────────────────────────────────────────────────────────
FINDING:
id: {id}
file: {file}
line: {line}
category: {category}
severity: {severity}
title: {title}
description:
{description}
recommendation:
{recommendation or "(none provided)"}
────────────────────────────────────────────────────────────────────────
PROCEDURE:
1. READ THE CODE. Open {file} at line {line} and the surrounding function.
Understand what the code does — do not trust the finding's description as
the only source.
2. ROOT CAUSE FIRST. Trace backward from the cited sink to where the bad
value or missing check originates. The fix usually belongs there, not at
the line the scanner flagged. Name the root-cause location (file:line).
3. VARIANT HUNT. Grep for sibling call sites with the same pattern. Your fix
should cover all of them, or your rationale should say why not.
4. MINIMAL DIFF. Smallest change that fixes the root cause. No refactoring,
no drive-by cleanup, no reformatting, no comment-only changes. Match the
surrounding code's style (brace placement, naming, error handling).
5. ADVERSARIAL SELF-CHECK. Re-read your diff as an attacker. Name one input
variation that would reach the same bad state without tripping your
change. If you can name one, your fix is at the wrong layer — go back to
step 2.
6. REGRESSION TEST. As part of the diff, add ONE test case that fails before
your change and passes after — placed wherever the project keeps its
tests (look for test_*/, *_test.*, tests/, spec/). If no test directory
exists, omit the test and say so in <test_note>.
────────────────────────────────────────────────────────────────────────
OUTPUT — your final response MUST contain exactly these tags. Emit the diff
verbatim between the markers; do NOT wrap it in ``` fences.
<patch_diff>
--- a/path/to/file
+++ b/path/to/file
@@ ... @@
context line
-removed line
+added line
</patch_diff>
<rationale>what changed and why, mechanically — file:line of root cause,
what the change enforces</rationale>
<variants_checked>file:function pairs you grepped for the same
pattern, and whether each needed the fix</variants_checked>
<bypass_considered>the input variation you tried in step 5 and why it
no longer reaches the bad state</bypass_considered>
<test_note>where the regression test landed, or why none was
added</test_note>
If you determine the finding is NOT fixable as described (wrong file, code
already patched, finding is a false positive), emit:
<patch_diff>NONE</patch_diff>
<rationale>why no patch is appropriate</rationale>
Spawn
For each finding in findings[], build a Task call with the prompt above
(substituting {REPO_PATH}, {id}, {file}, {line}, {category},
{severity}, {title}, {description}, {recommendation}).
description: "patch {id}".
If len(findings) > ~40, shard into sequential batches of ~40 (each batch
one message). Per-finding shard checkpoint after each result is parsed.
If any Task call returns status: "async_launched" instead of the
subagent's text, the runtime backgrounded it. Pick one recovery and use it
for the whole batch:
- If completion notifications arrive in your conversation: parse each
subagent's tagged blocks from its notification
resultas it lands. Do not end your turn until every finding is accounted for. - If notifications do not arrive: do NOT poll transcript files. Re-spawn the missing patch subagents in a fresh Task batch (smaller shard, e.g. 10) and use the synchronous results. The same recovery applies to reviewer subagents in Phase 3.
Parse
From each Task result, extract the five tagged blocks. Tolerate leading/
trailing whitespace, stray ``` fences, and HTML-escaped entities (<
> & — some runtimes escape angle brackets in notification
payloads; unescape before writing the diff). If <patch_diff> is NONE or
empty,
mark status: "no_patch". Otherwise write the diff text to
./PATCHES/bug_NN/patch.diff (NN = zero-padded index in sorted order) and
record rationale, variants_checked, bypass_considered, test_note.
Checkpoint per finding: Write tool → ./.patch-state/_chunk.tmp =
{"id": ..., "bug_nn": "NN", "status": ..., "rationale": ..., ...}, then Bash:
python3 .claude/skills/_lib/checkpoint.py shard ./.patch-state <id> --from ./.patch-state/_chunk.tmp.
After all findings, write the consolidated phase payload to _chunk.tmp then:
python3 .claude/skills/_lib/checkpoint.py save ./.patch-state 2 generate --from ./.patch-state/_chunk.tmp
Phase 3: Independent review (static mode only)
One reviewer subagent per generated diff, all in ONE message,
subagent_type: "general-purpose".
The reviewer never sees the finding's description, recommendation, or
the patch author's rationale. It gets only {file, line, category}
plus the raw diff bytes, and re-derives whether the diff is a minimal,
in-scope fix by reading the source itself. This keeps any instructions
embedded in finding prose from reaching both the author and the gate.
Reviewer prompt (assemble once, reuse per diff)
You are reviewing a candidate security patch as a maintainer would. You have
read-only access to the unpatched source at {REPO_PATH}. You may use Read,
Glob, Grep. You may NOT build, run, or apply the diff.
You have NOT seen the scanner's description of the vulnerability or the
patch author's reasoning. Work only from the location, the category, and the
diff.
LOCATION: {file}:{line}
CATEGORY: {category}
DIFF UNDER REVIEW:
<diff>
{diff_text — or, for diffs over ~50 lines, replace this block with:
"Read the diff at ./PATCHES/bug_NN/patch.diff" and let the reviewer Read it}
</diff>
────────────────────────────────────────────────────────────────────────
ANSWER FOUR QUESTIONS:
1. SCOPE. Does the diff touch only files/functions on the path between
{file}:{line} and its callers? List any hunk that falls outside that
path.
2. SUPPRESSION. Does the diff fix a root cause, or does it suppress the
symptom (try/except: pass, early-return on a magic value, deleting the
check that fired, lowering a log level)?
3. NEW SURFACE. Does the diff add parsing, trust a new input field, weaken
validation elsewhere, or remove a security-relevant check?
4. STYLE. 0-10: would you merge this as-is?
0-3 wrong layer / suppression; 4-6 correct but noisy; 7-10 minimal,
targeted, matches surrounding style.
────────────────────────────────────────────────────────────────────────
End your response with EXACTLY:
REVIEW: ACCEPT | REJECT
STYLE_SCORE: <0-10>
OUT_OF_SCOPE_HUNKS: <comma-separated file:line, or none>
REASON: <2-4 sentences citing specific diff hunks and source lines>
ACCEPT requires: in-scope, root-cause fix, no new attack surface,
style >= 5. Otherwise REJECT.
Spawn and parse
One Task per finding with status != "no_patch". Parse the trailing block.
Attach review, style_score, out_of_scope_hunks, review_reason to the
finding. Set verified: "static_review_only" for every static-mode result
regardless of ACCEPT/REJECT — the label describes the verification class,
not the outcome.
Checkpoint: Write tool → ./.patch-state/_chunk.tmp:
{"phase": 3, "findings": [...]}
Then Bash:
python3 .claude/skills/_lib/checkpoint.py save ./.patch-state 3 review --from ./.patch-state/_chunk.tmp
Phase 4: Output
4a. Per-finding patch_result.json
For each finding (both modes), Write
./PATCHES/bug_NN/patch_result.json:
{
"id": "f003",
"source": "TRIAGE.json#2",
"title": "...",
"file": "...",
"line": 0,
"category": "...",
"severity": "HIGH",
"owner_hint": "...",
"mode": "exec" | "static",
"verified": "ladder_passed" | "ladder_failed" | "static_review_only",
"review": "ACCEPT" | "REJECT" | null,
"style_score": 0,
"out_of_scope_hunks": [],
"rationale": "...",
"variants_checked": "...",
"bypass_considered": "...",
"test_note": "...",
"review_reason": "...",
"verdict": { "t0_builds": true, "...": "(exec mode only, from pipeline)" }
}
In exec mode, also Read the pipeline's
<findings_path>/reports/bug_NN/patch.diff and Write its bytes to
./PATCHES/bug_NN/patch.diff so both modes land in the same place.
4b. ./PATCHES.json
{
"patch_completed": true,
"mode": "exec" | "static",
"repo": "...",
"summary": {
"input_count": 0,
"patched": 0,
"no_patch": 0,
"accepted": 0,
"rejected": 0,
"ladder_passed": 0
},
"findings": [ { ...patch_result.json shape... } ]
}
4c. ./PATCHES.md (incremental)
Step 1 — header. Write tool → ./PATCHES.md (clobbers prior):
# Candidate Patches
{if mode == "static":}
> **Static review only.** These diffs were authored and reviewed by
> independent agents reading source. They were NOT compiled, run, or
> re-attacked. Read each diff yourself before applying — see
> `docs/patching.md#reviewing-generated-patches` for what to look for.
{if mode == "exec":}
> **Execution-verified.** Each diff passed (or failed) the pipeline
> verification ladder: build → reproduce → regress → re-attack. The ladder
> proves the crash is gone, not that the diff introduces no new problems.
**Input:** {findings_path} · **Repo:** {repo} · {N} findings → {M} diffs
---
Step 2 — per finding (sorted: ACCEPT/ladder_passed first, then by
severity). Write ./.patch-state/_chunk.tmp:
## bug_{NN}: [{severity}] {title} ({id})
`{file}:{line}` · {category} · owner: {owner_hint or "?"}
**Status:** {verified} · review {review or "n/a"} · style {style_score or "n/a"}/10
**Diff:** `PATCHES/bug_{NN}/patch.diff` ({hunk count} hunks, {line count} lines)
**Rationale:** {rationale}
**Variants checked:** {variants_checked}
**Bypass considered:** {bypass_considered}
{if review == "REJECT":}
> **Rejected by reviewer:** {review_reason}
{if out_of_scope_hunks:}
> **Out-of-scope hunks:** {out_of_scope_hunks}
---
Then checkpoint.py append ./PATCHES.md --from ./.patch-state/_chunk.tmp.
Step 3 — footer. Append a ## Skipped table for findings with no file
or status == "no_patch", one line each with the reason.
Checkpoint (final): Bash:
python3 .claude/skills/_lib/checkpoint.py done ./.patch-state 4
4d. Terminal summary
Under ~10 lines:
Patches generated ({mode} mode): {N} findings → {M} diffs.
Accepted: {n} {title of top accepted}
Rejected: {n}
No patch: {n}
{if exec:} Ladder passed: {n}/{M}
Wrote ./PATCHES/bug_NN/, ./PATCHES.md, ./PATCHES.json
{if static:} These are drafts. Review before applying — see docs/patching.md.
Guard rails
- The skill never applies diffs. No
git apply, nopatch, no Edit against--repo. If you find yourself needing to, the design is wrong. - Write only under
./PATCHES/and./.patch-state/. - Reviewer isolation. The reviewer prompt receives
{file, line, category, diff}and nothing else from the finding. Do not pass itdescription,recommendation,exploit_scenario, or the patch author'srationale. - Always set
subagent_type. Forking would leak every finding's prose into every patch subagent. - All Task calls for a phase in ONE message. Serial spawning is correct but N× slower.
- Checkpoint before starting the next phase, every time.
- Exec mode delegates, never reimplements. If
vuln-pipeline patchisn't on PATH, stop and tell the user; don't fall back to static mode silently.
Testing this skill
Static mode against the canary fixture:
/vuln-scan targets/canary
/triage VULN-FINDINGS.json --repo targets/canary --auto
/patch TRIAGE.json --repo targets/canary --top 3
Expected: three diffs under PATCHES/bug_00..02/, each
verified: "static_review_only", review: ACCEPT, style ≥ 7 for the
planted overflow/UAF/format-string bugs.
Execution-verified mode against pipeline output:
vuln-pipeline run drlibs --runs 3 --parallel --stream --model <m>
/patch results/drlibs/<ts>/ --model <m>
Expected: delegates to vuln-pipeline patch, surfaces
verified: "ladder_passed" per bug, copies diffs into ./PATCHES/.
Design notes
- TRIAGE.json is canonical input because patching unverified findings wastes tokens on false positives. VULN-FINDINGS.json is accepted with a warning for convenience.
- Static mode emits a regression test inside the diff rather than running it. The skill cannot execute target code (constraint of the static pipeline); the test is for the human who applies the diff.
- Reviewer never sees finding prose. Target source can contain
injected instructions that survive into a scanner's
descriptionfield. The patch author sees that prose (it has to, to know what to fix); the reviewer doesn't, so injected text cannot pass its own gate. verifiedis the verification class, not pass/fail.static_review_onlymeans "an agent read it" regardless of ACCEPT/REJECT.ladder_passed/ladder_failedmeans "ASAN decided." Downstream tooling should branch on this field, not onreview.- Output shape matches the pipeline (
PATCHES/bug_NN/{patch.diff, patch_result.json}) so consumers don't care which mode produced it.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.