Connecting GitHub
Cairn earns its evidence from the systems where work actually happens. A connector is how it is reached from outside — the first place it stops trusting anything, and the one place worth being careful.
This page takes a GitHub repository from nothing to a pull request appearing in your Review Queue.
What a connector is, and what it is not
A connector is inbound only. GitHub posts to Cairn; Cairn never calls GitHub. There is no token to store, no App to install, no private key — just a signing secret that Cairn generates and you paste at the provider.
Two properties are worth knowing before you start, because both are load-bearing later:
- Reach is enumerated, never implied. A connector may touch exactly the repositories you list and nothing else. There is deliberately no "all repositories" option.
- Every delivery is signed. Cairn computes
HMAC-SHA256over the exact bytes of the request body and compares it with the header. Nothing is written on behalf of an unverified sender — a failed signature never becomes a row.
1 · Link the connector in Cairn
Go to Connectors and choose GitHub. Name the repositories it may reach, as
owner/name, one per line or comma-separated.
Submitting returns "Waiting for approval", and that is the designed path, not an error. Linking a connector is a Level-5 action — it grants Cairn reach into a system other people can see — so it stops for a human even when an owner asks. Approve it in the Attention Inbox, then submit again.
Cairn then shows you a signing secret, once. It is stored encrypted, and no screen will ever show it again. If you lose it, rotate to issue a new one.
2 · Point GitHub at Cairn
Repository → Settings → Webhooks → Add webhook.
| Field | Value |
|---|---|
| Payload URL | the URL Cairn showed you, ending /api/connectors/…/webhook |
| Content type | application/json |
| Secret | the secret Cairn showed once |
| SSL verification | Enable |
| Events | Let me select individual events — the five below |
Content type is the one to get right. GitHub defaults to
application/x-www-form-urlencoded, which Cairn cannot parse. A hook with a
perfect secret and the wrong content type fails every time, and the error looks
nothing like the cause.
The five events
pull_request · pull_request_review · check_suite · workflow_run ·
deployment_status
Everything else is accepted and ignored, so subscribing to more only adds noise to the delivery log. Leave Pushes unchecked — Cairn ignores push events deliberately.
3 · Bind a Mission — the step people miss
This is the one that catches everyone. The allow-list lets a delivery in; the binding gives it somewhere to land.
Open the Mission this work ships in, and set Ships in to the repository.
Without it, a correctly signed delivery about an allowed repository is accepted,
authenticated, and shelved — recorded as no_mission_ships_in_this_resource.
From GitHub's side that is a 202, indistinguishable from success. Nothing is
broken and nothing happens.
4 · Check it works
- GitHub's Recent Deliveries shows
202. - Cairn's Connectors page shows "last delivery: just now".
- Open a pull request. It appears in the Review Queue badged "from
GitHub", and a committed Mission moves to
reviewable.
When something is wrong
Every delivery returns 401
The signature did not verify. In order of likelihood:
- The secret does not match. Rotate it in Cairn and paste the new one at GitHub. Both ends must change together.
CONNECTOR_SECRET_KEYwas added after the connector was linked. This is the subtle one. Webhook secrets are encrypted at rest with a key taken fromCONNECTOR_SECRET_KEY, or derived fromAUTH_SECRETwhen that is not set. Introducing the variable later changes the key, so the stored secret can no longer be opened — and Cairn answers401because it cannot check the signature at all. Rotating fixes it. SetCONNECTOR_SECRET_KEYbefore linking anything and it never arises.- The content type is form-encoded. See above.
Cairn shows a rejection count on the Connectors page when this is happening, because silence is otherwise the only symptom of a wrong secret.
Deliveries succeed but nothing appears
Almost always the missing Mission binding — step 3. Check the connector's
delivery log: an event recorded as ignored with
no_mission_ships_in_this_resource says exactly this. An event recorded against
a repository you did not list says resource_not_allowed, which means the hook
is sending something the connector was never granted.
Rotating the secret
Connectors → Rotate secret. Like linking, rotation is Level 5, so the first attempt raises a checkpoint; approve it in the Inbox and rotate again. The old secret stops working immediately, so deliveries fail until you paste the new one at GitHub — do the two together.
A GitHub App instead of a repository webhook
A repository webhook is enough for everything described here. A GitHub App is worth it for one reason: its "Only select repositories" installation is a stronger control than anything Cairn enforces internally, because GitHub enforces it — a compromised Cairn still cannot reach a repository the App was never installed on.
Same Payload URL, same secret, no callback URL or private key needed. Read-only
permissions are sufficient: Metadata, Pull requests, Checks,
Actions and Deployments. Leave Contents off — it is only needed for
push, which Cairn ignores.
One constraint to know: a GitHub App has a single webhook URL and secret for the whole App, while Cairn's are per connector and a connector belongs to one organization. One App therefore maps to one Cairn workspace. Per-repository webhooks do not have this limit.
Other systems
The Generic webhook connector accepts a signed JSON POST from anything, on
the same governed path: same signature check, same allow-list, same audit trail.
Include a resource field naming one of the resources you listed, and sign the
exact bytes of the body.
What it does not give you is meaning. A deploy.ok from a bespoke script is
stored and emitted, and nothing derives Evidence from it until someone teaches
Cairn what it means.