
Threat Model
- 3 installs
- 6.9k repo stars
- Updated August 4, 2026
- anthropics/defending-code-reference-harness
threat-model is a Claude Code skill that builds a threat model for a target codebase via interview, code-plus-vulnerability bootstrap, or a chained mode, writing THREAT_MODEL.md.
About
This skill builds a threat model for a target codebase, answering what could go wrong, who would do it, and what to do about it. It offers interview mode (walks an owner through a four-question framework), bootstrap mode (derives the model from code plus past vulnerabilities when no owner is available), and a chained mode. It performs static analysis only and writes THREAT_MODEL.md in a shared schema that downstream scan and triage steps consume.
- Builds a threat model for a target codebase in interview, bootstrap, or chained mode
- Static analysis only; reads source, git history, and supplied vuln reports
- Writes THREAT_MODEL.md in a shared schema for downstream scan and triage
Threat Model by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,752 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
threat-model capabilities & compatibility
- Capabilities
- security audit · vuln scan
- Works with
- github
What threat-model says it does
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.
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`).
A good threat model tells the pipeline where to look and tells triage which findings matter.
npx skills add https://github.com/anthropics/defending-code-reference-harness --skill threat-modelAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 6.9k |
| Last updated | August 4, 2026 |
| Repository | anthropics/defending-code-reference-harness ↗ |
What it does
Build a THREAT_MODEL.md for a codebase via interview or code-plus-CVE bootstrap to steer downstream vuln scanning and triage.
Who is it for?
Mapping the attack surface and threats of a codebase to steer where the pipeline looks and which findings matter.
Skip if: Validating a threat by running an exploit; it does static analysis only and points to the vuln-pipeline for execution.
When should I use this skill?
Asked to 'threat model', 'build a threat model', 'map the attack surface', or 'what should we be worried about in this codebase'.
What you get
- THREAT_MODEL.md in the shared schema
By the numbers
- 3 modes (interview, bootstrap, bootstrap-then-interview)
- four-question interview framework
- STRIDE gap-fill in bootstrap mode
Files
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.mdorbootstrap.mdin full up front. Read the
mode file (or the relevant section of it) at the point you need it, one question or stage at a time.
- If a re-read via the Read tool is refused as "file unchanged", the prior
result was evicted; reload with cat <path> via Bash instead.
Interview backbone (so you can proceed even if interview.md is unavailable mid-session):
| Q | Question | Fills schema sections |
|---|---|---|
| Q1 | What are we working on? | section 1 context, section 2 assets, section 3 entry points |
| Q2 | What can go wrong? | section 4 threat rows (id, threat, actor, surface, asset) |
| Q3 | What are we going to do about it? | section 4 impact/likelihood/status/controls; section 5 deprioritized; section 8 recommended mitigations |
| Q4 | Did we do a good job? | validate ranking, coverage check, section 6 open questions |
bootstrap-then-interview mode
When the owner is available and the codebase is checked out, this is the recommended path: the owner's time goes to refining a code-grounded draft instead of describing the system from scratch.
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
- docs/security.md and
docs/prompting.md for the engagement-context and authorization framing this skill inherits.
/threat-model bootstrap
Re-read note: If you need this file mid-session and the Read tool
reports "file unchanged", the prior result was evicted from context; reload
with cat .claude/skills/threat-model/bootstrap.md via Bash.Derive a threat model from code + past vulnerabilities when no application owner is available. Five stages: spawn a parallel research swarm, synthesize its findings into sections 1-3 and a vuln working table, generalize vulns into threat classes, gap-fill with STRIDE, emit THREAT_MODEL.md per schema.md.
This mode is read-only static analysis and is language-agnostic: the same stages apply whether the target is C/C++, Rust, Go, Python, Java/Kotlin, JavaScript/TypeScript, or polyglot. Do not build, run, or fuzz the target. The Bash tool is permitted only for git (history mining), find/ls (layout), gh api (public advisory lookup), and cat (re-reading skill files). Do not execute anything from inside <target-dir>. The same restriction applies to every subagent you spawn: pass it verbatim in each prompt.
---
Inputs
<target-dir>(required): local checkout.--vulns <path>(optional): past vulnerabilities. Any of:- newline-separated CVE IDs (
CVE-2026-29022) - CSV with columns
id,title,component,description(extra columns ignored) - markdown pentest report (parse headings + body for finding descriptions)
- JSON array of objects with at least
idanddescriptionkeys --depth recon|full(optional, defaultfull):reconruns stages 1-2
only. Still write all eight sections (schema requires sections 1-7; section 8 optional); leave section 4, section 5, and section 8 as header + empty table, and put "run with --depth full to populate" in section 6. Use for fast context-building before a deeper pass.
If --vulns is absent, the Vuln-file parser agent is skipped; the History miner and Advisory fetcher agents in the Stage-1 swarm cover the same ground from <target-dir>'s own git history and public advisories.
--fresh(optional): ignore any existing checkpoint in
./.threat-model-state/ and start from Stage 1.
---
Checkpointing (runs before Stage 1 and after every stage)
On large codebases the Stage-1 swarm can exhaust context or hit rate limits before Stage 5 emits THREAT_MODEL.md. Stage state persists to ./.threat-model-state/ (in the current working directory, not <target-dir>) so a fresh /threat-model bootstrap session can resume without re-spawning the swarm. The state dir is cwd-relative because checkpoint.py confines all paths to cwd as a guard against prompt-injected writes outside the repo.
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 ./.threat-model-state/:
progress.json— single source of truth for resume position:
{"status": "running"|"complete", "stage_done": N}. Resume decisions read ONLY this file.
stageN.json— data payload for stage N (schemas at the tail of each stage
below).
_chunk.tmp— transient payload buffer; overwritten before every
save/append call.
Start of run — resume check. Bash: python3 .claude/skills/_lib/checkpoint.py load ./.threat-model-state
status == "absent"OR"complete", OR--freshin$ARGUMENTS→
fresh start. Bash: python3 .claude/skills/_lib/checkpoint.py reset ./.threat-model-state, then proceed to Stage 1.
status == "running"withstage_done == N→ resume. Read
stage1.json through stageN.json in order, merging keys into working state (later files override earlier — checkpoints may be deltas). Print Resuming from checkpoint: Stage N complete, and skip directly to Stage N+1.
End of every stage N. Two tool calls: 1. Write tool → ./.threat-model-state/_chunk.tmp containing the stage's output JSON. 2. Bash → python3 .claude/skills/_lib/checkpoint.py save ./.threat-model-state <N> <name> --key stage --from ./.threat-model-state/_chunk.tmp
End of run. After writing <target-dir>/THREAT_MODEL.md, Bash: python3 .claude/skills/_lib/checkpoint.py done ./.threat-model-state 5 --key stage
---
Stage 1 — Research swarm
Goal: gather everything needed to fill sections 1-3 and the vuln working table, in parallel. Spawn the agents below in a single batch with the Task tool so they run concurrently. Each agent gets a narrow brief, the absolute path to <target-dir>, and the read-only restriction verbatim. Each returns a structured text block; you synthesize in Stage 2.
Skip the swarm and run the briefs yourself sequentially if <target-dir> is small (<50 source files) or --depth recon is set; the parallelism isn't worth the overhead there.
| Agent | Brief | Returns |
|---|---|---|
| Docs reader | Read README*, SECURITY.md, CHANGELOG*, top-level docs/, and the build manifest (setup.py / Cargo.toml / package.json / CMakeLists.txt). Summarize what the project says it is, who uses it, and any security claims or fix entries it documents. | Prose system description; list of self-documented security fixes. |
| Surface mapper | Grep the source tree for entry-point signatures (table below). For each hit, name the surface, the file:function, and what crosses it. Include supply-chain surfaces (lockfiles, vendored deps, `curl \ | sh in build scripts). Bound the scan: exclude vendor/, node_modules/, third_party/`, generated code; cap at ~5 representative hits per surface row. |
| Infra reader | Read deploy-time config: *.tf/*.tfvars, k8s manifests (*.yaml under k8s//deploy//manifests/), Dockerfile*, CI workflows, and any IAM/service-account/dataset-ACL files. For each, name (a) the identity it runs as and what that identity can reach, (b) any access grant not managed in this tree (ad-hoc IAM, hand-created SAs, missing column/policy tags), (c) credentials or principals that survive a migration or teardown. | Candidate section 3 rows for infra surfaces + candidate section 4 rows: {threat, surface, asset} where the config itself is the finding. |
| Asset finder | Identify what the code protects or produces: sensitive data it reads/writes (secrets, keys, user records, DBs), process integrity (always present for native code), service availability, and downstream embedder assets if it's a library. | Candidate section 2 rows: {asset, description, sensitivity}. |
| History miner | Two steps. (a) Glance at the build manifest and file extensions to identify language and domain, then derive 6-10 commit-message keywords specific to that stack on top of the base set CVE- security vuln fix exploit. Derive from what the code does, not from a lookup table; the three examples below illustrate the specificity bar, not coverage: native parser → overflow OOB UAF integer; web service → injection SSRF IDOR traversal; crypto → timing constant-time nonce. (b) `git -C <target-dir> log --all -i --grep='<base ∪ derived, \ | -joined>' --oneline, then read the full message + diff of each hit. Also grep any issues/ or bugs/` export in-tree. |
| Advisory fetcher | If git -C <target-dir> remote get-url origin is GitHub and gh is on PATH: gh api /repos/{owner}/{repo}/security-advisories. Otherwise return "no public advisory source". | Vuln rows: {id (CVE/GHSA), title, component, class, vector}. |
| Vuln-file parser | Only spawn if --vulns <path> was provided. Parse the file (newline CVE list, CSV, markdown report, or JSON array) into normalized rows. | Vuln rows: {id, title, component, class, vector}. |
Surface-mapper grep targets (pass this table in its prompt). Treat the "Look for" column as a seed, not a checklist: one concrete token per row to set the specificity bar, then extend with the idioms of whatever language/framework the target actually uses.
| Surface | Look for |
|---|---|
| Network | socket listen/accept/bind; HTTP route definitions (e.g., @app.route); RPC/gRPC/GraphQL service defs |
| File / format parsing | file-open calls (e.g., open(); format magic-byte checks; "parse"/"decode"/"load"/"unmarshal" function names |
| CLI / env | argv parsers (e.g., argparse); env reads (e.g., getenv) |
| Deserialization | language-native deserializers on external data (e.g., pickle, ObjectInputStream) |
| DB / query | raw query-string construction; ORM .raw()/.query() escapes |
| IPC / plugins | dynamic load (e.g., dlopen); subprocess spawn; eval/exec on config; dynamic import |
| Supply chain | dependency lockfiles; vendored libs; `curl \ |
| Infra / IAM | terraform google_*_iam_*/aws_iam_*; k8s serviceAccountName/WIF annotations; BigQuery dataset/table access{} blocks; secrets mounts |
Checkpoint: Write tool → ./.threat-model-state/_chunk.tmp:
{
"stage": 1,
"swarm": {
"docs_reader": "<returned text block>",
"surface_mapper": [ {entry_point, description, trust_boundary, file_refs} ],
"infra_reader": { "surfaces": [...], "threats": [...] },
"asset_finder": [ {asset, description, sensitivity} ],
"history_miner": [ {id, title, component, class, vector} ],
"advisory_fetcher": [ {id, title, component, class, vector} ],
"vuln_file_parser": [ {id, title, component, class, vector} ]
}
}Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.threat-model-state 1 swarm --key stage --from ./.threat-model-state/_chunk.tmp
This is the most expensive checkpoint — the swarm is the majority of the mode's token spend. Agents that were skipped (no --vulns, no GitHub remote, --depth recon) get an empty list/null. If the swarm was run inline (small target), populate the same keys from your own sequential passes.
---
Stage 2 — Synthesize
Goal: turn the swarm returns into ## 1-3 of the schema plus a vuln working table. This stage runs in the orchestrating agent, not a subagent; it's the join.
Section 1: System context. From the Docs reader's summary plus your own glance at the tree layout, write 1-2 paragraphs: what it is, language, rough size, who would embed or deploy it, where it would run.
Section 2: Assets. Take the Asset finder's rows. Dedupe, fill any obvious gaps (native code without "host process integrity" → add it), assign sensitivity.
Section 3: Entry points & trust boundaries. Merge Surface mapper + Infra reader rows. Dedupe, name the trust boundary for each ("untrusted file → process memory", "unauth HTTP → application logic", "namespace workload → WIF identity"), and for each list which section 2 assets are reachable from it. Supply-chain, build-time, and infra/IAM surfaces are entry points even though no runtime input crosses them. Every row here must get at least one threat in Stage 3 or 4; that's the coverage invariant the emit-time check enforces.
Vuln working table. Concatenate rows from History miner + Advisory fetcher + Vuln-file parser. Dedupe by id. For each row, decide which section 3 entry point it traversed; read the relevant source to confirm (e.g., "CVE-2026-29022 is in drwav__read_smpl, reached via the WAV file-parsing entry point"). If a vuln's entry point isn't in section 3, the Surface mapper missed one; add it now. Hold this table in your working notes; it does not go into THREAT_MODEL.md verbatim. It becomes the evidence column in Stage 3.
Checkpoint: Write tool → ./.threat-model-state/_chunk.tmp:
{
"stage": 2,
"section1_context": "<markdown prose>",
"section2_assets": [ {asset, description, sensitivity} ],
"section3_entry_points": [ {entry_point, description, trust_boundary, reachable_assets} ],
"vuln_table": [ {id, title, component, class, vector, entry_point} ]
}Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.threat-model-state 2 synthesize --key stage --from ./.threat-model-state/_chunk.tmp
---
Stage 3 — Generalize: vulns → threats
Goal: cluster Stage-2 vulns into threat rows at the right abstraction level.
3a. Cluster
Group the Stage-2 vuln table by (entry point, bug class, asset reached). Each cluster becomes one candidate threat. Examples:
- 3 heap overflows + 1 integer overflow, all in WAV/FLAC parsers, all
reaching process memory → one threat: "Memory corruption leading to RCE via untrusted audio file parsing". Evidence: all 4 IDs.
- 2 SQL injections in different endpoints → one threat: "Data exfiltration
/ tampering via SQL injection in HTTP API". Evidence: both IDs.
Apply the litmus test to each cluster's threat statement: would it still be true after every listed evidence item is patched? If not, you're still at vuln level; zoom out.
3b. Variant scan (raises likelihood)
For each cluster, look for siblings: code paths with the same shape that weren't in the vuln list. Grep for the same pattern (other format parsers, other endpoints calling the same unsafe helper, other size fields multiplied without overflow checks). You are not trying to prove these are exploitable; you are estimating how much of the surface shares the pattern. More siblings → higher likelihood.
Keep sibling locations in your working notes and surface them in the hand-back (Stage 5, item 4). Do not put file:func references in the section 4 evidence cell; evidence is for confirmed past vulns only. Sibling counts inform the likelihood score, not the evidence column.
3c. Score
For each cluster, assign:
actor: from the entry point (file parsing → whoever supplies the file;
network endpoint → remote_unauth or remote_auth depending on whether auth precedes it).
impact: from the asset and the bug class (memory corruption on a network
service → critical; info leak of non-sensitive data → low).
likelihood: start from the evidence. ≥1 confirmed past vuln in this exact
surface → at least likely. Public exploit or active exploitation → almost_certain. No evidence, but siblings found and technique is well known → possible. Adjust down for controls.
controls: grep for mitigations relevant to the stack (size caps, input
validation, sandboxing/seccomp; ASLR/stack-protector/CFI in native builds; parameterized queries / ORM; auth middleware / CSRF tokens / CSP; rate limiting; SecurityManager/JEP-411 replacements in Java; etc.). none if none found.
status:unmitigatedunless you found a control that fully closes it.recommended_mitigation(working notes, not a section 4 column): for each cluster,
name one class-level control that would close or materially shrink the whole threat regardless of which instance is found next (e.g., "sandbox the decoder process", "parameterized queries everywhere", "drop pickle for json", "enable CSP default-src 'self'", "size-cap all length fields before allocation"). Prefer a control that survives the next bug over a patch for the last one. These become section 8 rows in Stage 5.
Write each cluster as a section 4 row.
Checkpoint: Write tool → ./.threat-model-state/_chunk.tmp:
{
"stage": 3,
"section1_context": "...",
"section2_assets": [...],
"section3_entry_points": [...],
"section4_threats": [ {threat, actor, surface, asset, impact, likelihood, status, controls, evidence} ],
"mitigation_notes": [ {cluster, recommended_mitigation} ],
"sibling_locations": [ {threat, locations: ["file:func", ...]} ]
}Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.threat-model-state 3 generalize --key stage --from ./.threat-model-state/_chunk.tmp
---
Stage 4 — Gap-fill (the part past vulns can't give you)
Past vulnerabilities are biased toward what's already been found. A threat model must also cover what hasn't. For every section 3 entry point that has no section 4 row yet, walk STRIDE and add at least the plausible ones:
| For this entry point, could an attacker… | |
|---|---|
| Spoofing | …pretend to be a trusted source? |
| Tampering | …modify data in transit or at rest? |
| Repudiation | …act without leaving attributable logs? |
| Info disclosure | …read data they shouldn't? |
| DoS | …exhaust a resource (CPU, memory, disk, connections)? |
| Elevation | …end up with more privilege than they started with? |
Also walk the entry points that do have rows: is the existing row the only plausible threat, or are other STRIDE categories live too? (A file parser with an RCE threat probably also has a DoS threat.)
For infra/IAM entry points (from the Infra reader), STRIDE maps less cleanly than for code. Walk these instead:
- Over-grant: does the identity reach more than the app needs (whole
dataset vs one table; project-level vs resource-level)?
- Lateral identity: can a co-located workload (same namespace, same node,
same SA) assume this identity?
- Drift: is any grant managed outside this tree (click-ops IAM, ad-hoc
ACL, unmanaged SA), so it won't be reviewed or torn down with the code?
- Residual access: do credentials or principals from a predecessor system
survive the migration?
- Column exposure: does a broad table read expose identity/PII columns
the app doesn't need?
- Scope enforcement: where an automated approval, merge, or write path
exists, what bounds it to its intended scope (path allowlist, label, reviewer set)?
Threats added in this stage have empty evidence. That's fine; score likelihood from technique prevalence and surface reachability alone. The final section 4 table must contain at least one row with empty evidence, or this stage didn't run.
Populate ## 5. Deprioritized with STRIDE categories you considered and ruled out, with the reason ("Repudiation: not applicable, no multi-user actions").
Checkpoint: Write tool → ./.threat-model-state/_chunk.tmp:
{
"stage": 4,
"section1_context": "...",
"section2_assets": [...],
"section3_entry_points": [...],
"section4_threats": [ {...stage3 rows + gap-fill rows with empty evidence} ],
"section5_deprioritized": [ {threat, reason} ],
"mitigation_notes": [...],
"sibling_locations": [...]
}Then Bash: python3 .claude/skills/_lib/checkpoint.py save ./.threat-model-state 4 gap-fill --key stage --from ./.threat-model-state/_chunk.tmp
---
Stage 5 — Emit
Coverage check (do this before writing the file). For every section 3 entry point, confirm at least one section 4 row names it in the surface column. Match on the entry-point's name string, not the concept; the downstream scorer is a text match. Any section 3 row with zero section 4 coverage means Stage 4 was incomplete; go back and add the missing threat now.
Sort section 4 by (impact desc, likelihood desc). Assign id = T1, T2, … in sorted order.
Populate ## 6. Open questions with everything the code couldn't tell you:
- Deployment context ("Is this exposed to the network or only local?")
- Intended actors ("Who supplies the input files in practice?")
- Controls you couldn't verify ("Is there a WAF / sandbox / size limit
upstream of this?")
- Risk appetite ("Is DoS acceptable for this use case?")
These seed a later /threat-model interview --seed THREAT_MODEL.md pass.
Populate ## 8. Recommended mitigations from the Stage-3c working notes: one row per class-level mitigation, listing the threat_ids it covers, whether it closes_class (yes/partial), and a rough effort (S/M/L). If two clusters are closed by the same control, emit one row with both clusters' ids. Gap-fill threats from Stage 4 get rows here too where an obvious class-level control exists.
Assemble the file incrementally in ./.threat-model-state/THREAT_MODEL.md (one chunk per ## N. section; a stalled chunk loses that section, not the file), then copy the assembled result to <target-dir>/THREAT_MODEL.md in one Write. The assembly happens in cwd because checkpoint.py append is cwd-confined; the final Write tool call is not.
1. Write tool → ./.threat-model-state/THREAT_MODEL.md (clobbers any prior file) containing only the title line and ## 1. Context section. 2. For each remaining section (## 2. Assets, ## 3. Entry points, ## 4. Threats, ## 5. Deprioritized, ## 6. Open questions, ## 7. Provenance, ## 8. Recommended mitigations):
- Write tool →
./.threat-model-state/_chunk.tmpcontaining that ONE
section's markdown.
- Bash:
python3 .claude/skills/_lib/checkpoint.py append ./.threat-model-state/THREAT_MODEL.md --from ./.threat-model-state/_chunk.tmp 3. Read tool → ./.threat-model-state/THREAT_MODEL.md, then Write tool → <target-dir>/THREAT_MODEL.md with the same content.
Set ## 7. Provenance:
- mode: bootstrap
- date: <today>
- target: <target-dir> @ <git rev-parse --short HEAD or "not a git repo">
- inputs: <--vulns path, or "git-log + CHANGELOG mined">
- owner: unsetCheckpoint (final): After the file is on disk, Bash: python3 .claude/skills/_lib/checkpoint.py done ./.threat-model-state 5 --key stage
Hand back to the user:
1. Path to the file. 2. Top 5 threats (id, threat, impact × likelihood). 3. Count of threats with evidence vs without (shows gap-fill ran). 4. Stage-3b sibling locations as candidate leads for /vuln-scan or the pipeline find stage. 5. The section 8 recommended mitigations, top 3 by (closes_class, effort asc). 6. The section 6 open questions, framed as "ask the owner".
/threat-model interview
Re-read note: If you need this file mid-session and the Read tool
reports "file unchanged", the prior result was evicted from context; reload
with cat .claude/skills/threat-model/interview.md via Bash.Build a threat model by interviewing the application owner using the four-question framework. The owner is in the session; your job is to ask, listen, ground their answers in the code where you can, and emit THREAT_MODEL.md per schema.md.
The four questions (use this exact wording when you introduce each phase; the phrasing is deliberate):
1. What are we working on? 2. What can go wrong? 3. What are we going to do about it? 4. Did we do a good job?
Reference: Shostack, The Four Question Framework for Threat Modeling (2024).
---
Inputs
<target-dir>(required): local checkout. You will read it to ground
answers; you will not execute it.
--design-doc <path>(optional): architecture or design document. Read it
before asking Q1 so you can summarize back instead of starting cold.
--seed <THREAT_MODEL.md>(optional): a priorbootstrapoutput. If
present, the interview focuses on its ## 6. Open questions and any threat rows with uncertain likelihood, instead of building from scratch.
---
Provenance discipline
Every fact you write into THREAT_MODEL.md carries one of two tags in your working notes:
[Code-verified]— you read the source in<target-dir>and confirmed it.[Owner-states]— the owner told you and you have not (or cannot) verify it
in code.
The final THREAT_MODEL.md does not include the tags inline (they would clutter the table), but every [Owner-states] fact that affects a likelihood or status score MUST be listed in ## 6. Open questions as a follow-up to verify. This is how an interview-mode threat model stays honest about what is asserted versus observed.
---
Method
Work through the four questions in order. Within each, ask one thing at a time, wait for the answer, then move on. Do not dump a questionnaire.
Q1 — What are we working on?
Goal: fill ## 1. System context, ## 2. Assets, ## 3. Entry points & trust boundaries.
If --design-doc was provided: read it, then summarize the system back to the owner in 4-6 sentences and ask "Is this right? What did I miss?" This is faster than asking them to describe it cold and surfaces drift between doc and reality.
If no design doc: ask directly. Prompts, in order:
- "In two or three sentences, what does this system do and who uses it?"
- "What data does it hold or pass through that would be bad to lose, leak, or
tamper with?" → assets table.
- "Where does input come from? Walk me from the outside in: network, files,
CLI, other services, anything a user or another system hands you." → entry points.
- "Where does privilege change? Unauth to auth, user to admin, one service
trusting another?" → trust boundaries.
While the owner answers, read the code in <target-dir> to corroborate: look for main, route definitions, file-open calls, socket listeners, deserializers, argv parsing. Where code confirms the owner, tag [Code-verified]. Where code shows an entry point the owner did not mention, ask about it: "I see a /admin/debug route in routes.py:88; is that reachable in production?"
If --seed was provided: read its sections 1-3, summarize back, and ask only "What's wrong or missing here?"
Q2 — What can go wrong?
Goal: fill ## 4. Threats rows (id, threat, actor, surface, asset).
Start open: "For each of those entry points, what can go wrong? What's the worst thing someone could do?" Let the owner answer in their own words first. Capture each answer as a candidate threat row.
When the owner stalls or stays vague, switch to structured prompts. Walk each entry point from section 3 through STRIDE:
| Ask | |
|---|---|
| Spoofing | "Could someone pretend to be a user or service they're not, here?" |
| Tampering | "Could input or stored data be modified in transit or at rest?" |
| Repudiation | "If someone did something bad here, would you know who?" |
| Information disclosure | "Could this leak data it shouldn't?" |
| Denial of service | "Could someone make this unavailable or too expensive to run?" |
| Elevation of privilege | "Could someone end up with more access than they started with?" |
Then derive the domain-specific classes. From the section 1 context (stack, language, deployment, data flows), name the 5-8 attack classes most likely to matter for this system. Derive from what the owner described, not from a generic checklist. Name classes at the granularity of "IDOR on dataset rows" or "integer overflow on length fields", not "web vulnerabilities" or "memory bugs".
Show the derived list to the owner: "Based on what you've described, these are the classes I'd focus on. Anything you'd add from incidents you've seen here or on similar systems?" Their additions are high-signal; weight them above your own. If a class you'd expect for this stack (injection, deserialization, auth, memory safety, crypto, supply chain, infra/IAM) didn't make either list, ask the owner why before dropping it.
Walk each section 3 entry point through STRIDE plus the derived-and-confirmed classes. For each candidate threat, pin down: actor (who, from the enum in schema.md), surface (which section 3 entry point), asset (which section 2 row). Phrase the threat at the level where it survives a patch: "RCE via untrusted WAV parsing", not "missing bounds check at line 412".
If --seed was provided: walk the seed's section 4 table row by row and ask "Does this apply? Is the actor right?" Then ask "What's missing?"
Q3 — What are we going to do about it?
Goal: fill impact, likelihood, status, controls for every section 4 row, and fill ## 5. Deprioritized.
For each threat row, ask:
- "What's in place today that stops or limits this?" →
controls. Verify in
code where possible ([Code-verified] vs [Owner-states]).
- "If it happened anyway, how bad is it?" →
impact(read them the scale
from schema.md if needed).
- "How likely is it that someone tries and succeeds, given the controls?" →
likelihood. If past incidents, CVEs, or pentest findings exist for this surface, list them in evidence and weight likelihood up.
- "Is this mitigated, partially mitigated, unmitigated, or are you accepting
the risk?" → status. If the owner says "risk accepted", capture their reason verbatim and put the row in section 5 with that reason.
The answer to Q3 is allowed to be "nothing, and we're not going to": deprioritized threats with a recorded reason are a valid output. "Threat modeling can result in knowing what we're not going to do and why."
After scoring, ask one closing question per threat class (not per row): "If we could land one engineering control that makes this whole class go away or shrink, what would it be?" Record the answer (or your own proposal if the owner punts) as a section 8 row: mitigation | threat_ids | closes_class | effort. Prefer controls that survive the next bug (sandboxing, type-safe parsers, parameterized queries, CSP, allocation caps) over patches for the last one.
Q4 — Did we do a good job?
Goal: validate before writing.
- Read the draft section 4 table back to the owner, sorted by impact × likelihood.
Ask: "Does the top of this list match your gut? Is anything ranked too high or too low?" Adjust.
- Ask: **"Is there anything you've been worried about that isn't on this
list?"** Add it.
- Check coverage yourself: for every row in section 3, the
entry_pointname must
appear verbatim in at least one section 4 surface cell, OR a section 5 row must say "<entry_point>: out of scope because …". If neither, either add a threat for that surface or ask the owner why it's safe and record the answer in section 5.
- Ask: **"Would you do this again for the next service? What would make it
easier?"** Record the answer in your hand-back to the user (not in the file); it's feedback for this skill.
---
Emit
Write <target-dir>/THREAT_MODEL.md per schema.md. Set ## 7. Provenance:
- mode: interview
- date: <today>
- target: <target-dir> @ <git rev-parse HEAD if available>
- inputs: <design-doc path or "none">; <seed path or "none">
- owner: <name the user gave, or "present, unnamed">Then hand back to the user:
1. Path to the file. 2. Top 5 threats by impact × likelihood, one line each. 3. The section 8 recommended mitigations, top 3 by (closes_class, effort asc). 4. Every [Owner-states] claim that affects a score, as a follow-up list. Format each as a section 6 bullet: - [Owner-states] <claim>. Affects: <Tn field>. Verify by: <suggested check>. 5. If --seed was provided: a short diff summary ("added T7-T9, downgraded T2 likelihood from likely → possible because owner confirmed input is size-capped").
threat-model
A Claude Code skill that builds a threat model for a target codebase. Two modes: bootstrap derives the threat model from the target itself (source tree, git history, public advisories, an optional past-vulns file); interview discovers the threat model by walking an application owner through the four-question framework. Both write THREAT_MODEL.md in a shared schema.
Status
The skill is read-only (it does not build, run, or probe the target) and is safe to point at any local checkout. The output is a starting point for human review, not a substitute for it.
Why a threat model
Vulnerability scanners find instances; a threat model is the map of where instances are likely to be and which ones matter. Hand the pipeline a threat model and it knows where to look. Hand triage a threat model and it knows which findings to escalate. Use the output's focus areas to seed the vuln-pipeline recon partition and to inform how you prioritize /triage results.
Model selection
The skill has no model: frontmatter pin; it runs on whatever model your session uses (or --model if you pass one). It is designed for reasoning-capable Claude models — use the same model you run the rest of the pipeline with. If you want to lock the model regardless of session, add a model: line to SKILL.md; frontmatter takes precedence over /model and --model.
Installation
Project-scoped (already done if you cloned this repo):
ls .claude/skills/threat-model/User-scoped:
cp -r .claude/skills/threat-model ~/.claude/skills/Usage
Bootstrap (derive from target, git history, advisories)
Use when no application owner is available. Point it at a checkout and, optionally, a list of past vulnerabilities:
/threat-model bootstrap targets/drlibs
/threat-model bootstrap targets/drlibs --vulns targets/drlibs/vulns.txtWithout --vulns the skill mines git log, CHANGELOG, and GitHub Security Advisories itself; with it, it ingests your supplied list first.
The skill spawns a parallel research swarm (docs reader, surface mapper, asset finder, git-history miner, advisory fetcher, vuln-file parser), synthesizes their returns into the system-context / assets / entry-points sections, generalizes the collected vulns into threat classes, gap-fills with STRIDE for surfaces the vuln history didn't cover, and writes targets/drlibs/THREAT_MODEL.md. On small targets (<50 source files) it runs the same briefs sequentially instead of spawning.
Interview (discover via owner conversation)
Use when an application owner is in the session.
/threat-model interview targets/alsa
/threat-model interview targets/alsa --design-doc targets/alsa/README.mdWithout --design-doc the interview opens cold by asking the owner to describe the system; with it, the skill reads the doc first and summarizes it back for confirmation.
The skill will walk the owner through the four questions ("what are we working on?", "what can go wrong?", "what are we going to do about it?", "did we do a good job?"), grounding answers in the code as it goes, and write targets/alsa/THREAT_MODEL.md.
Bootstrap then Interview (bootstrap a draft, then refine via interview)
Use when an owner is available but their time is limited: bootstrap produces the draft unattended, then the interview spends owner time only on what the code couldn't answer.
/threat-model bootstrap targets/drlibs/
/threat-model interview targets/drlibs/ --seed targets/drlibs/THREAT_MODEL.mdThe interview will focus on the bootstrap's open questions instead of starting cold.
Checkpointing and resume (bootstrap mode)
Bootstrap writes per-stage checkpoints to ./.threat-model-state/ in the current working directory (cwd-confined by checkpoint.py). If a run is interrupted, re-invoking /threat-model bootstrap <target-dir> from the same working directory resumes from the last completed stage — the research swarm is not re-spawned if Stage 1 already landed. Pass --fresh to start over. The state directory is scratch; add it to .gitignore.
Output
<target-dir>/THREAT_MODEL.md with seven sections: system context, assets, entry points & trust boundaries, threats (the table), deprioritized, open questions, provenance. See schema.md for the full contract; a worked example lives at targets/drlibs/THREAT_MODEL.md.
References
- Shostack, The Four Question Framework for Threat Modeling (2024) —
https://shostack.org/files/papers/The_Four_Question_Framework.pdf
- OWASP Threat Modeling Cheat Sheet —
https://cheatsheetseries.owasp.org/cheatsheets/Threat_Modeling_Cheat_Sheet.html
- This repo's
docs/security.mdanddocs/prompting.md.
THREAT_MODEL.md schema
Re-read note: If you need this file mid-session and the Read tool
reports "file unchanged", the prior result was evicted from context; reload
with cat .claude/skills/threat-model/schema.md via Bash.Both /threat-model interview and /threat-model bootstrap write this file to <target-dir>/THREAT_MODEL.md. The format is markdown so humans can read and edit it, but the section headings, table columns, and enum values below are a contract: keep the headings and column order exactly as shown so downstream tooling can parse them with regex.
---
Required sections, in order
# Threat Model: <system name>
## 1. System context
## 2. Assets
## 3. Entry points & trust boundaries
## 4. Threats
## 5. Deprioritized
## 6. Open questions
## 7. Provenance
## 8. Recommended mitigationsA consumer that only needs the threat table can regex for ^## 4\. Threats$ and read until the next ^## . Section 8 is optional and additive: older threat models may omit it, and consumers must tolerate its absence.
---
Section contents
1. System context
One to three paragraphs of prose: what the system is, what it does, who uses it, where it runs. No table. This is the answer to "what are we working on?".
2. Assets
Markdown table. One row per thing worth protecting.
| asset | description | sensitivity |
|---|
sensitivity ∈ {low, medium, high, critical}.
3. Entry points & trust boundaries
Markdown table. One row per place untrusted input enters the system or privilege level changes.
| entry_point | description | trust_boundary | reachable_assets |
|---|
trust_boundary is free text naming the crossing (e.g. "untrusted file → process memory", "unauth network → authenticated session"). reachable_assets is a comma-separated list of asset names from section 2.
4. Threats
Markdown table. This is the threat model proper. One row per actor-wants-outcome pair, at the abstraction level where it survives a patch.
| id | threat | actor | surface | asset | impact | likelihood | status | controls | evidence |
|---|
id:T1,T2, … Stable across edits; do not renumber when rows are
removed.
threat: One sentence, active voice, names the outcome. "Remote code
execution via untrusted media parsing", not "buffer overflow in dr_wav".
actor∈ {remote_unauth,remote_auth,adjacent_network,
local_user, local_admin, supply_chain, insider}.
surface: Which entry point(s) from section 3 this threat traverses.asset: Which asset(s) from section 2 this threat compromises.impact∈ {low,medium,high,critical,existential}.likelihood∈ {very_rare,rare,possible,likely,
almost_certain}.
status∈ {unmitigated,partially_mitigated,mitigated,
risk_accepted}.
controls: Current mitigations, ornone.evidence: CVE IDs, issue links, pentest finding IDs, or git commit
hashes that instantiate this threat. May be empty. Evidence raises likelihood; it is not the threat.
Sort the table by (impact, likelihood) descending so the top rows are the priorities.
5. Deprioritized
Markdown table. Threats considered and explicitly parked.
| threat | reason |
|---|
Common reasons: out of scope, actor not in threat model, asset not present, risk accepted by owner.
6. Open questions
Bullet list. Things the mode could not determine. For bootstrap these are questions for a human owner; for interview these are claims the owner made that were not verifiable in code.
7. Provenance
- mode: interview | bootstrap | bootstrap-then-interview
- date: YYYY-MM-DD
- target: <path or repo url @ commit>
- inputs: <design doc path | --vulns path | "none">
- owner: <name, for interview> | <unset, for bootstrap>8. Recommended mitigations
Optional, additive: older THREAT_MODEL.md files may omit this section, and consumers must tolerate its absence. Each row is one class-level control, not a per-finding patch: a mitigation that closes or materially shrinks an entire threat cluster regardless of which instance is found next.
| mitigation | threat_ids | closes_class | effort |
|---|---|---|---|mitigation: imperative, one line (e.g., "sandbox the decoder process",
"parameterized queries everywhere", "drop pickle for json", "enable CSP default-src 'self'", "size-cap all length fields before allocation").
threat_ids: comma-separated section 4 ids (e.g.,T1,T3) this mitigation covers.closes_class:yes|partial.effort:S|M|L.
---
Scoring guide
Impact
| value | means |
|---|---|
low | Nuisance; no data or availability loss. |
medium | Limited data exposure or degraded availability for some users. |
high | Significant data exposure, integrity loss, or full availability loss. |
critical | Full compromise of a primary asset (RCE, auth bypass, data exfil at scale). |
existential | Compromise threatens the organization's continued operation. |
Likelihood
| value | means |
|---|---|
very_rare | Requires nation-state resources or an unlikely chain of preconditions. |
rare | Requires significant skill and a non-default configuration. |
possible | A motivated attacker with public tooling could plausibly do this. |
likely | The attack surface is reachable and the technique is well known; prior evidence exists in this or similar systems. |
almost_certain | Actively exploited in the wild, or trivially automatable against the default configuration. |
Evidence (past CVEs in the same surface, pentest findings, public exploit code) moves likelihood up. Existing controls move it down. Score the residual likelihood after current controls.
---
Example (excerpt)
## 4. Threats
| id | threat | actor | surface | asset | impact | likelihood | status | controls | evidence |
|---|---|---|---|---|---|---|---|---|---|
| T1 | Memory corruption leading to RCE via untrusted audio file parsing | remote_unauth | dr_wav/dr_flac decoders | host process integrity | critical | likely | unmitigated | none | CVE-2026-29022, CVE-2025-14369 |
| T2 | Denial of service via resource exhaustion on decode | remote_unauth | dr_flac decoder | service availability | medium | likely | unmitigated | none | CVE-2025-14369 |
| T3 | Supply-chain compromise of vendored single-header dependency | supply_chain | build pipeline | host process integrity | critical | rare | partially_mitigated | pinned commit | |T1 stays in the model after both CVEs are patched: attackers will still send malformed audio files. The CVEs are evidence the surface is fertile, not the threat itself.
Related skills
FAQ
What are the modes?
Interview walks an owner through a four-question framework, bootstrap derives the model from code plus past vulnerabilities, and bootstrap-then-interview chains the two.
Does it run code?
No. It performs static analysis only, reading source, git history, and supplied vulnerability reports, and never builds, executes, or fuzzes the target.