Agent Skills › anthropics/defending-code-reference-harness

anthropics/defending-code-reference-harness

GitHub

将C/C++ ASAN漏洞检测流水线适配至其他领域(如Web、智能合约等)。通过阅读现有代码理解通用与特定部分,访谈用户明确需求,并重构相关配置文件和提示词以支持新目标。

6 skills 6,352

Install All Skills

npx skills add anthropics/defending-code-reference-harness --all -g -y
More Options

List skills in collection

npx skills add anthropics/defending-code-reference-harness --list

Skills in Collection (6)

将C/C++ ASAN漏洞检测流水线适配至其他领域(如Web、智能合约等)。通过阅读现有代码理解通用与特定部分,访谈用户明确需求,并重构相关配置文件和提示词以支持新目标。
需要将漏洞检测流水线迁移到其他编程语言或安全领域 希望自定义或分叉现有的ASan检测流程
.claude/skills/customize/SKILL.md
npx skills add anthropics/defending-code-reference-harness --skill customize -g -y
SKILL.md
Frontmatter
{
    "name": "customize",
    "description": "Adapt this C\/C++ ASAN vulnerability pipeline to a different vulnerability class, target shape, language, or detection mechanism. Use when the user wants to port, migrate, retarget, customize, or fork the pipeline for something other than C\/C++ memory-safety bugs — web apps, smart contracts, deserialization, ML systems, or any other domain."
}

Customize the vuln-pipeline

This pipeline ships as an opinionated C/C++ + AddressSanitizer demo. Its real shape is more general: an agent crafts an input, runs a target in a sandbox, a detector fires, a second agent verifies, a third agent analyses exploitability. Every noun in that sentence can be swapped. Your job is to interview the user, figure out which nouns they want to swap, and rewrite the relevant files.

The existing C/C++ code is the worked example. You don't need a playbook for each domain — read what's there, understand what's generic vs. ASAN-specific, and adapt.

STEP 1 — Read the pipeline (do this BEFORE asking anything)

Skim these files so your questions are grounded:

  • README.md — pipeline overview (recon → find → grade → judge → report)
  • harness/cli.py — orchestration; shows how stages wire together and what lands on disk
  • harness/find.py, harness/grade.py, harness/report.py — the three container-agent loops; mostly generic plumbing
  • harness/prompts/find_prompt.py, harness/prompts/grade_prompt.pythe C/C++-specific parts; bug taxonomy, quality tiers, grading rubric
  • harness/prompts/report_prompt.py, harness/prompts/report_grader_prompt.pyalso C/C++-specific; exploitability sections (primitive, heap layout, escalation path) and the rubric that scores them
  • harness/prompts/judge_prompt.py — triage prompt; keys on ASAN excerpts and memory-safety crash classes
  • harness/prompts/system_prompt.py — authorization block; hard-codes "C/C++ target" and "sanitizer output"
  • harness/asan.py — stack-trace parser for dedup/judge signatures; ASAN-specific regex
  • harness/artifacts.pyCrashArtifact, GraderVerdict, JudgeVerdict, ReportVerdict data contracts
  • harness/config.py, targets/drlibs/config.yaml — target config schema
  • targets/README.md — how a target directory is structured (Dockerfile + config.yaml + entry wrapper)

You don't need agent.py, docker_ops.py, recon.py, judge.py, or novelty.py in detail — they're generic plumbing (judge/novelty domain-specificity lives in the prompts and the asan parser, not the flow).

STEP 2 — Interview the user

Use AskUserQuestion to gather requirements. Start with broad context, then narrow to technical specifics based on what they say.

Round 1 — Context (always ask these first, together)

Two open-ended questions to understand who you're talking to and what they're after. Expect most answers to come via Other as free text — the options are there to prompt thinking, not to constrain.

Question A — Operating context

  • header: Context
  • question: What's your operating environment? Who will run this pipeline and why?
  • options: a few archetypes as inspiration — e.g. "Pentesting firm — client engagements, need reportable findings", "Internal appsec team — scan our own services in CI", "Smart-contract auditor — pre-deployment reviews", "Security researcher — hunting novel bug classes". These tell you what output format, grading rigor, and workflow integration matter.

Question B — Goal

  • header: Goal
  • question: Describe in your own words what you want this pipeline to find. What kind of target, what kind of bugs?
  • options: 2–3 concrete examples (e.g. "Web vulnerabilities like SQLi/XSS in HTTP services", "Reentrancy and access-control bugs in Solidity contracts", "Deserialization RCE in Java microservices").

The context answer calibrates your follow-ups: a pentesting firm probably cares about CVSS scoring and SARIF output; a researcher may want differential testing and novel detection signals; an internal team likely wants CI integration and low false-positive rates.

Round 2 — Technical follow-ups (adaptive — derive from round-1 answers)

Parse their round-1 answers against the axes of variation below. For each axis left ambiguous, ask a targeted follow-up. Batch up to 4 questions per AskUserQuestion call. Common follow-ups:

  • Detection signal — "How will the pipeline know it found something?" (crash, exception, canary file appears, DNS callback, differential mismatch, invariant violation)
  • PoC shape — "What does a proof-of-concept look like?" (single file, HTTP request sequence, transaction list, test-pipeline code)
  • Isolation — "Where does the target run?" (Docker, VM, testnet, remote sandbox, or no execution — static-only)
  • Grading criteria — "What makes a finding high-quality vs. low-quality in this domain?"
  • Exploitability analysis — "What sections should a report contain?" The C/C++ report has primitive · reachability · heap layout · escalation path · constraints. A web-vuln report might want injection vector · auth bypass · data exposure · chaining potential. Ask what they need, or whether they want the report stage at all.
  • Novelty/upstream check — "Should the pipeline check if a finding is already fixed upstream?" The C/C++ version shallow-clones the target's GitHub and checks git log <commit>..HEAD -- <crash_file>. Only applies if targets have a canonical upstream and a sensible "crashing file" to key on — many domains won't.
  • Scope — "Replace the C/C++ support entirely, or keep it alongside the new domain via a profile system?"

Keep going until you can fill in every row of the architecture map in STEP 3. If an answer is vague, ask a narrower follow-up rather than guessing.

Background — axes of variation (context for formulating follow-ups)

These are the dimensions along which customers might want to deviate from the C/C++ demo. Use this list to spot gaps in the user's description and generate follow-up questions — do not present it as a menu.

Vulnerability class: memory safety · web/API (SQLi, XSS, SSRF, XXE, path traversal, IDOR) · deserialization RCE · logic/race (TOCTOU, privilege escalation) · crypto (weak RNG, timing, nonce reuse) · DoS (ReDoS, hash flooding) · smart contracts (reentrancy, access control, front-running) · ML/AI (prompt injection, jailbreaks, data extraction) · protocol parsing

Target shape: CLI binary + file · HTTP service · library via test harness · network daemon · smart contract · browser extension · mobile app

Detection mechanism: crash/abort · uncaught exception · sanitizer hooks (Jazzer/Atheris) · outcome-based (canary file, DNS callback, shell spawn) · differential testing · invariant violation · taint tracking

Input modality: single file · HTTP request chain · multi-file archive · stdin stream · args + env + config combo · transaction sequence

Isolation boundary: Docker container · full VM · remote sandbox · local testnet · none (static analysis)

Dedup signature: (crash_type, top_frame) · (vuln_type, endpoint, param) · (function, state_transition) · (component, precondition)

Report structure: primitive/heap/escalation (memory safety) · vector/auth/exposure (web) · invariant/path/impact (contracts) · or drop the report stage entirely if find+grade is the deliverable

Output format: result.json + poc.bin · SARIF · Nuclei template · prose report

Patch verification signal: ASAN-clean exit · uncaught-exception-free · sanitizer hook silent (Jazzer/Atheris) · canary file untouched · invariant assertion holds · differential output matches reference. This is what _t1_passes() in patch_grade.py encodes — "the bug is gone" for the new domain.

Background — architecture map (what changes vs. what stays)

