← All articles

Your agent doesn't have a token problem: what raw scanner output costs an AI assistant

July 28, 2026 · Wilson Santos

Gitleaks found an RSA private key committed to a repository. Here is the entire finding, verbatim, with only the snippet of the key itself removed:

{
  "ruleId": "private-key",
  "message": { "text": "private-key has detected secret for file /tmp/demo-real/app/config.example.pem." },
  "locations": [{ "physicalLocation": {
    "artifactLocation": { "uri": "/tmp/demo-real/app/config.example.pem" },
    "region": { "startLine": 1, "endLine": 8 }
  }}],
  "partialFingerprints": { "commitSha": "", "author": "", "date": "" },
  "properties": { "tags": [] }
}

Read it for what it doesn't say. There is no level, because SARIF has a field for severity and this finding declines to use it. No score, no tags. The fingerprints that would let you track the finding across scans are present and empty. The path is absolute, and it points into a temporary checkout that will not exist in ten minutes.

A leaked signing key and a missing trailing newline arrive at your agent looking exactly alike. The only thing separating them is a sentence of English.

That is not a Gitleaks bug but a SARIF file doing what its author intended. It is just that its author was writing for a human who would read the message. If you've handed the output to a model and asked what should I fix first, you have a translation problem, and it will not be solved by making the file smaller.

The same repo, four dialects#

We ran the four scanners Draugr uses against draugr-demo, our public sandbox, which is twelve files, 56 findings between them. Then we read what came back, rather than how much of it there was.

Thirty percent of the findings have no severity where SARIF puts severity. All 16 Semgrep results and the Gitleaks one come back with level absent. Semgrep does supply a severity, on the rule definition, in defaultConfiguration, several hundred kilobytes away from the finding that references it. An agent that reads result.level and finds no such key has to already know to go looking. Gitleaks doesn't supply one anywhere.

They don't agree on where a file is. Trivy writes repo-relative paths against a base it calls ROOTPATH. Semgrep writes absolute paths against one it calls %SRCROOT%, and Gitleaks writes absolute paths and declares no base at all. Worse, those absolute paths point into the temporary checkout the scan ran in, /tmp/demo-real/app/config.example.pem, a directory that will not exist by the time anyone acts on the finding.

They don't agree on how bad things are, even within one tool. Trivy carries both CVSS v3 and CVSS v4 scores for 11 of its rules, and the two disagree on all 11:

CVSS v3CVSS v4
CVE-2018-1000656 (Flask)7.58.7
CVE-2019-10906 (Jinja2)8.67.7
CVE-2020-28493 (Jinja2)5.36.9

Note the middle row: v3 ranks the Jinja2 sandbox escape above the Flask denial-of-service, and v4 ranks it below. Pick a different field and you get a different fix-first list. Trivy does nominate one in security-severity: "7.5", the SARIF-standard field, but only an agent that knows that field is authoritative will ignore the 8.7 sitting three lines away.

And the same problem shows up twice under two names. That planted private key is found by Gitleaks as private-key and by Semgrep as generic.secrets.security.detected-private-key.detected-private-key. One file, one key, two rule ids, two tools and two different severity conventions, neither of which says it is urgent.

This is the cost, and it isn't tokens#

Every one of those is a decision somebody has to make before an agent can act. Which severity field wins, and what to do when there is not one. How to turn a temp path into something that survives the scan. Whether two findings are the same finding. And then make again, next quarter, when a scanner changes its output.

That's not prompt engineering. That is writing a security tool, the boring half, the half with no demo, the half that breaks silently and stays broken for months because the output still looks plausible.

The bytes are real, but they are a symptom. Those four scanners emit 2,304,902 bytes to report 56 findings. About 94% of that is rule catalogs, every rule the scanner knows, matched or not, with Semgrep alone describing 1,058 rules that never fired. Strip those and you're down to 138,417 bytes, which is worth doing and about twenty lines of code. It also gets you nowhere, because what's left is 138 KB of four-dialect JSON, and the dialects are the problem.

