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