← All articles

Don't make the model rank it

September 19, 2026 · Wilson Santos · updated October 5, 2026
On this page

Here are two problems found in the same repository, on the same run, by two different security tools. This is what each tool sent back, with the key itself and the committer's name removed.

{
  "ruleId": "private-key",
  "message": { "text": "private-key has detected secret for file app/config.example.pem at commit a103420…" },
  "locations": [{ "physicalLocation": {
    "artifactLocation": { "uri": "app/config.example.pem" },
    "region": { "startLine": 1, "endLine": 8 }
  }}]
}
{
  "ruleId": "yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag",
  "message": { "text": "GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed…" },
  "locations": [{ "physicalLocation": {
    "artifactLocation": { "uri": ".github/workflows/draugr.yml", "uriBaseId": "%SRCROOT%" },
    "region": { "startLine": 35, "endLine": 35 }
  }}]
}

The first is a private key committed to the repository. Somebody who copies it can sign things as you. The second is a build step that says "use version 4" instead of naming an exact version. That one is untidy and worth fixing whenever somebody gets to it.

You fix the first today and the second eventually. Now find the part of either message that tells you which is which.

0 of 23
problems that two security scanners found in one repository came with any hint of how urgent they are. A leaked private key was one of them
2.1 MB
of text those two scanners produced to report 23 problems. Most of it is a list of every check they know how to run, including the ones that found nothing
18 of 1,296
of the checks described in that text actually found something. The other 1,278 are written out in full anyway, and your model reads all of it

So both problems arrive looking alike, and the only thing separating them is a sentence of English. Whoever reads that sentence is choosing what gets fixed first. Hand the file to a model and you have chosen the model.

That is the most expensive way there is to sort a list, and the only one that sorts it differently each time you ask.

Finding things is the easy part

The obvious move is to point a model at your code and ask what is wrong with it. A team of researchers did exactly that, carefully, and published the result in a paper called Argus. Their tool read a large Java project, 841,000 lines, and reported back.

13,962
places in one Java project that an AI flagged as possibly vulnerable, each a spot where data from outside might reach something dangerous
0
of those 13,962 were real vulnerabilities. On six of the seven projects in the same study, the AI confirmed nothing at all

The traditional code-analysis tool they compared it against found nothing real on any of the seven projects either, so this is not a story about AI being worse. Both approaches failed at the same step, and it was not the step where things get found.

A list of places where something might be wrong is not a list of things to do.

Producing the list is cheap and solved, whether a scanner does it in a few seconds of computing or a model does it for the price of tokens. Deciding which entries matter is the part nobody has automated, and the reason is the same for both.

What decides is not in your code

Three things make a problem urgent: how bad the bug is, who can reach the thing it sits in, and what the business loses when that thing goes down.

Only the first is in the code. Whether a service faces the public internet, and whether orders stop when it goes down, are facts about your architecture and your company. Nothing in the file the model is reading contains them, and a better model will not change that.

So Draugr asks you once, in a small file beside your code, where a reviewer sees it in the pull request:

components:
  - name: api
    exposure: public
    criticality: important

Draugr combines those two lines with how severe each bug is, using two published tables, and that produces the ranking. Nothing guesses at any point in it. The same declarations and the same published advisories give you the same answer today, next month, and in next year's audit.

A model will not give you the same answer twice

Snyk built a test: ten projects, each with a known list of the bugs actually in it. They ran the same scan 300 times and compared every run against that list.

80 of 161
problems an AI reported that were not actually in the code showed up in only one of five identical runs. Ask again and it invents a different set
134 of 158
of the bugs that were genuinely in the code showed up in all five runs. An AI recognizes a real problem reliably, and invents the rest unreliably

A serious problem that appears in three runs out of five is one your build lets through 40% of the time.

The quieter failure is what this does to marking something as accepted. An ordinary scanner's false alarm is the same false alarm on every run, so somebody reviews it once, marks it as accepted, and never sees it again. A model words its false alarms differently each time, so there is nothing steady to mark. Marking things as accepted is how every team survives a backlog it inherited, and it stops working.

Has to come out the same every time

What was found, how severe it is, the order, and whether the build fails. These decide what ships, who accepted what, and what an auditor is shown next year.

Can vary freely

The explanation, the wording, a draft fix, the argument in an exception. A second opinion here is useful, and nothing further down breaks because the phrasing changed.

No amount of prompting makes a model repeatable. It works by picking from a range of plausible answers, and picking better answers does not turn into picking the same one every time.

Looking at everything is expensive

Here is one scan of draugr-demo, our deliberately vulnerable public sandbox, on 2026-10-05. Nine tools, 10.82 seconds. P1 is the top of the ranking and P4 the bottom.

