DOCS · INTEGRATIONS

GitHub

specd connects as a GitHub App with three permissions and hour-long tokens — how to register it, what it listens to, and how to test webhooks locally.

On this page

specd talks to GitHub as an App, not as a user with a personal access token. An App's credential mints repository-scoped tokens that expire within the hour and can only reach repositories someone explicitly granted. A PAT carries the full authority of whoever created it, forever, over everything they can see.

What it asks for

PermissionWhy
contents: writePush the setup branch and the spec branches.
pull_requests: writeOpen the pull requests those branches are reviewed in.
metadata: readMandatory for every GitHub App.

That is the complete list. No workflows, no packages, no organisation administration, no secrets.

What it listens to

EventWhat specd does with it
pull_request (merged)Setup branch → mark adopted and index knowledge/. Spec branch → mark the spec delivered and re-index.
push to the default branchRe-index if knowledge/ changed.

It also handles installation and installation_repositories to track revocation, so a removed App stops working immediately. Everything else is recorded and ignored.

Register it

With the API running, the one-click path uses GitHub's manifest flow — you approve, GitHub creates the App, and specd stores the credentials:

terminal
open http://localhost:4000/api/github/app/register

The by-hand path, and installing the App onto specific repositories, are written out step by step in docs/github-app.md in the repository.

Webhooks in local development

GitHub cannot reach localhost. Until the API has a public URL, forward the deliveries with whichever you already have:

terminal
# GitHub CLI
gh webhook forward --repo=<owner>/<repo> --events=push,pull_request \
  --url=http://localhost:4000/api/github/webhook

# or a tunnel, with the App's webhook URL set to the public hostname
ngrok http 4000
cloudflared tunnel --url http://localhost:4000

Without either, nothing breaks: merges are simply not detected, and the "I merged it" button remains the way to record adoption.

Checking it works

terminal
# what specd thinks it is configured for
curl -H "Authorization: Bearer $TOKEN" "$SPECD_API/github/status"

# the deliveries it has actually received for a project
curl -H "Authorization: Bearer $TOKEN" \
  "$SPECD_API/github/projects/$PROJECT_ID/deliveries"

Every delivery is recorded with what specd decided and why — including the ones it ignored. "The webhook arrived and specd chose not to act" and "the webhook never arrived" are different problems, and this tells you which one you have.

How deliveries are trusted

  • HMAC-verified over the raw bytes, in constant time, before parsing. A signature checked after parsing has already run a parser on unauthenticated input.
  • An unset secret rejects everything rather than waving it through. Fail-closed is the only safe default for a webhook endpoint.
  • Deliveries are deduped by delivery id, and specd acts only for a registered repository.