Threat modeling
at the speed of code.

Your threat model lives in the code, so it can’t go stale.

Read the docsGitHub →
~/src/guardlink · src/mcp/server.ts
28 * Resources:
29 * guardlink://model — Full ThreatModel JSON
30 * guardlink://definitions — Assets, threats, controls
31 * guardlink://unmitigated — Unmitigated exposures list
32 *
33 * Transport: stdio (for Claude Code .mcp.json, Cursor, etc.)
34 *
35 * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not"
36 * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input"
37 * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output"
38 * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path"
39 * @flows #mcp -> MCPClient via resource -- "Threat model data output"
40 * @flows #mcp -> #codegraph via guardlink_worklist -- "Ranked entry points for an annotating agent, when a code graph is installed"
41 * @flows #mcp -> #codegraph via guardlink_reach -- "Client-supplied symbol address, passed to the graph as one argument"
42 * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing"
43 * @handles internal on #mcp -- "Processes project annotations and threat model data"
44 * @feature "MCP Integration" -- "Model Context Protocol server for AI agent tooling"
45 * @entitles #mcp-agent to read-threat-model on #mcp against #data-exposure -- "By design: guardlink mcp exists to hand a connected coding agent the threat model — that disclosure is the product, not a leak…"
46 * @comment -- "Entitlement accepted by zippon on 2026-08-10 via guardlink entitle"
47 */
48
49import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
50import { resolve } from 'node:path';
51import { z } from 'zod';
TerminalProblemsOutput zsh
~/src/guardlink main
mainguardlink 2.5.01049 annotations14 unmitigatedLn 35, Col 1Spaces: 2UTF-8LFTS TypeScript
See it in your theme

Live from GitHub and npm, refreshed every hour. Each figure links to its source.

The problem

Security knowledge lives outside the code. That is the bug.

Teams pay for all three, and all three start going stale the next time someone opens an editor.

A page in ConfluenceExample
▤Security › Paymentsedited 14 months ago
Payments — Threat Model
3. Session handling
Sessions are server-side. The token never leaves the
database, so a stolen cookie cannot be replayed off-host.
4. Rate limiting
Covered by the edge proxy. See the platform runbook.
0commits since
0pipelines read this page
A scanner's outputExample
$scan --all --format textexit 0
HIGH CWE-89 src/orders/query.ts:114
HIGH CWE-79 src/web/render.tsx:38
MEDIUM CWE-352 src/web/form.tsx:21
MEDIUM CWE-200 src/api/user.ts:73
LOW CWE-693 src/web/headers.ts:9
… 195 more
0findings
0say what already defends them
Last year's pen testExample
▣pentest-2024-final-v3.pdf1 / 48
WEB APPLICATION
PENETRATION TEST
Engagement 3–14 March 2024
Prepared for Payments Platform
Classification Confidential
0days old
0commits since
Put the same claim in the repository, and it arrives in review.
src/tui/commands.ts@@ -9,5 +9,7 @@+2
9 * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files"
10 * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root"
+11 * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes"
+12 * @audit #tui -- "Child process spawning delegated to agents/launcher.ts"
13 * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage"

Two lines in a pull request. The reviewer reads them next to the code they describe, the parser checks that #cmd-injection and #tui are declared, and the exposure stays open until a control answers it.

What that buys, and what it costs
How it works

Three commands keep the model current.

GuardLink puts the claims in the repository and checks them, so a stale claim fails the build instead of sitting there unnoticed.

  1. 01 / 03

    An agent writes the claims

    Your coding agent reads the code and records what each part holds, what threatens it, and what already defends it. Claims go in .gal sidecar files by default, or inline in comments.

    $ guardlink annotate
  2. 02 / 03

    The CLI checks them

    Every #id must resolve to something declared. Dangling references, duplicate ids and syntax errors fail here, not six months later when someone finally reads the model.

    $ guardlink validate .
  3. 03 / 03

    CI enforces them

    An exposure with nothing answering it fails the build. To close one, name the control that handles it, or have a person accept the risk under their own name.

    $ guardlink ci --strict
