Threat modeling
at the speed of code.
Security decisions live as annotations beside the code they describe. Your coding agent writes them, the CLI checks them, and CI fails when an exposure has nothing answering it.
Truncated. The real block also reports acceptances, entitlements, transfers, validations, audits, ownership, data handling, assumptions, features, comments, and shields. GuardLink runs on its own codebase, and it reports 15 open exposures rather than a clean sheet.
Security knowledge lives outside the code.
That is the whole bug.
Every one of these is a real artifact that a real team paid for, and every one of them decays the moment someone opens an editor.
Written once at kickoff. Nothing in the pipeline reads it, so nothing notices when it stops being true.
Two hundred findings with no sense of which asset matters, what already defends it, or what someone decided to live with.
A PDF in a shared drive, accurate on the day it was written and about a codebase that has since moved.
Move the claims into the repository and the problem changes shape. A claim beside the code is one a reviewer sees in the diff, a parser can check, and CI can refuse.
What that buys, and what it costs →Three commands, and the model stops rotting.
A threat model goes stale because it lives somewhere the code does not. GuardLink puts the claims in the repository and makes them checkable, so a stale claim is a failing build rather than a quiet lie.
guardlink annotateAn agent writes the claims
Your coding agent reads the codebase and records what each part holds, what threatens it, and what already defends it. Claims go in .gal sidecars by default, or inline in comments.
guardlink validate .The CLI checks them
Every #id either resolves to something declared or it does not. Dangling references, duplicate ids, and syntax errors fail here rather than being discovered by a reader six months later.
guardlink ci --strictCI holds the line
An exposure with nothing answering it fails the build. Closing one means naming a control that handles it, or a person accepting the risk under their own name.

An agent working through a real codebase. It reads the source, then writes the asset, threat, and mitigation claims that the next two commands check.
Twenty verbs. Two of them close a finding.
GAL is a small grammar for security intent that works in any language, because it lives in comments. Its whole job is to make a claim about code checkable.
@asset@threat@control@actorNamed once, each with an #id the rest of the model references.
@exposesThe only verb that creates work.
@mitigates@acceptsA control handles it, or a person decides to carry it. Both are recorded; neither is silence.
@flows@boundary@handles@confirmed@entitles@audit@assumes@owns@validates@transfers@feature@comment@shieldFacts a reader, a report, or a downstream tool needs. None of them opens or closes anything.
// @asset App.Reports (#reports)
// @threat Scraping (#scrape) [medium]
// @exposes #reports to #scrape [medium]
// -- "No rate limit on export"
export function exportAll(userId) {
return db.query(
'SELECT * FROM reports WHERE user = $1',
[userId]
);
}@exposes is one of the 15 findings the status block above counts. It stays open until a @mitigates names a control or a person signs an @accepts.What comes out the other end.
One command turns the model into a self-contained HTML dashboard. Every screen below is real output, and the dashboard itself is open on this site, not a picture of one.

Executive summary. A risk grade, the counts behind it, mitigation coverage, and the severity split. This is the page a security lead opens.
Open this viewOpen the real dashboard
The actual file guardlink dashboard wrote, served unchanged. One HTML file, no server, no build step. Click through the drawers and diagrams yourself.
Watch the full run
An empty repository to a reviewed threat model, end to end: init, annotate, validate, and the dashboard the run produces.
Written by the thing that writes the code.
guardlink init detects your coding agent and wires two things: an MCP server it can query, and a rule in its instruction file telling it to annotate security-relevant code as it writes it.
- MCP + ruleClaude Code
CLAUDE.md + .mcp.json - MCP + ruleCursor
.cursorrules + .cursor/mcp.json - MCP + ruleWindsurf
.windsurfrules + .windsurf/mcp.json - MCP + ruleCline
.clinerules + .cline/mcp.json - rule onlyCodex
AGENTS.md - rule onlyGitHub Copilot
.github/copilot-instructions.md
24 tools the agent can call
Read the model, validate it, suggest annotations for a snippet, query threats by keyword, generate a report or a dashboard, export SARIF, diff against a git ref. The agent asks “what threatens #api?” before it writes code that touches the API.
An agent may find an exposure and propose an entitlement. It may not accept a risk, and it may not decide a caller was always meant to hold a power. Those two statements close a finding or excuse one, so the MCP server refuses to write @accepts or @entitles at all. A person makes those calls at a terminal, and their name goes on the record.
A threat model that can fail a build.
A document nobody has to satisfy drifts. GuardLink runs as a check, so a new route with no annotations, or a control removed from under an exposure, shows up in the diff and stops the merge.
- name: Install GuardLink
run: npm install -g guardlink
- name: Validate annotations
run: guardlink validate .
- name: Threat model diff
run: guardlink diff --from origin/main --to HEAD
- name: Export SARIF
run: guardlink sarif . -o guardlink.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: guardlink.sarif }The full workflow, with PR comments and SARIF upload, ships as examples/github-action.yml. Multi-repo setups get two more in examples/ci/.
Unmitigated exposures: 15 (high 3, medium 6, low 6)Anchor drift: 0 (no anchored @source blocks to check)⚠ 15 unmitigated exposure(s):#mcp → #cmd-injection [high] (src/mcp/index.ts:6)#tui → #cmd-injection [high] (src/tui/commands.ts:11)#mcp → #prompt-injection [medium] (src/mcp/server.ts:36)#mcp → #data-exposure [medium] (src/mcp/server.ts:40)#suggest → #dos [low] (src/mcp/suggest.ts:16)…
Advisory by default and exits 0; --strict turns it into a gate. Ten more lines are truncated here.
Model your first threat
in about ten minutes.
Node 18 or newer is the only requirement. init detects your agent and writes the config; everything after that is three commands.