DRAUGR  FAIL  draugr-demo 1.0  10.824s

 221 P1 650 P2 232 P3 18 P4

CONTROLS
  iac         FAIL   4 P1 5 P2 15 P3 16 P4
  images      FAIL   191 P1 267 P2 37 P3
  licenses    FAIL   353 P2 178 P3
  provenance  pass   no findings
  sast        FAIL   7 P1 8 P2 2 P4
  sca         FAIL   18 P1 17 P2 2 P3
  secrets     FAIL   1 P1

MEASURED AGAINST
  provenance  cosign · coverage: 1 of 2 images checked, 1 unsigned · scope: 1 signer ·
              pinning: none, all by tag
  sca         govulncheck v1.8.0;db@2026-10-01 20:24:15 · database: local copy, fetched
              2026-10-04

COMPONENTS
  api         FAIL   217 P1 646 P2 228 P3
  storefront  FAIL   4 P1 4 P2 1 P3
  platform    pass   3 P3 18 P4

SIGNALS
  KEV           2 findings raised
  EPSS          3 findings raised
  reachability  govulncheck · 2 reachable, 2 unreachable

ACCEPTED
  config.exclude  1 finding suppressed
  VEX             1 finding excused

That run found 1,121 problems. Here is what each version of them costs to hand to a model.

What you could give the model
All 1,121 problems, in full2,535,608 bytes
Only the 221 at the top of the ranking642,729 bytes
The summary: the verdict, the counts, what was already accepted, and the twenty most urgent problems25,879 bytes

The bars are to scale. The summary is about one hundredth of the full list, and it answers the question a model asks most often: am I blocked, and how badly?

98×
more text in the full list of problems than in the summary that answers for them, with 10.82 seconds of ordinary computing in between.

A model working through a task does it in steps, and every step re-sends everything that came before it. Give it a large file at step one and you pay for that file again at step two, and at step three, and onward. Caching makes each repeat cheaper without making it stop happening.

One team instrumented a run over a single 800-line pull request and was billed for 156 million tokens, about $41, of which the model's own writing was 0.19%. That is one team's measurement rather than a controlled study.

What a price does to behavior matters more than what it does to the invoice. At twenty dollars a run, nobody runs it twice, and a result nobody re-ran is a result nobody checked. A scan you can repeat for a few seconds of computing is a scan whose second answer you can compare against its first. Draugr remembers the answer for a given commit, so asking again at that commit returns what it worked out the first time.

Give the model the other job

Draugr's MCP server hands a model the 25,879-byte summary first, then more of the ranked list when it asks, then the detail of one problem. It never sends the catalog of checks. Everything that comes back is structured: the rank, what produced it, which tool said so, the file and the line, and the reason recorded against anything already accepted.

With that underneath it, a model spends its budget on the work that needs judgment.

Ask it toBecause
Explain this to the team that owns the serviceNobody there reads security scores, and explaining is what models are good at
Draft the upgrade and the tests around itA version bump that might change behavior is exactly the kind of writing to hand over
Write the exception, with a reason somebody can argue withThe reason is what an auditor reads. The model drafts it and a person signs it
Answer who accepted this, and whenThe record already exists, so the model reads it instead of reconstructing it

None of those decide whether the build fails. That was decided by two published tables before the model was called, and it will be decided the same way tomorrow.

The same model, handed two different things

What reaches itAn ordinary scannerDraugr
Shapeone format per tool, three ways of writing a file pathone shape, whichever tool found it
How bad it ismissing, or two scores that disagreeon every problem, one scale, decided once
Orderthe model's, worked out afresh each runpublished tables, nothing guessed
Your business contextnot in the repository, not recoverable from itdeclared beside the code
Something already acceptedthe wording moves, so it comes backstays accepted, with a reason and the commit that wrote it
What it has to readeverythingthe answer

Building that structure is not free. The Argus researchers spent eight times the tokens getting their list from 13,962 entries down to 19. Draugr pays that cost once per commit, in 10.82 seconds, and every model that asks afterwards reads the result instead of recomputing it.

Try it on your own code

draugr scan .

You do not need to set anything up first. It works out which checks apply from what is in the directory, runs them, and gives you one ranked answer. Then add exposure and criticality to the components that have them, and watch which problems move.

If you want to see the difference for yourself, run those scanners directly and read what they hand back before Draugr touches it.

(Sandbox figures from a scan of draugr-demo on 2026-09-19, Draugr 0.127.0. Advisories move and rule sets grow, so re-run it rather than trusting the numbers. The 13,962 figure is from Liang, Xie et al., Argus: Reorchestrating Static Analysis via a Multi-Agent Ensemble. The repeatability figures are from Snyk, VulnBench JS 1.0.)