Draugr reports the same 56 findings in 36,358 bytes, 3.8× smaller again than the stripped version. That isn't compression but what's left when you stop hauling four rule catalogs, three path conventions and two CVSS scales around to describe 56 problems. The translation is what makes it small; the size is just the receipt.

Four dialects, one answer On the left, four ways the same repository is described: Gitleaks supplies no severity level anywhere, Semgrep leaves it absent from the finding, and Trivy carries both a CVSS v3 and a CVSS v4 score that disagree. In the middle, one translation guarantees every finding has a level and a priority, every path is repo-relative, and one severity convention applies. On the right, 56 findings ranked: 21 P1, 22 P2, 13 P3 and no P4. Four dialects One translation One answer Gitleaks no level, anywhere Semgrep level absent on the finding Trivy · CVSS v3 one score Trivy · CVSS v4 a different score every finding has a level every finding has a priority every path repo-relative one severity convention chosen once, in one place SARIF, then the ranking 56 findings ranked, one dialect P1 21 P2 22 P3 13 P4 0 the private key is P1
The translation is the product. The size of the output is just the receipt.

What one answer looks like#

The point is that all 56 come back in one dialect:

  • Every finding has a level. Not sometimes, and not on the rule definition, but on the finding.
  • Every finding has a priority, P1–P4, computed from what the component is and what it's exposed to. Severity says how bad a bug is in the abstract; priority says whether it's your problem this week.
  • Every path is repo-relative against one declared base, so it still means something after the temp directory is gone.
  • One severity convention, chosen once, in one place, by us.

And the private key is a P1 error, not because Gitleaks said so but because Gitleaks said nothing, and not because Semgrep said so, because Semgrep's rule defaults it to a warning. It's P1 because Draugr's secrets control holds that a committed private key is never a warning. That is a judgement call somebody has to make. The question is only whether it's you, on a deadline, inside a prompt.

Four tools, 56 findings, one ranking:

Draugr · FAIL   (draugr-demo 0.0.0)

Priorities:  P1 21   P2 22   P3 13   P4 0

Controls:
  iac      FAIL   4 high  5 medium  12 low
  sast     FAIL   7 high  9 medium
  sca      FAIL   3 critical  6 high  8 medium  1 low
  secrets  FAIL   1 high

Fix first (top 10 of 56, by priority):
  Priority  Severity  Score  Rule            Control  Scanner  Location
  P1        critical  9.8    CVE-2019-20477  sca      Trivy    app/requirements.txt:4
            PyYAML: command execution through python/object/apply constructor in FullLoader
  P1        high      8.6    CVE-2019-10906  sca      Trivy    app/requirements.txt:5
            python-jinja2: str.format_map allows sandbox escape
  P1        high      8.0    DS-0002         iac      Trivy    app/Dockerfile:1
            Image user should not be 'root'
  …

… and 46 more finding(s).

Trivy, Semgrep and Gitleaks findings interleave in that list by urgency, because by the time it's printed they're the same kind of object.

The loop this makes possible#

Once the answer is one shape, the cheap question becomes available. --format json --compact returns the verdict, per-control counts and priority totals in 756 bytes, with no findings, just whether you're blocked and how bad it is.

That's small enough for an agent to ask on every commit, in a loop, for nothing, and to pull the full 36 KB report only when the answer is yes, and you need to look. That's the difference between security tooling an agent consults and security tooling an agent avoids because the last time it looked it burned half its context window.

Draugr's MCP server exposes exactly that: an assistant can ask for the verdict, then the ranked findings, then the detail, and never see a rule catalog.

Try it against your own repo#

draugr scan .

No config, no descriptor. It infers which controls apply from what's in the directory, runs them, and gives you one answer.

Then run the four scanners yourself, because the argv is in internal/scanners, and read what they hand you. Find the severity on the Semgrep results, work out which of the two CVSS scores to believe, and decide what the leaked key is worth.

Then decide whether you want to maintain that.

(Figures from a build of main against draugr-demo on 2026-07-28. Advisories move and rulesets grow; re-run it rather than trusting the table.)