File C/C++-specific? What it does
harness/prompts/find_prompt.py Yes — rewrite Bug taxonomy, quality tiers, ASAN output format, exit-code examples
harness/prompts/grade_prompt.py Yes — rewrite 5-criterion rubric assumes ASAN traces and Unix signal exit codes
harness/prompts/report_prompt.py Yes — rewrite Exploitability sections: primitive, heap layout, escalation path — memory-safety-specific
harness/prompts/report_grader_prompt.py Yes — rewrite Scores the above sections; rubric is tied to the section set
harness/prompts/judge_prompt.py Yes — rewrite Triage keys on ASAN excerpts and crash-class taxonomy
harness/prompts/patch_prompt.py Yes — rewrite Asks for git diff -- '*.c' '*.h', assumes ASAN trace, memcpy-style root-cause guidance
harness/prompts/system_prompt.py Yes — rewrite Authorization block says "C/C++ target", "sanitizer output"
harness/asan.py Yes — rewrite Regex for #N 0xHEX in func /path:line frames; feeds dedup, judge, novelty
targets/README.md + Dockerfile template Yes — rewrite gcc -fsanitize=address, entry.c wrapper pattern
harness/patch_grade.py Light edit _t1_passes() checks AddressSanitizer: substring; rest of the verification ladder is generic
harness/report.py Light edit _SECTIONS tuple and token lists need to match the new report structure; flow is generic
harness/novelty.py Light edit crash_file_from_frame() is ASAN-specific; git-log logic is generic. Drop entirely if no upstream.
harness/config.py Light edit May need new fields (profile, run_command instead of binary_path); attack_surface likely stays
harness/artifacts.py Light edit crash_type/exit_code semantics may shift; ReportVerdict.section_scores keys must match new sections
harness/dedup.py Light edit Signature function needs the new parser; grouping logic is generic
harness/prompts/recon_prompt.py Light edit Mostly language-agnostic; scrub C idioms
harness/cli.py Unchanged Orchestration is domain-neutral
harness/agent.py Unchanged Agent runner is generic
harness/docker_ops.py Unchanged Container plumbing is generic (may need changes if isolation ≠ Docker)
harness/find.py, grade.py, recon.py, judge.py, patch.py Unchanged Flow is generic; only injected prompts change

STEP 3 — Present a plan and get confirmation

Before editing anything, summarize back to the user:

  1. What you understood — restate their goal in one sentence
  2. What will change — list each file you'll edit with a one-line rationale
  3. What stays — reassure them the orchestration core is untouched
  4. Open questions — anything you're still unsure about

Wait for explicit approval. If they adjust the plan, incorporate and re-confirm.

STEP 4 — Execute

