You do not have to write any of this by hand.
init reads the repository and
writes the whole file, including the branches to audit and every optional input
already wired to a secret: see the Quickstart. This page is the
reference for what it wrote, and for changing it afterwards.Minimum setup
No API key, no secret. The deterministic layer alone scores F1 0.889 against the project’s own evaluation, so this is worth running before you decide anything about model spend..github/workflows/contract-audit.yml
A Markdown summary is written to the run’s job summary automatically, so there
is something readable even before the SARIF upload is configured.
Getting the three paths right
Most first runs fail on one of these.
The last two are what differ between projects:
language is detected from the markers in the last column and only needs
setting in a polyglot repository, where the first marker found wins and may not
be the one you meant.
Telling the contract apart from the dashboard
Most codebases register two APIs in one place: the one integrators hold an API key for, and the one a dashboard or admin console talks to with a session token. Only the first was ever promised to anyone. Their paths do not separate them, which is why a list of path globs goes stale the week after it is written. What separates them is which guard they sit behind.Route middleware is extracted for every supported language. Where a project supplies
none, the run fails and says so instead of quietly excluding every route. Use
exclude-paths there instead.What the guard tells you beyond the filter
Restricting the audit to one guard also answers a question nobody asks in code review: what can this credential actually reach? The findings that come back are exactly the endpoints behind the guard that reads the credential your spec promises integrators. An endpoint you believed was dashboard-only appearing in that list is a finding about your architecture, not your documentation. It happens for an ordinary reason. A guard written to accept an API key or a session token protects every route behind it with both, so a key issued to move money also reaches account management. Nothing contradicts it, because nothing wrote it down. Two rules report the clear-cut cases directly, wherever route middleware is recorded:
Both are reported per file and only for files that guard something, so a project
applying authentication once at application level gets silence rather than a page
of false alarms.
Routes that are not part of the contract
Most codebases register endpoints no integrator was ever promised: a dashboard’s own session routes, internal health checks, an admin surface. Reporting them as drift is not wrong, just irrelevant, and a report that is mostly irrelevant stops being read.strip-prefix has been removed, and * crosses slashes: /auth/* covers
/auth/me/password. A trailing /* covers the collection itself as well, so
/auth/* also excludes /auth while leaving /authorize alone. Commas work as
well as newlines, for a short list on one line.
An excluded path leaves the audit in both directions. It counts as neither
missing from the spec nor missing from the code, because the spec is not wrong to
stay quiet about a route you have declared internal.
Excluding every endpoint fails the run rather than reporting a clean audit. The
usual way to do that by accident is writing the prefix back in:
/api/v1/*
matches nothing, because the prefix is already gone by the time patterns are
applied.Adding the judgment pass
The deterministic rules settle everything mechanical. Three kinds of drift need reading comprehension instead: a handler that requires a field the spec calls optional, a default that quietly changed, and a validation bound loosened below what is documented.OPENROUTER_API_KEY under Settings → Secrets and variables → Actions.
Any OpenAI-compatible endpoint works; set base-url to point elsewhere.
API keys and secrets covers where each credential comes from, and
why a pull request from a fork never receives one.
Every claim, from the rules and the model alike, is executed against your handler
before it reaches the report. In the project’s own evaluation the model’s raw
precision was 0.23; the gate refuted every false claim and the report came out
at precision 1.0. What you end up reading has been checked against your own
code. See the verification gate.
Handing findings to a coding agent
Every run produces a fix brief: one document covering every finding, what was observed, and the test that proved it. There are three ways to use it, and none of them need storage set up. The files are uploaded as a GitHub Actions artifact, which needs no bucket, no account and no credentials.- Copy it
- Download the brief
- Download the zip
The pull request comment contains the whole brief in a collapsible block.
Expand it, copy it, paste it into Cursor, Codex or Claude Code. Nothing to
download.Best when you want a fix started immediately and do not care about running
the tests yourself.
The brief opens by telling the agent the one judgement the tool deliberately
leaves alone: whether the code drifted or the document went stale. Getting that
backwards turns a documentation edit into a breaking change for everyone already
integrated, so the brief asks the agent to decide explicitly and say why.
Inputs
Outputs:
findings, critical, high, sarif, summary, brief,
brief-zip, brief-dir.
Turning on self-improvement
The auditor can learn from its own mistakes across runs, but only if you tell it where to keep the record. There is no default location and no shared store. Leavememory-url out and it keeps no memory at all.
Two things change when you set this. Each run reads the ledger before auditing
and appends to it afterwards, so the store has to be reachable from the runner.
And the verification gate starts running against your repository, because a claim
with no verdict teaches nothing: a temporary test is written next to your code,
executed, and deleted. A claim its test disproves is dropped from the report. One
whose test cannot be built is kept and marked, never silently lost.
Alerting a human
Two different jobs, deliberately separate.- Slack or Telegram
- Data sink
- PR comment
Formatted, verified-only, and silent on a clean run.See notifications for the formatting and filtering rules.
Rolling it out to a team
1
Report only
fail-on: none with continue-on-error: true. Nothing blocks.2
Put findings on the diff
Add the SARIF upload, so they appear where people already look.
3
Clear the backlog
Fix what is real. Record deliberate differences in
auditor/memory/allowlist.json. Each entry needs a reason and a date, so the
allowlist does not become a place where findings go to be forgotten.4
Tighten
fail-on: critical, then high once the signal is trusted.Scheduled audits
Pull request runs catch drift as it is introduced. A weekly run catches the drift that was already there before you adopted the tool, and drift in the spec itself.Troubleshooting
Every route reported as missing from the spec
Every route reported as missing from the spec
strip-prefix is wrong. If code registers /api/v1/payouts and the spec
documents /payouts, set strip-prefix: /api/v1.The report is mostly routes that were never meant to be public
The report is mostly routes that were never meant to be public
Dashboard session routes, health checks and admin surfaces are registered in
code and deliberately absent from an integrator-facing spec, so every audit
reports them. Leave them out with
exclude-paths, one glob per line, matched
against the path as the spec writes it.manifest unknown when pulling the image
manifest unknown when pulling the image
The package is private, or was never linked to its repository. Check with
gh api /user/packages/container/contract-auditor --jq '.visibility'.
Publishing from a public repository normally yields a public package
automatically; if it reports private, set Package settings → visibility →
Public.denied: permission_denied when publishing
denied: permission_denied when publishing
Settings → Actions → General → Workflow permissions → Read and write.
The workflow requesting
packages: write is not enough on its own; the
repository default caps what the token can be granted.Findings show verdict: error
Findings show verdict: error
The gate could not compile or run its generated test, usually a Go toolchain
mismatch: the image pins Go 1.24. These findings are kept and flagged, never
silently dropped, because a broken toolchain must never look like a clean
bill of health. Deterministic findings are unaffected; they execute nothing.
The run reported unread_endpoints
The run reported unread_endpoints
The model returned something unparseable for those endpoints even after a
retry, so the judgment pass did not read them. That gets reported rather than
hidden, because an endpoint nobody read is not the same as an endpoint with
no drift.
TypeScript findings show unsupported
TypeScript findings show unsupported
Route extraction works for TypeScript; the verification gate does not yet.
Unverified claims are not let through, so they are reported as unsupported.