$ guardlink annotate "cover the webhook sender" .
app/notify.py
1import requests
2from .config import WEBHOOK_TIMEOUT
3
4
5def send_webhook(url, payload):
6 """POST an expense event to a team-configured webhook URL."""
7 return requests.post(url, json=payload, timeout=WEBHOOK_TIMEOUT)
8
9
10def fetch_exchange_rate(currency):
11 resp = requests.get(
12 f"https://api.exchangerate.host/latest?base={currency}",
13 timeout=WEBHOOK_TIMEOUT,
14 )
15 return resp.json().get("rates", {})
.guardlink/annotations/app/notify.py.gal
+ @source file:app/notify.py line:5 symbol:send_webhook
+ @exposes #notify to #ssrf [high] cwe:CWE-918 -- "url is body['webhook'] from POST /expenses, POSTed unvalidated…"
+ @confirmed #ssrf on #notify [high] cwe:CWE-918 -- "POST /expenses with webhook=http://169.254.169.254/latest/meta-data/…"
+ @ticket #ssrf on #notify DEMO-42 sla "High" by security-team until 2026-11-30 -- "Demo ticket: restrict outbound webhook destinations"
+ @audit #notify -- "Needs an allowlist of registered webhook hosts…"
+ @mitigates #notify against #malformed-input using #http-timeout -- "WEBHOOK_TIMEOUT is passed explicitly…"
+ @flows #api -> #notify via send_webhook -- "Caller-supplied URL plus the user id and amount…"
+ @flows #notify -> ExternalHost via requests.post -- "Expense event leaves the trust boundary…"
+ @handles financial on #notify -- "Payload carries the expense amount and the user id"

guardlink 2.5.0 on the sample project the dashboard below is drawn from. The sidecar records its claims for app/notify.py, including a fictional tracking ticket; validate and ci print what they say about the whole model.

Release 2.5.0

What 2.5.0 adds.

Track remediation beside the exposure, focus annotation work, and keep per-PR checks precise.

Track the work to fix an exposure

guardlink dashboard .guardlink sarif . -o guardlink.sarif
@ticket links a threat on an asset to a tracker ID or URL, with an optional SLA label, assignee and due date. The model, dashboard, diff and SARIF carry it. The annotation is the record: GuardLink never calls the tracker, and a ticket leaves the exposure open.

Give annotation work a clear scope

guardlink unannotated . --jsonguardlink unannotated . --excluded
Get the files still needing annotations and the reason each other file is left out. Coverage measures this scope; annotations in excluded files still stay in the model.

Keep reminders separate from gate failures

guardlink lint . --json
A ticket on a mitigated or accepted pair is an info-level reminder. The JSON includes summary.infos; info never fails the gate. Keep the ticket as a record or remove its annotation when it closes.

Review changes against a consistent base

guardlink diff origin/main --fail-on-new
An unchanged exposure stays stable when lines shift within its anchored symbol. Base and head use the same anchors, and summaries count coding-agent configuration and reach changes too.

Write reach identities the parser can read

guardlink annotate "review agent reach" . --stdout
The prompt and reference now teach the optional as identity as an asset reference: #id, Dotted.Path or a quoted name. Malformed reach annotations explain the expected shape and the token that needs attention.
The annotation language

Twenty-five verbs. Two of them close a finding.

GAL is a small grammar for security intent. It lives in comments, so it works in any language, and its one job is to make a claim about code checkable.

Browse the verbs by category

Or print the full reference in your terminal.

Relationships

Connect threats to assets. Record the controls and decisions that resolve them.

@exposesopens finding
Marks an asset exposed to a threat at this line. Every one creates a finding.
@mitigatescloses finding
Names the control that answers a threat on an asset, and closes the finding.
@confirmed
Records a threat as verified exploitable, with the evidence that proved it.
@ticket
Records an open tracker ticket for a threat on an asset, with an optional SLA label, assignee and due date. Remove the line when it closes; no tracker is called.
@acceptscloses finding
Records a decision to carry the risk instead, which also closes the finding.
@entitles
States an actor is entitled to a capability by design. An agent proposes; a person accepts.
@transfers
Moves responsibility for a threat to another asset or team.
Only @mitigates and @accepts close a finding.
What it produces

What comes out the other end.

One command turns the model into a self-contained HTML dashboard. The screens below are real guardlink 2.5.0 output for a sample Flask service with an AI assistant built in, and that dashboard is hosted on this site.

GuardLink dashboard overview: open exposures by severity, every exposure accounted for, what changed since a commit, and what to do next

Overview. How many exposures are open and how bad, what changed since the commit you name, and what to do next, each item carrying the command that does it.

Open this view
Coding agents

Your coding agent writes the annotations.

guardlink init detects your coding agent and sets up two things: an MCP server it can query, and a rule in its instruction file that tells it to annotate security-relevant code as it writes it.

