Point your AI assistant at Draugr
On this page
Open a repository in an AI coding assistant and ask it to check the code for security problems.
You will get an answer. It'll read some source, probably notice the obvious things, maybe run a scanner if one happens to be on your PATH, and give you a considered paragraph. It'll sound right. The uncomfortable question is what it was looking at when it decided.
What it looks at
We measured this on draugr-demo, a small Flask app built to have problems, made of 35 lines of Python, a requirements.txt, a Dockerfile, one
Kubernetes manifest.
The unaided path
Four scanners an assistant would plausibly reach for, run over the source tree: 56 findings, across roughly 197 KB of output in four different schemas. Not a bad haul, and the three command-injection paths in app.py are all in there.
The same repository, through its descriptor
439 findings. The same code, the same scanners available, and one file saying what the application is made of.
The extra 383 aren't noise we invented but sit in python:3.8-slim, the base image the app
ships, ten critical and sixty-eight high. And here is the number that matters:
78 of the 99 highest-priority findings were in that image. The source-tree scan never looked at it.
Not because the assistant is bad at its job. Because nothing in the source tree said the app ships a container image. The Saga says so, in one line:
components:
- name: api
exposure: public
images:
- image: python:3.8-slim
"It would have found the Dockerfile"
Maybe. A thorough assistant could read FROM python:3.8-slim and think to scan the base image.
Ours didn't, and we didn't rig that, but a better prompt or a more careful model might.
So the claim isn't that it's impossible, but something narrower and harder to argue with:
It's left to chance. Whether the image gets scanned depends on the model, the prompt, and the day. The descriptor doesn't depend on any of them.
It doesn't generalize. A Dockerfile is one place images come from. Plenty of components ship images built by CI, tagged elsewhere, or pulled from a registry with nothing in the repo naming them. And images are the easy case. The endpoints to probe, the infrastructure to check, whether this component is internet-facing at all. None of that is inferable from source, and all of it is one line in a descriptor.
That's the whole argument, and it isn't about model quality:
Scope is a fact about your organization, not about your code.
It tells you what it checked
A scan comes back with the controls that ran, and with any surface your descriptor declares that none of them looked at:
{
"verdict": "fail",
"controls": ["sast", "sca", "secrets"],
"uncovered": ["api declares the images surface, and the images control is not enabled"]
}
Coverage is something you read, not something you infer from an absence of findings. That is the difference worth paying for, and it is sharper than "a scanner finds more things".
Consider what a confident answer costs when nothing states its scope. An assistant that greps for
credentials across *.py, finds none, and writes "no hardcoded secrets" has told you a true
thing about Python files and filed it as a fact about the repository. The grep was right. The
sentence was not. Nothing in the output marks the difference, and the sentence is the part you
act on.
A declared scope cannot make that trade. The controls are chosen before the run and reported after it, so "we did not check that" and "we checked that and it was clean" never arrive looking the same. Your assistant gets the same distinction, which is why it keeps going instead of stopping at a verdict.
The other half, with nothing to rank by
Even where the unaided scan found the same finding, it couldn't tell you what to do about it.
Severity
Ranks a vulnerability in the abstract. Any scanner computes it, and it is the same number wherever the flaw turns up.
Priority
Needs to know how reachable the thing is and how much you would care if it broke, a different question and one no scanner can compute, because the answer is not in the code. exposure: public on that component is why its findings sort to the top.
So the assistant hands you a list. Draugr hands you an order.
Two answers, one of which is wrong
The deeper problem is that you now have two sources of truth. The assistant told you one thing in your editor; CI will tell you another on push. They used different scopes, different scanners and no shared notion of priority, so of course they disagree, and the one that blocks your merge is the one you didn't see.
That is the case for wiring them together, and it is not that AI is exciting but that one descriptor gives one answer.
Point it at Draugr
claude mcp add draugr -- draugr mcp
In Claude Code, the Draugr plugin does the same from inside a session:
/plugin marketplace add draugr-dev/draugr, then /plugin install draugr@draugr.
Draugr speaks the Model Context Protocol, so your assistant can ask it directly:
| Tool | What it answers |
|---|---|
list_controls | Which controls exist, what each checks, which scanner backs it, and the options each scanner takes |
get_saga_schema | The descriptor schema your build enforces, from the binary you installed rather than the web. It rejects unknown keys, so an invented field name fails loudly instead of being ignored |
validate_saga | Whether a descriptor is valid, and why not |
propose_saga | The descriptor draugr init would write for a project that has none, returned rather than written |
check_tools | Which scanners are installed, and the command to fix what isn't. It will not run that command: installing binaries writes to your machine, and your client already has a stronger permission model than this server could offer |
feeds_status | Whether the KEV, EPSS and Go vulnerability feeds are cached and current, and the command that fetches a stale one |
summarize_report | An existing scan, ranked by priority, each finding carrying its remediation and the version that fixes it |
fix_list | The same scan as things to do, one row per fix, saying how many findings each clears and which release clears them. The difference between here are forty problems and here are five things to do, the first clears thirty-nine, and it is the grouping the terminal prints, so the two cannot describe one report differently |
explain_rule | What a check means and how to fix it, out of the report already on disk. The remediation the scanner published, rather than whatever a search engine offers for the identifier, which for a benchmark check is a registration form in front of a PDF |
diff_reports | What a change introduced and what it resolved, and whether a pull-request gate would fail |
list_surveyors / survey | What a live cluster or organization is made of, as a descriptor to review |
scan | A fresh scan and verdict, the controls it covered, and where your publishers filed it |
Every *.saga.yaml nearby is also exposed as a resource, so the assistant reads your committed
scope without being told where it is.
A scan that does more than read asks first
A scan clones repositories, executes external scanners and may reach beyond this machine. A scan of read-only controls over a temporary copy, writing its report to a local directory, runs when the assistant calls it. A scan asks first when a scanner probes a live host, sends data to a third party, changes something or needs elevated access, or when a publisher delivers the report off this machine:
draugr mcp # asks before a scan that does more than read
draugr mcp --scan=ask # asks before every scan
draugr mcp --scan=always # never asks, for sandboxes and CI
draugr mcp --scan=off # the scan tool isn't offered
The prompt describes that scan. It names the controls, how many components they cover, each scanner that does more than read and what it does, whether anything sends traffic to a host you declared, and where the results will be filed. Approving five read-only controls over a checkout and approving a probe of a production endpoint are different decisions, and you are told which one you are making.
It needs a client that implements MCP elicitation, and if yours can't prompt, the scan is refused with a message saying so rather than quietly running anyway.
Everything else is read-only and free to call, and summarize_report answers what should I fix
first? from a scan your pipeline already ran, at no cost at all.
Your assistant is going to answer this question either way. The only choice is whether it answers from something your team wrote down, or something it made up on the spot.