init wrote it
and nothing here is required. Adding a secret turns a feature on without editing
any YAML.
The audit itself needs no credential at all. The deterministic layer costs
nothing, calls nothing, and finds most drift. Everything on this page is
optional.
Setting a secret
1
Open the repository's secrets
Settings → Secrets and variables → Actions → New repository secret.
The name must match exactly, including case.
2
Paste the value, not a file path
GitHub stores the value encrypted and masks it in logs. It cannot be read
back afterwards, only overwritten.
3
Re-run the workflow
Secrets are read at job start. A run already in progress will not pick up a
secret added while it is running.
environment: on the job; the generated
workflow does not use one.
What each secret turns on
None of them is needed for the audit to run and comment on a pull request.
OPENROUTER_API_KEY
Turns on the stage that reads code and decides whether it matches a sentence in your documentation. Without it the run is free and still catches most drift.1
Create the key
Sign in at openrouter.ai/keys and create a
key. It begins
sk-or-v1-.2
Add credit
OpenRouter is prepaid. The evaluation in this repository — 16 cases, 159
model calls — cost $0.076, so a few dollars lasts a long time.
3
Set it
gh secret set OPENROUTER_API_KEYUsing a different provider
The client sends whatever model id it is given to whatever endpoint it is pointed at, so any OpenAI-compatible API works. Setbase-url alongside the
key:
https://openrouter.ai/api/v1 with z-ai/glm-5.3-flash. Azure
OpenAI, Together, Groq, Fireworks and a self-hosted vLLM all speak the same
protocol. The secret’s name is yours to choose — only the input matters.
Keeping the bill small
workers controls parallelism, not spend. What controls spend is how many
endpoints the model is asked about, and the deterministic layer already settles
most of them before the model is called. Two further levers:
contract-middlewareremoves routes outside the published contract from the audit entirely, so no model call is ever made for them.exclude-pathsdoes the same for paths you name.
Slack
Sends a short message with counts and severities. Never endpoint paths — a channel holds people who cannot read the code.1
Create an app
api.slack.com/apps → Create New App →
From scratch. Name it, pick the workspace.
2
Enable incoming webhooks
Incoming Webhooks → toggle on → Add New Webhook to Workspace →
choose the channel.
3
Copy the URL
It looks like
https://hooks.slack.com/services/T…/B…/…. This URL is the
credential; anyone holding it can post to that channel.4
Set it
gh secret set CONTRACT_AUDIT_SLACK_WEBHOOK_URLTelegram
1
Create the bot
Message @BotFather, send
/newbot, follow the
prompts. He replies with a token like 123456789:AA….2
Find the chat id
Add the bot to the group (or message it directly), send it any message,
then:A group id is negative, e.g.
-1001234567890. A direct-message id is
positive.3
Set both
Both channels are gated on verification and on
notify-min-severity, which
defaults to high. A clean run stays silent rather than posting “nothing to
report” every morning.Your own sink
CONTRACT_AUDIT_WEBHOOK_URL receives the full JSON report as a POST — the
whole findings array, not a formatted message. Intended for a database or a
dashboard rather than a chat channel.
CONTRACT_AUDIT_WEBHOOK_SECRET is sent with it as the X-Auditor-Token header
so your receiver can tell a real delivery from anyone who guessed the URL:
Memory, for self-improvement
The ledger of which claims survived their test. It lives wherever your credentials point and nowhere else: nothing is stored in your repository, nothing ships inside the image, and no history is shared between projects. Five backends, chosen by the URL scheme:
For a non-AWS S3 store, give the endpoint in the URL:
Cloudflare R2, end to end
1
Create the bucket
Cloudflare dashboard → R2 → Create bucket. Note the account id from the
URL.
2
Create an API token
R2 → Manage R2 API Tokens → Create API token, permission Object Read
& Write, scoped to that bucket. Copy the Access Key ID and Secret Access
Key — the secret is shown once.
3
Set the secrets
4
Prove it round-trips
make memory-check writes a row and reads it back, so a misconfigured
store fails here rather than silently keeping no memory.GITHUB_TOKEN
You do not create this one. Actions mints it per run, and the generated workflow declares what it needs:Resource not accessible by integration, the
repository default is capping the token: Settings → Actions → General →
Workflow permissions → Read and write. A workflow cannot grant itself more
than the repository allows.
Forks
This catches people out, and it is worth knowing before you test on one. A fork does not inherit the upstream repository’s secrets. If you forked a project to try this out, every secret above has to be set again on your fork.gh secret list on the fork shows what it actually has, which is nothing until
you add it.
A pull_request run from a fork gets no secrets at all, even secrets the
base repository holds. This is a GitHub security boundary, not a setting: it
stops a stranger’s pull request from exfiltrating your keys. So on a pull
request from a fork the audit runs its deterministic layer, posts its comment,
and makes no model calls — which is the correct behaviour, and worth expecting
rather than debugging.
Pull requests within one repository, and push runs, have the secrets.
Workflows are disabled on a new fork. Open the Actions tab and choose
“I understand my workflows, go ahead and enable them” before anything runs.
Checking what is set
Rotating and revoking
Secrets cannot be read back, so rotation is always “set the new value over the old one”. Overwrite withgh secret set <NAME>, then revoke the old credential
at the provider — in that order, so no run lands in the gap.
If a key leaks, revoke it at the provider first. Removing the GitHub secret
stops this workflow using it but does nothing about the key itself.