triage

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 f

By anthropics · 979 installs

npx skills add anthropics/defending-code-reference-harness --skill triage

Source repository · Upstream listing

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 , an INCIDENTS.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.json — single 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 $ARGUMENTS → fresh start. Bash: python3 .claude/skills/ lib/checkpoint.py reset ./.triage state , then proceed to Phase 0. status == "running" with phase done == N → resume. 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 : 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[] . INCIDENTS.json (the /dnr hunt / /dnr respond output): read .incidents[] ; each incident is one finding. Map the nested fields — vuln.file → file , vuln.line → line , vuln.type → category , vuln.function → function , description ← vuln.mechanism plus impact — and keep id , title , verdict , and poc ; the 1b aliases route poc → exploit scenario , and a confirmed exploited verdict means the incident arrived execution verified, not statically guessed — verifiers should weight that. The top level ruled out array is not findings — don't ingest it. Incidents with no vuln.file follow the 1b missing file rule like any other finding. reports/bug /report.json or reports/manifest.jsonl (this repo's pipeline output): one finding per bug NN . Map crash.crash type → category , verdict.severity rating → severity , the prose report → description , 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"] , rational