Agents that guardlink init sets up
  • Claude Code
    CLAUDE.md + .mcp.json
    MCP + rule
  • Cursor
    .cursor/rules/guardlink.mdc
    rule
  • Codex
    AGENTS.md
    rule
  • GitHub Copilot
    .github/copilot-instructions.md
    rule
  • Gemini CLI
    .gemini/GEMINI.md
    rule
  • Windsurf
    .windsurfrules
    rule
  • Cline
    .clinerules
    rule

Any editor that speaks MCP can run the same server: guardlink mcp.

28 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, or diff against a git ref. The agent asks “what threatens #api?” before it writes code that touches the API.

What the agent itself may do is in the model too

Since 2.4.0, GuardLink reads the configuration that decides what a coding agent may run, edit and call without asking: .claude/settings.json, .mcp.json, .cursor/, .codex/config.toml, skills, hooks and more. Each capability lands in the threat model with the file and line that grants it. Nothing it names is started or fetched.

guardlink validate .
Flags hidden Unicode in instruction files, skills and commands.
guardlink ai-bom .
Exports agents, MCP servers and skill bundles as a CycloneDX 1.6 AI-BOM.
guardlink reach .
Checks the reach the model declares against an optional code graph.
guardlink attest <doc>
Compares a tool inventory or call log with the reach the model declares.
Two things it cannot do alone

An agent can find an exposure and propose an entitlement. It cannot accept a risk, or decide that a caller was always meant to hold a power: those two statements close a finding or excuse one. The MCP server’s annotate tools reject @accepts and @entitles. An entitlement goes to a person as a proposal, and an acceptance is written only with the name of the person who made the call.

In CI

A threat model that can fail a build.

GuardLink runs as a check. A new route with no annotations, or a control removed from under an exposure, shows up in the diff and can block the merge.

.github/workflows/guardlink.yml
- name: Install GuardLink
  run: npm install -g guardlink

- name: Validate annotations
  run: guardlink validate .

- name: Threat model diff
  run: guardlink diff origin/main --fail-on-new

- 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/.

guardlink ci .
Parse diagnostics: 0 error(s), 3 warning(s) (prose-like 3)
Annotations the parser could not read: 3
Unmitigated exposures: 16 (high 3, medium 7, low 6)
Confirmed exploits: 0
Acceptances: 0 (source: @accepts annotations in this repository)
Annotation coverage: 92% — 171 of 185 source file(s) annotated, 1061 annotation(s)
…
⚠ 16 unmitigated exposure(s):
#agent-config → #prompt-injection [low] (src/agentconfig/read.ts:26)
#agent-launcher → #prompt-injection [medium] (src/agents/launcher.ts:13)
#agent-launcher → #prompt-injection [high] (src/agents/prompts.ts:6)
#agent-launcher → #config-tamper [medium] (src/agents/prompts.ts:10)
#llm-client → #data-exposure [low] (src/analyze/index.ts:12)
#llm-client → #prompt-injection [medium] (src/analyze/llm.ts:17)
…

Run on GuardLink’s own repository at v2.5.0; ten more exposures and the parse-diagnostic, anchor-drift and stale-claim details are cut here. It is advisory by default and exits 0; --strict makes it a gate.

New route, no annotations
The diff shows the gap before a reviewer has to spot it.
Control removed
The exposure reopens, and --fail-on-new blocks the PR.
Exposure accepted
Recorded under a person's name, not deleted.
Every command and flag
Get started

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.

Source
Prove it

A threat model says what's exposed. Testing says what's reachable.

GuardLink stops at the code: it never builds or runs your app. When you need to know which exposures an attacker can actually reach, that is what Bravos, Bugb's pentesting product, is for.

GuardLink, open source

What's exposed

Reads the annotations in your source and lists every exposure: the asset, the threat, and whether anything mitigates it.

Bravos, from Bugb

What's reachable

Attacks a running copy of your app with real logins, and ends every threat with a verdict and the reason for it:

  • confirmed
  • refuted
  • not tested

Not sure what GuardLink would find in your code? Send a public GitHub repository and the Bugb team runs GuardLink on it, then emails you the threat model it finds. Nothing is pushed to your repository.

Community

GuardLink is open source. A star helps it travel.

It costs nothing and takes one click, and for a project this size it does two useful things.

Other teams find it
GitHub search, topic pages and trending lists all weigh stars. Each one makes GuardLink easier to find for the next team looking for a threat model they can keep in their code.
The maintainers know it is used
Stars tell us people rely on it. Issues tell us what to build next, so if something is missing, an issue is worth even more than a star.

Other ways to help

20stars on GitHub today

Star on GitHub

Opens the repository. The Star button is at the top right.