Skip to main content

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 findings", "validate scanner output", "prioritize vulns", or "review the backlog". Runs interactively by default; pass --auto to skip the interview.

Source facts

Repository
anthropics/defending-code-reference-harness
Last source activity
July 16, 2026 at 16:35
Detected SKILL.md language
English
Stars
7,566
Forks
619

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
3 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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.
argument-hint
<findings-path> [--auto] [--votes N] [--repo PATH] [--fp-rules FILE] [--fresh]
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:*)"]
# 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`: ```json {"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[]`. - `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"]`, `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`: ```json
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub