Threat modelling code and infrastructure as code with Claude
Christo Goosen9 min read
How to use the /threat-model skill from Anthropic’s defending-code reference harness to threat model application code and Terraform, Kubernetes and CI config.
Anthropic has published the defending code reference harness, an open-source (Apache 2.0) reference implementation for finding and fixing vulnerabilities with Claude. It ships an autonomous pipeline (recon, find, verify, report, patch) and a set of Claude Code skills you can run interactively. Most attention goes to the scanning. This post is about the part we think most teams should start with: the /threat-model skill, and in particular how it handles infrastructure as code alongside application code.
What is in the repository
Open the repository in Claude Code and the skills in .claude/skills/ become slash commands:
/quickstart: a guided first run and a place to ask how to adapt the harness to your stack./threat-model: builds a structuredTHREAT_MODEL.mdfor a target codebase./vuln-scan: a static review that uses the threat model to decide where to look./triage: deduplicates findings across runs and recalibrates severity against your threat model./patch: generates and checks fixes, starting with what triage ranked highest./customize: ports the autonomous pipeline to another language, detector or bug class./dnr-huntand/dnr-respond: a detection and response track for when an attacker is already in your logs.
The autonomous harness/ pipeline is configured for C and C++ memory bugs with Docker and AddressSanitizer, and the README is clear that it is “a reference, not a product”. The repository is also marked as unmaintained and does not accept contributions, so treat it as something to copy and adapt rather than a dependency. The threat-model skill is the easiest piece to lift out: it is a set of markdown instructions, with no build step and no language assumptions.
Why threat model before you scan
The repository’s threat model guide makes a point anyone who has triaged scanner output will recognise: one of the most common reasons a model reports a false positive is that it lacks context about the target. A bug can look exploitable in the source and be irrelevant in practice because the deployment mitigates it, or be far less severe than the code alone suggests. A threat model supplies that context. It tells the scanner where to look and tells triage which findings matter.
The skill is strict about the difference between a threat and a vulnerability. Its litmus test: if patching one line makes an entry disappear, it was a vulnerability, not a threat. “Remote code execution via untrusted file parsing” stays true for as long as the code parses untrusted files. A specific missing bounds check does not. The skill outputs threats, and known bugs and CVEs appear only as evidence that raises a threat’s likelihood. That is what makes the output durable: you can scan, fix and scan again against the same threat model.
Three ways to build the model
bootstrap: derives the threat model from the code alone, plus git history, public advisories and an optional file of past vulnerabilities. Use it for inherited systems, third-party code and open-source dependencies.interview: walks an application owner through Adam Shostack’s 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 the answers in the code. Use it for new systems and anywhere the risk lives in business logic the code does not show.bootstrap-then-interview: drafts the model from the code first, then spends the owner’s time only on what the code could not answer. This is the mode we would default to.
All three write the same THREAT_MODEL.md schema: system context, assets, entry points and trust boundaries, a threats table, deprioritised threats, open questions, provenance and recommended mitigations.
How bootstrap mode works
Bootstrap runs in five stages. First, a parallel research swarm of subagents reads the target:
- A docs reader for the README,
SECURITY.md, changelog and build manifest. - A surface mapper that greps for entry points: HTTP routes, file parsers, deserialisers, raw queries, subprocess calls, lockfiles and
curl | shin build scripts. - An infra reader for deploy-time configuration (more on this below).
- An asset finder for what the code protects: secrets, user records, databases and service availability.
- A history miner that derives stack-specific keywords and searches the git log for past security fixes.
- An advisory fetcher that pulls GitHub Security Advisories, and a parser for any past-vulnerability or pentest file you supply.
The second stage synthesises those returns into assets and entry points. The third clusters past vulnerabilities by entry point, bug class and asset, and turns each cluster into one threat, with a variant scan for sibling code paths that share the same pattern. The fourth is the gap-fill: past bugs are biased towards what has already been found, so every entry point without a threat gets a STRIDE walk. The fifth writes the file. On targets with fewer than 50 source files it runs the briefs one after another instead of spawning subagents, and it checkpoints after each stage so an interrupted run can resume.
Be aware that this can cost a lot of tokens. Each subagent in the swarm reads large parts of the codebase and git history independently, the variant scan and gap-fill read more code again, and an interview adds a long multi-turn conversation on top. On a large monorepo, on a capable model, a single bootstrap run can use a meaningful share of a monthly budget. The repository’s own view is that tokens spent here save human time triaging false positives later, and we agree, but plan for it: point the skill at one service or one infrastructure directory rather than the whole monorepo, let interrupted runs resume from their checkpoints instead of starting over with --fresh, re-run only when the architecture or IAM changes, and set a spend limit on the API key or workspace you use.
Threat modelling infrastructure as code
This is the part we find most useful. Many security incidents start in configuration, not code: an over-broad IAM role, a service account created by hand in the console, a deploy pipeline that can push anywhere. The bootstrap swarm’s infra reader reads Terraform (*.tf, *.tfvars), Kubernetes manifests under k8s/, deploy/ or manifests/, Dockerfiles, CI workflows and IAM or access-control files. For each one it answers three questions:
- Which identity does this run as, and what can that identity reach?
- Is any access grant managed outside this repository, such as console-created IAM, hand-made service accounts or missing policy tags?
- Do any credentials or principals survive a migration or a teardown?
The skill treats infrastructure, IAM, build-time and supply-chain surfaces as entry points even though no runtime input crosses them, and every entry point must end up with at least one threat. Because STRIDE maps poorly onto IAM, the gap-fill stage walks infrastructure entry points through a different checklist:
- Over-grant: does the identity reach more than the application needs, such as a whole dataset instead of one table, or project-level instead of resource-level access?
- Lateral identity: can a co-located workload in the same namespace, on the same node or with the same service account assume this identity?
- Drift: is any grant managed outside this tree, so it will not 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 or personal-information columns the application does not need?
- Scope enforcement: where there is an automated approval, merge or write path, what bounds it to its intended scope?
Take a small, made-up example: an application on EKS that uploads customer documents, and a GitHub Actions deploy role.
resource "aws_iam_role_policy" "app" {
role = aws_iam_role.app_irsa.id
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = ["s3:*"]
Resource = ["*"]
}]
})
}
resource "aws_iam_role" "deploy" {
name = "github-deploy"
assume_role_policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRoleWithWebIdentity"
Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:example-org/*"
}
}
}]
})
}A model that has read this config, plus a Kubernetes manifest that puts the application’s service account in a shared namespace, should produce threats at this level rather than “line 7 uses a wildcard”. An illustrative excerpt in the skill’s schema:
| id | threat | actor | surface | asset | impact | likelihood | status | controls | evidence |
|---|---|---|---|---|---|---|---|---|---|
| T1 | Bulk exfiltration of customer documents via a compromised app pod's over-granted S3 role | remote_unauth | upload API → app pod → IRSA role | customer documents | critical | possible | unmitigated | none | |
| T2 | Production deploy from any repository in the GitHub org via a broad OIDC trust policy | supply_chain | CI → github-deploy role | production infrastructure | critical | possible | unmitigated | none | |
| T3 | Another workload in the shared namespace assumes the app's identity | insider | k8s namespace → service account | customer documents | high | rare | partially_mitigated | network policy | |The recommended-mitigations section then asks for one class-level control per threat cluster rather than a patch per finding: scope the role to the one bucket and prefix it needs, pin the OIDC trust to a single repository and protected branch or environment, and give the workload its own namespace and service account. Those controls survive the next misconfiguration, not just this one.
Running it on your own repositories
The skill is read-only and static. It reads source, git history and the vulnerability files you give it, writes one THREAT_MODEL.md, and does not build, run or probe the target. Install it at user scope so you can point it at any checkout:
git clone https://github.com/anthropics/defending-code-reference-harness
cp -r defending-code-reference-harness/.claude/skills/threat-model ~/.claude/skills/
# The skill's checkpointing calls this helper by relative path, so run Claude
# Code from the harness checkout, or copy _lib/ into your project's .claude/skills/
cd defending-code-reference-harness
claude
> /threat-model bootstrap-then-interview ~/code/payments-service
> /threat-model bootstrap ~/code/platform-infra --vulns ~/reports/2026-pentest.mdSome practical points:
- Threat model code and infrastructure together where you can. The infra reader only sees the tree it is pointed at, so if your Terraform lives in a separate repository from the service it deploys, either check them out side by side under one target directory or run the skill on both and reconcile the entry points.
- Feed it your past pentest reports with
--vulns. Findings become evidence that raises the likelihood of the threat they belong to, which is exactly how a pentest report should be read. These reports are sensitive, so only send them to a Claude deployment your organisation has approved; the harness works with the Anthropic API, Amazon Bedrock, Google Vertex AI and Azure. - For open-source dependencies, read the project’s
SECURITY.mdfirst and pass its stated scope as a constraint. Many projects rule out whole bug classes as by design, and aligning up front cuts findings the maintainers would reject. - Commit
THREAT_MODEL.mdnext to the code, add.threat-model-state/to.gitignore, and re-run it when the architecture or the IAM changes, not on every commit.
Feeding the threat model into scanning and triage
The threat model is the input to everything else in the harness. /vuln-scan looks for THREAT_MODEL.md in the target directory and uses its entry points and trust boundaries to scope the review subagents. The autonomous pipeline’s recon stage turns it into focus areas. /triage uses it to decide which findings to escalate. Without one, the scanner falls back to a quick recon of its own and has far less context to judge what matters.
Keep the safety model in mind as you go further. The threat-model, scan and triage skills only read and write files and are safe to run with Claude Code’s normal per-tool approvals. The autonomous pipelines execute target code, so they refuse to run outside a gVisor sandbox unless explicitly overridden; set that up with the repository’s sandbox script before you use them.
What we would tell you first
- Start with the threat model, not the scanner. It is cheap, read-only and makes every later step more accurate.
- Prefer bootstrap-then-interview. The code answers what the system does; only the owner can say what matters to the business.
- Point it at your infrastructure as code, not just your application. Over-grants, drift and broad CI trust are threats in their own right.
- Ask for class-level mitigations. A scoped role or a pinned OIDC trust closes a whole class of problems; a one-line fix closes one.
- Treat the output as a draft for human review. The skill says so itself, and the open questions section is where the real conversation starts.
Threat modelling is the first step in our AI security engagements, and a threat model makes a penetration test far better targeted. If you want a threat model of your own system, or help wiring this into your SDLC, get in touch.
Keep reading
9 min read
Securing vibe-coded apps: the last mile
AI app builders get you a working demo fast. Here is the security checklist for the last mile: data access, secrets, authorisation, LLM features and the agent itself.
Read post3 min read
What is a vCISO, and does your South African company need one?
A plain-English guide to vCISOs for South African companies: what a vCISO does, how it differs from a full-time CISO, and how it fits with POPIA and ISO 27001.
Read postNeed a hand with your own project?
Fractional CTO leadership, CISO services and AI security, from Cape Town to anywhere remote.