Edit the files per the approved plan. Work through them in dependency order: prompts and parser first (they're standalone), then config/artifacts, then the target template, then README. Commit incrementally if the user wants checkpoints.

STEP 5 — Validate

  1. Add a canary target under targets/<domain>-canary/ with 2–3 planted bugs of the new class
  2. Run: bin/vp-sandboxed run <domain>-canary --model <model-id> --runs 3 --parallel --stream --max-turns 50 (use Claude Opus unless the user specifies a different model). Run ./scripts/setup_sandbox.sh once first if the sandbox isn't already set up.
  3. Confirm all planted bugs are found and graded PASS
  4. Confirm judge triage worked: cat results/<domain>-canary/<ts>/reports/judge_log.jsonl — expect one NEW per distinct bug, DUP_SKIP for repeats
  5. Confirm reports landed: ls results/<domain>-canary/<ts>/reports/bug_*/report.json and spot-check section scores
  6. Run vuln-pipeline dedup results/<domain>-canary/ and confirm signatures group correctly
仓库入门助手,提供两种模式:空参数时进行简短介绍并引导完成首次漏洞扫描演练;带参数时基于仓库文档解答用户关于使用方法、配置或错误的具体问题。
用户输入 /quickstart 且无后续内容 用户询问如何开始使用 用户提出关于仓库功能、配置或操作的具体问题
.claude/skills/quickstart/SKILL.md
npx skills add anthropics/defending-code-reference-harness --skill quickstart -g -y
SKILL.md
Frontmatter
{
    "name": "quickstart",
    "description": "The front door for this repo. With no argument: a 30-second intro, then an offer to walk you through your first run on the canary target. With a question: answers it from this repo's own docs and source, cites where it looked, and hands you the next command. Use for \"how do I…\", \"why does…\", \"where is…\", \"can this…\", or just \"\/quickstart\" to get oriented.",
    "allowed-tools": [
        "Read",
        "Glob",
        "Grep",
        "Task",
        "AskUserQuestion"
    ],
    "argument-hint": "[question]   (blank = 30-sec intro)"
}

/quickstart

Two modes, picked by whether $ARGUMENTS is empty.

  • Empty → Intro mode. Short orientation, then offer the guided first run.
  • Non-empty → Help mode. Treat $ARGUMENTS as the operator's question.

Intro mode

Keep it short and a little warm; this is the first thing a new operator sees.

Say roughly:

Welcome! This repo takes you from finding your first vulnerability to patching at scale, using a set of Claude Code skills and an autonomous pipeline. Two ways in: interactive skills (no setup, safe, start here) and the autonomous pipeline (Docker, scales to hundreds of parallel agents).

The ramp-up:

| Day 1 | Threat-model + first static scan + triage | | Day 2 | Run the reference pipeline (C/C++) | | Day 3-4 | Customize it for your stack | | Week 2 | Autonomous scanning, triage, and patching |

Day-1 goal: threat-model, scan, and triage the bundled canary target. Most teams get there before lunch.

Remind them to export CLAUDE_CODE_SUBAGENT_MODEL=<model-id> so subagents use the same model as the session.

Then AskUserQuestion with three options:

  1. Walk me through Day 1 on the canary (~10 min) → run "Guided first run" below.
  2. I have a question → ask what it is, then switch to Help mode.
  3. I'll read the README → point at README.md Step 1 and stop.

Guided first run

Runs the three Step-1 skills on targets/canary, pausing after each to show what landed on disk. These only read/write files in the repo; no sandbox needed.

  1. /threat-model bootstrap targets/canary via Task. When done, open THREAT_MODEL.md, show the focus areas, explain in 2-3 sentences how this steers the scan.
  2. /vuln-scan targets/canary via Task. When done, open targets/canary/VULN-FINDINGS.md, summarize the count and top 2-3 findings, point at VULN-FINDINGS.json.
  3. /triage targets/canary/VULN-FINDINGS.json via Task. When done, open TRIAGE.md, explain what changed vs. raw findings (verified, deduped, re-ranked).

Pause for the operator between each (AskUserQuestion); don't barrel through. Close with a one-line recap of the three artifacts on disk, then point at README Step 2 for the execution-verified pipeline. Never run vuln-pipeline or anything that executes target code here; that's Step 2 and needs Docker + a sandbox.


Help mode

Answer the operator's question using this repo as ground truth: README, docs/*.md, harness/*.py, targets/*/config.yaml, .claude/skills/*. Don't answer from general knowledge when the repo has a specific answer.

Routing map

If the question is about… Read first Then offer
running the pipeline docs/pipeline.md, README Step 2 the recon / run command
too many findings, triage docs/triage.md /triage <path>
porting, Java/Go/Rust/etc. docs/customizing.md, README Step 3 /customize
safety, sandbox, Docker docs/security.md cite; no action
rate limits, 429, token budget docs/pipeline.md: Rate limits, docs/troubleshooting.md#rate-limits cite the numbers
duplicates, dedup docs/troubleshooting.md#duplicate-findings known_bugs: hint
CLI flags, "what does --X do" harness/cli.py (grep the argparse) exact flag + example
which model, subagent pinning docs/troubleshooting.md: Subagents the export line
best practices, prompting docs/best-practices.md, docs/prompting.md cite the principle
"how do I start" README Step 1 offer Guided first run
patching, fix, diff, re-attack docs/patching.md, README Step 4 /patch <input>
binary, pentest, other domains docs/other-use-cases.md cite section
anything else README Table of contents best-match doc

Answer format

  1. Direct answer in 2-5 sentences.
  2. > source: the file(s) and section you used.
  3. Next action: one copy-pasteable command or skill invocation, if one applies. If none does, say so.
  4. If the question is ambiguous, ask one clarifying question; don't guess.

Constraints

  • Never fabricate CLI flags or file paths. If unsure, Grep for it in harness/cli.py or the target configs and quote what you find.
  • If the repo doesn't answer the question, say so plainly and suggest the operator open a GitHub issue on this repo.
  • Keep the Q&A dry and cited. Save the warmth for Intro mode.
对原始安全扫描结果进行验证、去重、按可利用性重新排序并分配负责人。支持交互或自动模式,通过源码分析确认真实性,输出分类后的报告供工程团队优先处理。
triage findings validate scanner output prioritize vulns review the backlog
.claude/skills/triage/SKILL.md
npx skills add anthropics/defending-code-reference-harness --skill triage -g -y
SKILL.md
Frontmatter
{
    "name": "triage",
    "description": "Triage a batch of raw security findings. Verify each is real, collapse duplicates, re-rank by derived exploitability, and tag with an owner. Takes a directory or file of scanner output and writes TRIAGE.json + TRIAGE.md sorted by what actually needs engineering attention. Use when asked to \"triage findings\", \"validate scanner output\", \"prioritize vulns\", or \"review the backlog\". Runs interactively by default; pass --auto to skip the interview.",
    "allowed-tools": [
        "Read",
        "Glob",
        "Grep",
        "Write",
        "Task",
        "AskUserQuestion",
        "Bash(git log:*)",
        "Bash(jq:*)",
        "Bash(find:*)",
        "Bash(ls:*)",
        "Bash(wc:*)",
        "Bash(python3 .claude\/skills\/_lib\/checkpoint.py:*)"
    ],
    "argument-hint": "<findings-path> [--auto] [--votes N] [--repo PATH] [--fp-rules FILE] [--fresh]"
}

triage

Adversarial triage of raw security-scanner output. Does four jobs: verify each finding is real, deduplicate across runs and scanners, rank survivors by derived exploitability rather than the scanner's claimed severity, and route each to a component owner. Output is a short, ranked, owned list instead of a raw dump.

Invoke with /triage <findings-path> [--auto] [--votes N] [--repo PATH] [--fp-rules FILE].

Arguments (parse from $ARGUMENTS; positional $1/$2 expansion is not stable across runtimes):

  • findings path (first positional, required): a JSON file, a directory of JSON files, a VULN-FINDINGS.json, a pipeline results/<target>/<ts>/ directory, or a markdown report.
  • --auto: skip the interview and use defaults. Default mode is interactive.
  • --votes N: verifier votes per finding (default 3; use 1 for a quick pass, 5 for high-stakes batches).
  • --repo PATH: path to the target codebase, read-only (default cwd). Verification needs source access; the skill stops with an error if the cited files aren't reachable.
  • --fp-rules FILE: append the contents of FILE to the verifier's exclusion-rule list (Phase 3a). Use for org-specific precedents: "we use Prisma ORM everywhere — raw-query SQLi only", "k8s resource limits cover DoS", etc. Plain text, one rule per line or paragraph.
  • --fresh: ignore any existing checkpoint in ./.triage-state/ and start from Phase 0. Without this flag the skill resumes from the last completed phase if a checkpoint is present.

Tools: Read, Glob, Grep, Write, Task, AskUserQuestion. Bash is permitted only for git, find, wc, ls, jq, and python3 .claude/skills/_lib/checkpoint.py (checkpoint I/O).

Do not execute target code. No building, running, installing dependencies, or sending requests. A proof-of-concept that accidentally works against something real is unacceptable, and "couldn't write a working PoC" is weak evidence of non-exploitability. Every conclusion comes from reading source. This applies to the orchestrator and every subagent; include the constraint in every Task prompt. For high-confidence HIGH findings, recommend a human-built PoC as a follow-up instead.

Do not reach the network. No package-registry lookups, CVE-database queries, or upstream-commit fetches.


Checkpointing (runs before Phase 0 and after every phase)

On large finding batches a full run can exhaust context or hit rate limits mid-way — particularly Phase 3, which spawns candidates × votes verifiers. Phase state persists to ./.triage-state/ so a fresh /triage session can resume without re-asking the interview or re-spawning verifiers.

All checkpoint I/O goes through python3 .claude/skills/_lib/checkpoint.py (atomic writes, JSON-validated). Never use the Write tool for progress.json directly. Never pass payload via heredoc or stdin; target-derived strings could collide with the heredoc delimiter and break out to shell. The Write→--from pattern keeps repo-derived bytes out of Bash argv.

State files in ./.triage-state/:

  • progress.jsonsingle source of truth for resume position: {"status": "running"|"complete", "phase_done": N, "shards_done": [...]}. Resume decisions read ONLY this file, never a glob of phase*.json or shard files (stale files from a prior run must not be trusted).
  • phaseN.json — data payload for phase N (schemas at the tail of each phase section below).
  • _chunk.tmp — transient payload buffer; overwritten before every save/shard/append call.

Start of run — resume check. Bash: python3 .claude/skills/_lib/checkpoint.py load ./.triage-state

  • status == "absent" OR "complete", OR --fresh in $ARGUMENTSfresh start. Bash: python3 .claude/skills/_lib/checkpoint.py reset ./.triage-state, then proceed to Phase 0.
  • status == "running" with phase_done == Nresume. Read ./.triage-state/phase0.json through phaseN.json in order (and any shard_*.json files listed in shards_done), merging keys into working state (later files override earlier — checkpoints may be deltas). Print Resuming from checkpoint: Phase N complete (./.triage-state/phaseN.json), and skip directly to Phase N+1.

End of every phase N. Two tool calls:

  1. Write tool → ./.triage-state/_chunk.tmp containing the phase's output JSON (schema at the tail of each phase section).
  2. Bash → python3 .claude/skills/_lib/checkpoint.py save ./.triage-state <N> <name> --from ./.triage-state/_chunk.tmp

End of run. After writing TRIAGE.json and TRIAGE.md, Bash: python3 .claude/skills/_lib/checkpoint.py done ./.triage-state 6


Phase 0: Mode select and interview

0a. Parse arguments

From $ARGUMENTS: extract the findings path (first positional), --auto flag, --votes N (default 3), --repo PATH (default .), --fp-rules FILE (default none). If no findings path was given, ask for one and stop. If --fp-rules was given, Read the file now and carry its contents as context.extra_fp_rules for injection into the Phase 3a verifier prompt.

0b. Interactive mode (default): interview the user

Unless --auto was passed, use AskUserQuestion to gather context that shapes verification and ranking. Batch into one or two calls of up to four questions. Expect free-text answers via "Other"; the multiple-choice options are prompts, not constraints.

Round 1 (single AskUserQuestion call):

  1. Environment & trust boundary (header Environment, single-select) What kind of system are these findings from, and where does untrusted input enter it? Options: Internet-facing web service (HTTP is untrusted), Internal service (callers are authenticated peers), Library / SDK (caller is the trust boundary), CLI / batch tool (operator inputs trusted, file inputs not), Embedded / firmware (physical access in scope). Reachability is judged against this boundary; "command injection from env var" is a true positive in a multi-tenant web service and a rule-8 false positive in an operator CLI.

  2. Threat model (header Threat model, multi-select) What does a worst-case attacker look like for this system, and what must never happen? Free text is best. Options: Unauthenticated remote code execution, Tenant-to-tenant data leakage, Privilege escalation to admin, Supply-chain compromise of downstream users, Denial of service against a paid SLA, Compliance-scoped data exposure (PII / PCI / PHI). Phase 4 boosts findings that map onto a stated threat.

  3. Scoring standard (header Scoring, single-select) How should severity be expressed in the output? Options: Derived HIGH/MEDIUM/LOW from preconditions (default), CVSS v3.1 vector + base score, CVSS v4.0 vector + base score, OWASP Risk Rating (likelihood x impact), Organization bug-bar (describe in Other). The precondition rule is always computed; this controls what severity_label additionally shows.

  4. Noise tolerance (header Noise tolerance, single-select) When verifiers disagree, which way should ties break? Options: Precision: drop anything not majority-confirmed (fewer FPs, may miss real bugs), Recall: keep split votes as needs_manual_test (more to review, fewer misses), Ask me per-finding when it happens.

Round 2 (conditional): if the threat-model answer was empty or generic, or the scoring answer was Organization bug-bar, ask one targeted follow-up.

Record the answers as a context dict carried through every phase and echoed in the output under triage_context.

0c. Auto mode defaults

When --auto is set, do not call AskUserQuestion. Use:

  • Environment: Unknown. Treat any externally-reachable entry point as untrusted; flag trust-boundary assumptions explicitly in rationale.
  • Threat model: empty (no boost).
  • Scoring: derived HIGH/MEDIUM/LOW.
  • Noise tolerance: precision.

Checkpoint: Write tool → ./.triage-state/_chunk.tmp:

{"phase": 0, "context": {mode, environment, threat_model, scoring, noise_tolerance, votes_per_finding, repo, findings_path}}

Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.triage-state 0 interview --from ./.triage-state/_chunk.tmp On resume past Phase 0, the interview is not re-asked; context is restored from this file.


Phase 1: Ingest and normalize

Turn the input into a flat findings[] list with stable ids, regardless of source format.

1a. Detect input shape

Inspect the findings path:

  • Directory: Glob for **/*.json and **/*.jsonl. Recognized containers, in priority order:
    • VULN-FINDINGS.json (a {findings: [...]} container): read .findings[].
    • reports/bug_*/report.json or reports/manifest.jsonl (this repo's pipeline output): one finding per bug_NN. Map crash.crash_typecategory, verdict.severity_ratingseverity, the prose reportdescription, crash file from the ASAN top frame → file/line.
    • found_bugs.jsonl: one finding per line.
    • Any other *.json whose top level is a list of objects, or an object with a findings/results/issues/vulnerabilities array: that array.
  • Single .json / .jsonl file: same recognition as above.
  • Markdown / text: split on level-2/3 headings or --- rules; for each section, extract file, line, category, severity, description by pattern (File:, Line:, Severity: labels or path:NN spans). Best-effort; mark source_format: "markdown_heuristic".

If nothing parseable is found, stop and report what was seen.

1b. Normalize fields

For each raw record, build a finding dict. Pull what's present; never guess what's absent. Field map (source-key aliases → canonical):

Canonical Also accept
file path, location.file, filename, ASAN top-frame file
line line_number, location.line, lineno
category type, cwe, rule_id, crash_type, vulnerability_class
severity severity_rating, level, priority, risk
title name, summary, message
description details, report, body, evidence
exploit_scenario attack_scenario, poc, reproduction
preconditions requirements, assumptions
recommendation fix, remediation, mitigation
scanner_confidence confidence, score, certainty (normalize to 0.0-1.0)

Attach to every finding:

  • id: f001, f002, ... in ingest order. If scanner_confidence is present on most findings, order ingest by it descending so high-signal findings get verified (and surface in partial output) first; otherwise keep source order. This is a scheduling prior only — it does not affect verdicts.
  • source: relative path of the file it came from, plus source format.
  • missing_fields: list of canonical fields that were absent. If file is missing or does not resolve under --repo, the finding is unlocatable: it skips dedup and verification and is emitted directly with verdict: false_positive, verify_verdict: needs_manual_test, confidence: 0, refute_reasons: ["doesnt_exist"], rationale: "no source location in input; cannot verify statically; human review required". Never emit a confident verdict on a finding you could not locate, and never let it absorb or be absorbed by dedup.

1c. Locate the target codebase

Resolve --repo (default cwd). For the first 5 findings with a file, check the path resolves under the repo. Try, in order: (a) repo/file as-given; (b) file as an absolute or cwd-relative path; (c) repo/file with common prefixes stripped from file (src/, app/, ./, or the repo's own basename, e.g. harness/grade.py with --repo harness). Record which resolution worked and apply it to every finding. If none resolve, stop: tell the user verification needs source access and the cited files aren't reachable, and suggest a --repo value based on the longest common suffix you can see.

Checkpoint: Write tool → ./.triage-state/_chunk.tmp:

{"phase": 1, "context": {...}, "findings": [ {normalized finding dicts with id/source/file/line/category/...} ], "path_resolution": "<which of a/b/c worked>"}

Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.triage-state 1 ingest --from ./.triage-state/_chunk.tmp


Phase 2: Deduplicate (before verification)

Collapse repeats so duplicate findings don't each burn N verifiers.

2a. Deterministic pass (inline, no subagent)

Cluster findings where all of:

  • same file (after path normalization), AND
  • same category (case-insensitive, punctuation stripped), AND
  • line numbers within 10 of each other. Both-missing matches; one-side- missing does NOT (a line-less record must not absorb a located one).

Within each cluster, the canonical is the record with the fewest missing_fields; ties break to lowest id. Every other member gets verdict: duplicate, duplicate_of: <canonical id>, and is removed from the working set. Record duplicate ids on the canonical as absorbed: [...].

2b. Semantic pass (one subagent, only if >1 cluster survives)

Spawn ONE Task with subagent_type: "general-purpose" and this prompt:

You are deduplicating security findings before expensive verification. Two
findings are DUPLICATES if fixing one would also fix the other. Two findings
are DISTINCT if they have genuinely independent root causes, even if they
share a category or file.

Treat as DUPLICATE:
- Same root cause described with different wording or by different scanners
- A shared vulnerable helper function reported once per call site
- A missing global protection (auth check, output encoding) reported once
  per endpoint that lacks it
- A cause ("missing input validation on `name`") and its consequence
  ("SQL injection via `name`") in the same code path

Treat as DISTINCT:
- Different categories in the same file region (an "ssrf" near a
  "buffer_overflow" is not a duplicate just because the lines are close)
- Same file, same category, but different tainted variables reaching
  different sinks
- Same helper, but two independent bugs inside it
- Two endpoints missing the same check, where the fix is per-endpoint
  rather than a shared gate

Below are the candidate findings (one per line: id | file:line | category |
title). Group them. Respond with ONLY lines of the form:

  GROUP: <canonical_id> <- <dup_id>, <dup_id>, ...

One line per group that has duplicates. Omit singletons. Pick the most
specific / best-described finding as canonical. No prose.

CANDIDATES:
{one line per surviving finding: "f003 | src/auth.py:112 | sql_injection | User lookup concatenates name into query"}

Parse GROUP: lines. For each, mark the listed dup ids with verdict: duplicate, duplicate_of: <canonical>, append them to the canonical's absorbed, and drop them from the working set.

Carry forward candidates[] = the surviving canonicals.

Checkpoint: Write tool → ./.triage-state/_chunk.tmp:

{"phase": 2, "context": {...}, "findings": [ {all findings; duplicates carry verdict/duplicate_of} ], "candidates": ["f001", "f003", "..."]}

Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.triage-state 2 dedup --from ./.triage-state/_chunk.tmp


Phase 3: Verify

For each candidate, N independent adversarial verifiers re-derive the claim from the code and vote. Each verifier's stance is "find any reason this is wrong." Each starts from the code at the cited location, not the scanner's description, and never sees the other verifiers' reasoning (shared context propagates blind spots).

3a. Verifier prompt (assemble once, reuse for every spawn)

You are a skeptical security engineer adversarially verifying ONE finding
from an automated scanner. Your default assumption is that the scanner is
WRONG. Your job is to re-derive the claim from the source code yourself and
decide TRUE_POSITIVE or FALSE_POSITIVE.

You have read-only access to the target codebase at: {REPO_PATH}
You may use Read, Glob, and Grep, but ONLY on paths inside {REPO_PATH}.
Do NOT read, grep, or glob outside that root: anything outside it (the
triage pipeline itself, scanner outputs, fixtures, other repos on disk) is
out of scope and citing it contaminates your verdict. If a finding's
`file` resolves outside {REPO_PATH}, return CANNOT_VERIFY with
REFUTE_REASON: doesnt_exist. You may NOT build, run, or test the target,
install dependencies, or reach the network. Every conclusion must come
from reading source under {REPO_PATH}.

ENVIRONMENT (from the operator; this defines the trust boundary):
{context.environment or "Unknown. Treat any externally-reachable entry point as untrusted."}

────────────────────────────────────────────────────────────────────────
PROCEDURE: follow all four steps. Each exists because skipping it lets a
specific false-positive class through.

1. READ THE CODE AT THE CITED LOCATION YOURSELF.
   Open {file} at line {line}. Understand what the code actually does. Do
   NOT trust the scanner's description: scanners misread code surprisingly
   often, and if you start from the summary you inherit the misreading.

2. TRACE REACHABILITY BACKWARDS FROM THE SINK.
   Grep for callers of this function/method. Follow imports. Establish
   whether attacker-controlled input (per the ENVIRONMENT above) can
   actually reach this line. A plausible-sounding chain is NOT enough: for
   at least the FIRST link in the chain, READ the actual call site and
   QUOTE the file:line in your rationale. Unreachable code is the single
   largest false-positive source.

3. HUNT FOR PROTECTIONS.
   Actively look for reasons the finding is WRONG:
   - Input validation / sanitization upstream of the sink
   - Framework auto-escaping, parameterized queries, prepared statements
   - Type constraints (the value is an int, an enum, a fixed-length token)
   - Authentication / authorization gates before this path
   - Configuration that limits exposure (feature flag off, debug-only)
   - Dead code, test-only code, example/fixture code

4. STRESS-TEST EACH PROTECTION.
   For each protection you found: is it applied on EVERY path to the sink,
   or only the one the scanner happened to trace? Are there encodings,
   edge cases, or alternate entry points that bypass it?

────────────────────────────────────────────────────────────────────────
EXCLUSION RULES: if the finding matches any of these, it is FALSE_POSITIVE
even if technically accurate. Cite the rule number in your verdict.

  1. Volumetric DoS or missing rate-limiting (handled at infrastructure
     layer). ReDoS, algorithmic complexity, and unbounded recursion ARE
     still valid findings.
  2. Test-only code, dead code, example/fixture code, or a crash with no
     security impact.
  3. Behavior that is the intended design (compression middleware, a
     backward-compatible weak algorithm offered alongside a strong one).
  4. Memory-safety concerns in memory-safe languages outside `unsafe` /
     FFI blocks.
  5. SSRF where the attacker controls only the path, not the host or
     protocol.
  6. User input flowing into an AI/LLM prompt (prompt injection is not a
     code vulnerability in the target).
  7. Path traversal in object storage (S3/GCS) where `../` does not escape
     a trust boundary.
  8. Trusted inputs used as the attack vector (env vars, CLI flags set by
     the operator), UNLESS the ENVIRONMENT above marks them untrusted.
  9. Client-side code flagged for server-side vulnerability classes.
 10. Outdated dependency versions (managed by a separate process).
 11. Weak random used for non-security purposes (jitter, shuffling,
     dev-only fallbacks).
 12. Low-impact nuisance issues (log spoofing, CSRF on logout, self-XSS,
     tabnabbing, open redirect, regex injection).
 13. Missing hardening or best-practice gap with no concrete exploit path
     (missing security headers, no audit logging, permissive config that
     isn't actually reached by untrusted input).
 14. XSS in a framework with default auto-escaping (React, Angular, Vue,
     Jinja2 autoescape=on) UNLESS the sink is a raw-HTML escape hatch
     (dangerouslySetInnerHTML, bypassSecurityTrustHtml, v-html, |safe).
 15. Identifiers that are unguessable by construction (UUIDv4, 128-bit+
     random tokens) flagged as "predictable" or "needs validation".
 16. Race conditions or TOCTOU that are theoretical only — no realistic
     window, or no security-relevant state changes between check and use.

{if context.extra_fp_rules: append here verbatim under an
 "ORG-SPECIFIC RULES:" heading}

────────────────────────────────────────────────────────────────────────
VERDICT: your response MUST end with EXACTLY this block:

  VERDICT: TRUE_POSITIVE | FALSE_POSITIVE | CANNOT_VERIFY
  CONFIDENCE: <0-10>
  REFUTE_REASON: <one of: doesnt_exist, already_handled,
    implausible_trigger, intentional_behavior, misread_code, duplicate,
    not_actionable, n/a>
  EXCLUSION_RULE: <1-16, org rule, or none>
  FIRST_LINK: <file:line of the first call site you read, or "none found">
  RATIONALE: <2-5 sentences citing specific file:line evidence for
    reachability, protections found/absent, and why each held or didn't>

TRUE_POSITIVE requires ALL of: path is reachable from untrusted input per
the ENVIRONMENT; protections are insufficient or bypassable; real-world
exploitation is feasible.

FALSE_POSITIVE requires ANY of: unreachable from untrusted input;
adequately protected on all paths; scanner misread the code; an exclusion
rule applies.

CANNOT_VERIFY: static reasoning genuinely hit its limit (e.g. behavior
depends on runtime configuration you cannot read, or the code path crosses
into a binary you cannot inspect). Use sparingly; it must not become the
default.

3b. Spawn N verifiers per candidate, all in one message

For each finding in candidates[], build N Task calls (N = --votes, default 3) with subagent_type: "general-purpose" and description: "verify {id} vote {k}/{N}".

Always set subagent_type; never fork. Omitting subagent_type forks the orchestrator, and a fork inherits the full conversation context: every other finding's description, the scanner's prose, and any prior verifier results. That defeats verifier independence and re-introduces the inherited-framing failure mode this phase exists to prevent. Each verifier must start with a fresh, empty context and receive only the 3a prompt plus the single finding under review. The same applies to the ranking subagents in 4a.

Each prompt is the verifier prompt from 3a with this block appended:

────────────────────────────────────────────────────────────────────────
FINDING UNDER REVIEW (from the scanner; treat as a CLAIM, not a fact):

  id:        {id}
  file:      {file}
  line:      {line}
  category:  {category}
  severity (claimed): {severity}
  title:     {title}

  description:
  {description}

  exploit_scenario:
  {exploit_scenario or "(not provided)"}

  preconditions (claimed):
  {preconditions as bullets or "(not provided)"}

You are vote {k} of {N}. You have NOT seen the other verifiers' reasoning
and you must NOT try to find it. Work independently from the code.

Put all verifier Task calls in a single assistant message so they run concurrently. Do not set run_in_background; you need the final text, not an async handle. If len(candidates) * N exceeds ~40, shard into sequential batches of ~40, but keep each batch a single message.

Prompt size at scale. The 3a prompt is ~1200 words. When candidates * votes > ~50, use this compact form instead (same procedure and output contract, prose stripped):

Adversarially verify ONE scanner finding. Default: scanner is WRONG.
Read-only access scoped to {REPO_PATH} ONLY. No exec, no network.
ENVIRONMENT: {context.environment}

Steps: (1) Read {file}:{line} yourself; don't trust the description.
(2) Trace callers backwards; quote the first call-site file:line.
(3) Hunt for protections: validation, escaping, type bounds, auth gates,
dead/test code. (4) Stress-test each protection on every path.

Exclusion rules (FALSE_POSITIVE if matched): 1 volumetric DoS;
2 test/dead/fixture code; 3 intended design; 4 memory-safety in safe
lang outside unsafe/FFI; 5 SSRF path-only; 6 LLM prompt input;
7 object-storage traversal; 8 trusted operator env/CLI inputs;
9 client code, server vuln class; 10 outdated deps; 11 weak random
non-security; 12 low-impact nuisance (log spoof, open redirect, regex
inject); 13 missing-hardening-only, no concrete exploit; 14 XSS in
auto-escape framework w/o raw-HTML escape hatch; 15 unguessable
UUID/token flagged predictable; 16 theoretical-only race/TOCTOU.
{+ org rules from --fp-rules if any}

End with EXACTLY:
  VERDICT: TRUE_POSITIVE | FALSE_POSITIVE | CANNOT_VERIFY
  CONFIDENCE: <0-10>
  REFUTE_REASON: <doesnt_exist|already_handled|implausible_trigger|
    intentional_behavior|misread_code|duplicate|not_actionable|n/a>
  EXCLUSION_RULE: <1-16, org rule, or none>
  FIRST_LINK: <file:line or "none found">
  RATIONALE: <2-5 sentences, file:line cited>

FINDING: {id} {file}:{line} {category} (claimed {severity})
{title}
{description}
Vote {k}/{N}. Independent; do not seek other votes.

Findings with a file but no line get one verifier vote regardless of --votes (a file-level sweep is expensive and doesn't benefit from voting).

If any Task call returns status: "async_launched" instead of the verifier's text, the runtime backgrounded it (some runtimes do this automatically for large parallel batches). Pick one recovery and use it for the whole batch:

  • If completion notifications arrive in your conversation: parse each verifier's VERDICT block from its notification result as it lands. Do not end your turn until every vote is accounted for.
  • If notifications do not arrive: do not poll transcript files. Re-spawn the missing verifiers in a fresh Task batch (smaller shard size, e.g. 10) and use the synchronous results. The same recovery applies to the dedupe subagent in 2b and the ranking subagents in 4a.

3c. Tally votes

For each candidate, parse the trailing block from each of its N verifiers (tolerate code fences and whitespace). If a verifier errored, timed out, or produced no parseable VERDICT block, re-spawn it once. If the retry also fails, count that vote as cannot_verify with confidence: 0 and note "verifier_error" in refute_reasons. The remaining N-1 votes still decide.

Build:

  • vote_breakdown: {"true_positive": x, "false_positive": y, "cannot_verify": z}
  • confidence: mean CONFIDENCE across votes that agree with the majority, rounded to one decimal.
  • exclusion_rule: the modal EXCLUSION_RULE among FALSE_POSITIVE votes, else null.
  • refute_reasons: sorted unique REFUTE_REASON values from FALSE_POSITIVE votes.
  • first_links: unique FIRST_LINK values across all votes (reachability audit trail).
  • rationale: the RATIONALE from the highest-confidence vote on the winning side, verbatim.

Decide verdict:

  • Majority TRUE_POSITIVE → verdict: true_positive. Proceeds to Phase 4.
  • Majority FALSE_POSITIVE → verdict: false_positive. Skips Phase 4.
  • No majority (tie, or majority CANNOT_VERIFY):
    • Noise tolerance precisionverdict: false_positive; append "(split vote, dropped under precision policy)" to rationale.
    • Noise tolerance recallverdict: true_positive with verify_verdict: needs_manual_test. Proceeds to Phase 4.
    • Noise tolerance ask → collect all split findings and present them in one AskUserQuestion call at the end of Phase 3 (header: id + title, options: keep / drop), then apply the user's choices.

Build confirmed[] = candidates with verdict == true_positive.

Checkpoint: Write tool → ./.triage-state/_chunk.tmp:

{"phase": 3, "context": {...}, "findings": [ {all findings with verdict/vote_breakdown/confidence/refute_reasons/first_links/rationale/exclusion_rule} ], "confirmed": ["f001", "..."]}

Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.triage-state 3 verify --from ./.triage-state/_chunk.tmp

This is the most expensive checkpoint. When len(candidates) * votes exceeds ~40 and verifier spawns are sharded into sequential batches, additionally checkpoint per candidate as its votes are tallied:

  1. Write tool → ./.triage-state/_chunk.tmp = that finding's post-tally dict.
  2. Bash: python3 .claude/skills/_lib/checkpoint.py shard ./.triage-state <id> --from ./.triage-state/_chunk.tmp

On resume at phase_done == 2, the Phase-3 entry point reads progress.json:shards_done (default [] — do not glob shard files on disk; stale shards from a prior run may exist), loads the corresponding shard_{id}.json files, and spawns verifiers only for candidates[] ids from phase2.json that are NOT in shards_done. Once every candidate is in shards_done, write the consolidated phase3.json checkpoint as above.


Phase 4: Rank by exploitability (confirmed findings only)

Recompute severity from preconditions and reachability rather than category name, and judge the scanner's claimed severity separately. Verification and severity are independent judgments; "this is real" must not inflate into "this is critical."

4a. Ranking prompt

Spawn one Task per confirmed finding (subagent_type: "general-purpose", all in one message) with:

You are assigning severity to a CONFIRMED security finding. Verification
already happened; assume the finding is real. Your only job is to derive
how bad it is, independently of what the scanner claimed.

You may Read/Grep the codebase at {REPO_PATH} to check preconditions. Do
NOT execute code.

ENVIRONMENT: {context.environment}
THREAT MODEL (operator-stated, may be empty):
{context.threat_model as bullets, or "(none provided)"}
SCORING STANDARD: {context.scoring}

FINDING:
  id:        {id}
  file:      {file}:{line}
  category:  {category}
  claimed severity: {severity}
  reachability evidence: {first_links from Phase 3}
  verifier rationale: {rationale from Phase 3}

────────────────────────────────────────────────────────────────────────
STEP 1: Enumerate EVERY precondition that must hold for exploitation.
Be concrete: required auth state, configuration, prior request, race
window, attacker position. Then state the minimum ACCESS LEVEL required
(unauthenticated remote / authenticated / local / physical).

STEP 2: Derive severity from the precondition count and access level:

  | Preconditions | Access required          | Severity |
  |---------------|--------------------------|----------|
  | 0             | Unauthenticated remote   | HIGH     |
  | 1-2           | Authenticated            | MEDIUM   |
  | 3+            | Local-only / no demo path| LOW      |

  Evaluate each column independently and take the LOWER result. Example:
  0 preconditions but authenticated-only is MEDIUM, not HIGH; 1
  precondition but local-only is LOW. Cross-check: if your preconditions
  list has 3+ items, HIGH is almost certainly wrong.

STEP 3: Threat-model match. If the THREAT MODEL is non-empty and this
finding maps onto one of its entries, note which one. A match may raise
severity by ONE step (LOW to MEDIUM or MEDIUM to HIGH), never two. If the
threat model is empty, skip this step.

STEP 4: Judge the scanner's claimed severity. From the perspective of an
engineer who has reviewed two hundred scanner findings this week and is
allergic to inflation: would the CLAIMED severity contribute to alert
fatigue? Is it comparable to a real CVE at that level? Is the code in test
fixtures or dev-only config? Score in -5..+5:
  +3..+5  claimed severity is justified or understated
   0..+2  roughly right
  -1..-3  inflated by one level
  -4..-5  badly inflated (LOW dressed as HIGH)

STEP 5: verify_verdict. Exactly one of:
  exploitable        preconditions are realistically satisfiable
  mitigated          real, but a deployed control reduces it below the
                     derived severity (name the control)
  needs_manual_test  severity hinges on something only a runtime test can
                     settle; recommend a human build a PoC

STEP 6: If SCORING STANDARD is a CVSS or OWASP variant, emit a
`severity_label` in that format (vector string + base score for CVSS;
likelihood x impact for OWASP). Otherwise set it equal to the derived
HIGH/MEDIUM/LOW.

────────────────────────────────────────────────────────────────────────
Respond with ONLY this block:

  PRECONDITIONS:
  - <one per line>
  ACCESS_LEVEL: <unauthenticated_remote|authenticated|local|physical>
  SEVERITY: <HIGH|MEDIUM|LOW>
  SEVERITY_LABEL: <per scoring standard>
  THREAT_MATCH: <matched threat-model entry, or none>
  SEVERITY_ALIGNMENT: <-5..+5>
  VERIFY_VERDICT: <exploitable|mitigated|needs_manual_test>
  RANK_RATIONALE: <2-4 sentences>

4b. Merge

For each confirmed finding, parse the block and attach preconditions (replacing any scanner-supplied list), access_level, severity (recomputed), severity_label, threat_match, severity_alignment, verify_verdict, and append RANK_RATIONALE to rationale (separated by a blank line from the Phase-3 rationale).

For findings that did NOT reach Phase 4 (false_positive, duplicate, unlocatable): set severity: null, verify_verdict: null, severity_alignment: null, preconditions: [].

Checkpoint: Write tool → ./.triage-state/_chunk.tmp:

{"phase": 4, "context": {...}, "findings": [ {all findings with severity/severity_label/preconditions/access_level/threat_match/severity_alignment/verify_verdict} ]}

Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.triage-state 4 rank --from ./.triage-state/_chunk.tmp


Phase 5: Route

Tag each confirmed true-positive with the most specific component or owner inferable. For each finding in confirmed[], stop at the first hit:

  1. CODEOWNERS / OWNERS. Grep --repo for CODEOWNERS, OWNERS, .github/CODEOWNERS, docs/CODEOWNERS. If found, match the finding's file against its patterns (last match wins). Hint: "CODEOWNERS: <pattern> -> <owner(s)>".
  2. git log. If --repo is a git checkout, run git -C {REPO} log --format='%an' -n 50 -- "{file}" | sort | uniq -c | sort -rn | head -3. Hint: "top committer: <name> (<n>/<total> recent commits); no CODEOWNERS entry".
  3. Module fallback. Hint: "component: <top-level dir of file>/; no CODEOWNERS or git history".

Attach as owner_hint. State the source so confidence is clear; a bare username is less useful than "component: auth/; no CODEOWNERS entry; top committer jsmith (14/20 recent commits)". For non-true-positive findings, set owner_hint: null.

Checkpoint: Write tool → ./.triage-state/_chunk.tmp:

{"phase": 5, "context": {...}, "findings": [ {all findings with owner_hint} ]}

Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.triage-state 5 route --from ./.triage-state/_chunk.tmp


Phase 6: Output

6a. Sort

Order all findings by:

  1. verdict: true_positive, then duplicate, then false_positive.
  2. Within true positives: severity HIGH > MEDIUM > LOW, then confidence descending, then severity_alignment descending.
  3. Within others: original id.

6b. Write ./TRIAGE.json

{
  "triage_completed": true,
  "triage_context": {
    "mode": "interactive|auto",
    "environment": "...",
    "threat_model": ["..."],
    "scoring": "...",
    "noise_tolerance": "...",
    "votes_per_finding": 3,
    "repo": "..."
  },
  "summary": {
    "input_count": 0,
    "duplicates": 0,
    "false_positives": 0,
    "true_positives": 0,
    "needs_manual_test": 0,
    "by_severity": {"HIGH": 0, "MEDIUM": 0, "LOW": 0}
  },
  "findings": [
    {
      "id": "f001",
      "source": "VULN-FINDINGS.json#0",
      "title": "...",
      "file": "...",
      "line": 0,
      "category": "...",
      "claimed_severity": "HIGH",
      "verdict": "true_positive|false_positive|duplicate",
      "verify_verdict": "exploitable|mitigated|needs_manual_test|null",
      "confidence": 0.0,
      "severity": "HIGH|MEDIUM|LOW|null",
      "severity_label": "...",
      "severity_alignment": 0,
      "preconditions": ["..."],
      "access_level": "...",
      "threat_match": "...|null",
      "rationale": "file:line-cited prose: reachability, protections, why each held or didn't; then ranking rationale",
      "vote_breakdown": {"true_positive": 0, "false_positive": 0, "cannot_verify": 0},
      "refute_reasons": ["..."],
      "exclusion_rule": null,
      "first_links": ["file:line", "..."],
      "duplicate_of": null,
      "absorbed": ["..."],
      "owner_hint": "...",
      "missing_fields": ["..."]
    }
  ]
}

Every input finding appears exactly once (duplicates reference their canonical via duplicate_of). Do not silently drop anything. Do not print this JSON to the terminal; write to file only.

6c. Write ./TRIAGE.md

Reviewer-facing report. Build it incrementally. Do NOT emit the whole file in one Write. One chunk per finding; a stalled chunk loses that one section, not the file.

Step 1 — header. Write tool → ./TRIAGE.md (clobbers any prior file) containing only the title block, summary, and ## Act on these heading:

# Triage Report

{summary line: N in -> D duplicates, F false positives, T confirmed (H high / M med / L low), X need manual test}

Context: {mode}; environment = {environment}; scoring = {scoring}; {votes}-vote verification.

## Act on these

Step 2 — per finding. For each true_positive in severity order:

  1. Write tool → ./.triage-state/_chunk.tmp containing ONE finding's section:
### [{severity}] {title}  ({id})
`{file}:{line}` | {category} | claimed {claimed_severity} (alignment {severity_alignment:+d}) | confidence {confidence}/10
**Owner:** {owner_hint}
**Verdict:** {verify_verdict}, votes {vote_breakdown}
**Preconditions ({n}):** {bulleted}
**Threat-model match:** {threat_match or "none"}
**Why:** {rationale}
**Reachability evidence:** {first_links}
{if verify_verdict == needs_manual_test:}
> Recommend a human build a PoC; static reasoning hit its limit.
  1. Bash: python3 .claude/skills/_lib/checkpoint.py append ./TRIAGE.md --from ./.triage-state/_chunk.tmp

Repeat for each true_positive.

Step 3 — footer. Write tool → ./.triage-state/_chunk.tmp containing the Dropped table, then checkpoint.py append it the same way:

## Dropped

| id | title | file:line | why dropped |
{false_positives: refute_reasons + exclusion_rule}
{duplicates: "duplicate of {duplicate_of}"}
{unlocatable: "no source location in input"}

Checkpoint (final): Bash: python3 .claude/skills/_lib/checkpoint.py done ./.triage-state 6 The next invocation's resume check sees status == "complete" and starts fresh.

6d. Terminal summary

Under ~12 lines:

Triage complete: {N} findings -> {T} confirmed, {F} false positives, {D} duplicates.

  HIGH:   {n}   {title of top HIGH, owner_hint}
  MEDIUM: {n}
  LOW:    {n}
  Needs manual test: {n}

  Top refute reasons: {top 3 refute_reasons with counts}

Wrote ./TRIAGE.md and ./TRIAGE.json

Testing this skill

Smoke test (five-finding fixture: 2 real, 1 dup, 2 FP):

/triage .claude/skills/triage/fixtures/canary-findings.json --auto --repo targets/canary

Expected: f001 and f003 confirmed; f002 duplicate of f001; f004 dropped (misread_code: it's a read buffer, not a randomness source); f005 dropped (already_handled: there is a null check at line 68).

Or against pipeline output:

vuln-pipeline run drlibs --runs 3 --parallel --stream
/triage results/drlibs/<ts>/ --repo targets/drlibs

Hand-check a sample of TRUE_POSITIVE/HIGH results (the first_links should point at real call sites) and a sample of FALSE_POSITIVE rejects (the exclusion_rule or refute_reasons should be defensible).


Design notes

  • Checkpoints are per-phase JSON, not conversation state. The pipeline's --resume <session_id> (docs/pipeline.md) restores transcript history but doesn't help when the orchestrator's context window itself fills; file-backed checkpoints let a brand-new session pick up from the last completed phase. ./.triage-state/ is scratch — add to .gitignore.
  • Dedupe runs before verify to cut verifier spend by the duplication factor (often 2-4x on multi-scanner input) at the cost of one cheap subagent.
  • Semantic dedupe is one agent, given only id/file/line/category/title: enough to cluster, not enough to leak one scanner's reasoning into another finding's verification.
  • Bash is allowed narrowly for git log (owner hints), jq/find (ingest), and python3 .claude/skills/_lib/checkpoint.py (state I/O). The actual safety property is "no execution of target code," which is preserved.
  • CANNOT_VERIFY exists so verifiers aren't forced into a false binary. It maps to needs_manual_test under recall policy and to a drop under precision policy.
  • Threat-model boost is capped at one step so a stated threat can't re-inflate a LOW back to HIGH and defeat the precondition rule.
  • severity_label is separate from severity. Sorting always uses the precondition-derived HIGH/MEDIUM/LOW; the label is presentation-layer for whatever standard the reviewer's tooling expects.
  • Pipeline report.json ingest is best-effort. Those reports describe ASAN crashes with prose exploitability analysis rather than the file/line/category shape static verifiers expect. Expect more needs_manual_test verdicts on that input than on static-scanner JSON.
  • Sharding at ~40 parallel Tasks is a conservative ceiling for typical agent-spawn limits; tune up if your runtime allows.
  • No network, deliberately. CVE-database enrichment and upstream-fix checks would help ranking but break the air-gapped-review property.
对源码目录进行静态安全漏洞扫描,不执行代码。支持并行子代理审查,生成VULN-FINDINGS报告供后续处理。适用于需识别潜在安全风险、审计代码逻辑或遵循威胁模型的场景。
scan for vulns review this code for security issues find bugs in <dir>
.claude/skills/vuln-scan/SKILL.md
npx skills add anthropics/defending-code-reference-harness --skill vuln-scan -g -y
SKILL.md
Frontmatter
{
    "name": "vuln-scan",
    "description": "Static source-code vulnerability scan. Reads a target directory (and THREAT_MODEL.md if present), spawns parallel review subagents per focus area, and writes VULN-FINDINGS.json + .md for \/triage to consume. Read-only — no building, running, or network. For execution-verified crashes, use vuln-pipeline instead. Use when asked to \"scan for vulns\", \"review this code for security issues\", \"find bugs in <dir>\", or as the step between \/threat-model and \/triage.",
    "allowed-tools": [
        "Read",
        "Glob",
        "Grep",
        "Write",
        "Task",
        "Bash(rg:*)",
        "Bash(grep:*)",
        "Bash(ls:*)",
        "Bash(wc:*)",
        "Bash(head:*)",
        "Bash(file:*)"
    ],
    "argument-hint": "<target-dir> [--focus <area>] [--single] [--extra <file>] [--no-score]"
}

/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

  1. Resolve <target-dir>. If it doesn't exist or has no source files, stop with an error.
  2. 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.
  3. 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 as harness/prompts/recon_prompt.py.
  4. If --focus was 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

  1. Collect <finding> blocks from all subagents. Drop category=none placeholders.
  2. Light dedupe — if two findings cite the same file:line with 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.)
  3. 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:

  1. Counts: N findings (H/M/L split, X low-confidence), across K focus areas, from M source files.
  2. Top 3 by confidence, one line each.
  3. Next step: > /triage <target-dir>/VULN-FINDINGS.json --repo <target-dir>
  4. 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 at vuln-pipeline.
  • 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>. 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. /triage does 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.

将已验证的安全漏洞转换为候选补丁。读取Triage或扫描结果,生成不可执行的差异文件供人工审查,严禁直接修改代码库,支持断点续传和参数过滤。
修复发现的问题 修补这些漏洞 生成修复方案 闭环处理分类结果
.claude/skills/patch/SKILL.md
npx skills add anthropics/defending-code-reference-harness --skill patch -g -y
SKILL.md
Frontmatter
{
    "name": "patch",
    "description": "Generate candidate fixes for verified security findings. Consumes TRIAGE.json (preferred), VULN-FINDINGS.json, or a vuln-pipeline results directory. Pipeline input is delegated to the execution-verified `vuln-pipeline patch` ladder; static-analysis input gets a per-finding patch subagent + independent reviewer and is written as inert diffs for human review. Writes PATCHES\/bug_NN\/{patch.diff,patch_result.json}, PATCHES.md, and PATCHES.json. Use when asked to \"fix the findings\", \"patch these vulns\", \"generate fixes\", or \"close the loop on triage\".",
    "allowed-tools": [
        "Read",
        "Glob",
        "Grep",
        "Write",
        "Task",
        "Bash(python3 .claude\/skills\/_lib\/checkpoint.py:*)",
        "Bash(vuln-pipeline patch:*)",
        "Bash(rg:*)",
        "Bash(grep:*)",
        "Bash(ls:*)",
        "Bash(wc:*)",
        "Bash(head:*)",
        "Bash(file:*)",
        "Bash(jq:*)"
    ],
    "argument-hint": "<findings-path> [--repo PATH] [--top N] [--id fNNN] [--model M] [--fresh]"
}

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 pipeline results/<target>/<ts>/ directory, or any JSON the /triage ingest 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 to vuln-pipeline patch in 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 --fresh in $ARGUMENTS → fresh start. Bash: python3 .claude/skills/_lib/checkpoint.py reset ./.patch-state, proceed to Phase 0.
  • status == "running" with phase_done == N → resume. Read phase0.json..phaseN.json in order (and any shard_*.json listed in shards_done), merge into working state, print Resuming 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.jsonl OR found_bugs.jsonl OR run_*/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)

  1. TRIAGE.json — read .findings[]. Filter to verdict == "true_positive". This is the canonical input: already verified, deduped, ranked, owner-tagged.
  2. VULN-FINDINGS.json — read .findings[]. Unverified; print Warning: VULN-FINDINGS.json is unverified scanner output. Consider /triage first. and continue.
  3. Pipeline results directory — one finding per reports/bug_NN/. Map report.jsondescription, crash.crash_typecategory, ASAN top-frame → file/line. Record bug_id = NN for the --bug N delegate flag.
  4. Generic *.json with a top-level list or a findings/results/ issues/vulnerabilities array.

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 by severity HIGH > MEDIUM > LOW then confidence desc, keep the first N.
  • Drop findings with no file (cannot patch what cannot be located). Record them as skipped with 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 result as 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 (&lt; &gt; &amp; — 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, no patch, 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 it description, recommendation, exploit_scenario, or the patch author's rationale.
  • 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 patch isn'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 description field. 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.
  • verified is the verification class, not pass/fail. static_review_only means "an agent read it" regardless of ACCEPT/REJECT. ladder_passed/ladder_failed means "ASAN decided." Downstream tooling should branch on this field, not on review.
  • Output shape matches the pipeline (PATCHES/bug_NN/{patch.diff, patch_result.json}) so consumers don't care which mode produced it.
为代码库构建威胁模型,支持访谈、引导及组合模式。通过静态分析识别潜在风险与攻击面,生成标准化THREAT_MODEL.md文件,用于指导漏洞发现优先级和系统安全加固,不执行实际代码或网络请求。
要求对代码库进行威胁建模 询问应关注哪些安全风险 请求映射攻击面 需要评估系统潜在故障点
.claude/skills/threat-model/SKILL.md
npx skills add anthropics/defending-code-reference-harness --skill threat-model -g -y
SKILL.md
Frontmatter
{
    "name": "threat-model",
    "description": "Build a threat model for a target codebase. Three modes: \"interview\" walks an application owner through the four-question framework and produces a threat model from their answers; \"bootstrap\" derives a threat model from the code plus past vulnerabilities (CVEs, git history, pentest reports) when no owner is available; \"bootstrap-then-interview\" chains the two when both owner and codebase are present. All write THREAT_MODEL.md in a shared schema. Use when asked to \"threat model\", \"build a threat model\", \"map the attack surface\", or \"what should we be worried about in this codebase\".",
    "allowed-tools": [
        "Read",
        "Glob",
        "Bash(python3 .claude\/skills\/_lib\/checkpoint.py:*)",
        "Grep",
        "Write",
        "Bash(git:*)",
        "Bash(gh api:*)",
        "Bash(find:*)",
        "Bash(ls:*)",
        "Bash(cat:*)",
        "AskUserQuestion",
        "Task"
    ],
    "argument-hint": "[bootstrap-then-interview|bootstrap|interview] <target-dir> [--vulns <file>] [--design-doc <file>] [--seed <THREAT_MODEL.md>] [--fresh]"
}

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:

  1. The target directory exists and is a local checkout you can read.
  2. You will not execute any code from the target directory.
  3. If --vulns points 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.md or bootstrap.md in 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.

  1. 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.
  2. Read bootstrap.md and follow it end-to-end. Write <target-dir>/THREAT_MODEL.md.
  3. Immediately continue into interview mode: read interview.md and follow it with --seed <target-dir>/THREAT_MODEL.md in effect. The section 6 open questions from bootstrap become your Q1-Q4 prompts; the owner confirms, corrects, and adds rather than starting from nothing.
  4. Overwrite <target-dir>/THREAT_MODEL.md with the refined model. Set provenance mode: 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:

  1. The path to THREAT_MODEL.md.
  2. The top 5 threats by likelihood × impact (id, one-line description, L×I).
  3. For bootstrap: any open questions the code could not answer (these seed a later interview pass).
  4. For interview: any owner statements that could not be verified in code (these seed follow-up code review).

References

Главная - Вики-сайт
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-08 20:17
浙ICP备14020137号-1 $Гость$