Wiring a self-hosted AI code reviewer into GitHub — the GitHub App way (and why not a PAT or OAuth app)

Published 2026-07-22 · Target keyword: self-hosted AI code review GitHub App setup

You have decided to self-host an AI code reviewer, and now you have to connect it to GitHub. There are three ways to do that — a personal access token, an OAuth app, or a GitHub App — and only one of them is right for a bot that reads every pull request, posts reviews, and controls a merge gate. Pick the wrong one and you either over-grant access to your source code or you tie a critical control to one employee's account.

This is a practical guide to the GitHub App path: why it is the correct credential model for a review bot, and how to register, permission, and install one step by step. Every step links to GitHub's official documentation, which carries current screenshots. Gatekeep is the worked example — a self-hostable, governance-first reviewer whose runtime is named mergegate — but the credential reasoning applies to any self-hosted reviewer you deploy.

Three ways to authenticate, and why only one fits

A code-review bot has an unusual authentication profile. It acts on its own, continuously, across many repositories, and it holds enough power to block a merge. That rules out two of the three options fast.

Why not a personal access token (PAT)

A PAT is a long-lived credential tied to a human account. Using one for a bot has four problems that matter for review:

  • It impersonates a person. Every review comment, every check, every action shows up as that user. When they change roles or leave, the bot breaks — and your audit trail says a human did the work.
  • It is coarse-grained. Even fine-grained PATs grant their scopes across everything the user can reach. The token inherits a person's blast radius, not the bot's actual needs.
  • It is long-lived. A PAT sits in your config until someone remembers to rotate it. If it leaks, it stays valid. For a credential that can read all your source, that is the wrong default.
  • It has a single, low rate-limit pool. One user's 5,000 requests/hour is shared with everything else that account does.

Why not an OAuth app

OAuth apps are built for a different job: acting on behalf of a user who clicks "Authorize." That is the right model for a login button or a tool a developer drives interactively. It is the wrong model for an autonomous reviewer, which has no user in the loop and should not borrow anyone's identity. OAuth apps also grant broad, user-scoped access and issue tokens that, again, act as the authorizing human.

Why a GitHub App is the right answer

A GitHub App is a first-class identity of its own — a bot account, not a borrowed human one. That single fact fixes everything above, and it is why GitHub itself recommends Apps for automation. Five properties make it the correct choice for a self-hosted reviewer:

  • Least-privilege permissions. You grant the App a precise, per-resource permission set (read this, write that) and nothing else. The bot gets exactly what review needs.
  • Short-lived installation tokens. The App authenticates with a signed JWT and exchanges it for an installation token that expires in one hour. There is no long-lived secret sitting in your config that can be replayed if it leaks — the useful credential is minted on demand and dies fast.
  • Org-owned credential custody. You register the App inside your own organization. The App ID, private key, and webhook secret are generated in your GitHub org and handed to your self-hosted instance. For a self-host / data-sovereignty story this is the crux: the credentials never transit a vendor server, because there is no vendor server in the path.
  • Per-repo installation. You install the App on selected repositories only. Repos you do not gate are entirely out of reach — the bot cannot see them.
  • Higher, dedicated rate limits. Installation tokens get their own rate-limit pool that scales with your org, rather than competing with a single user's quota.

Read the two credentials together: an org-owned App whose only durable secret is a private key, minting one-hour tokens scoped to a handful of repos, is dramatically less dangerous than a long-lived PAT that inherits a person's full reach. For a bot that reads all your source and guards the merge button, that difference is the whole point.

Registering the App — step by step

Registration takes about five minutes. Do it in the GitHub organization that owns the repositories — not under a personal account, or you lose the org-custody property above. Keep GitHub's docs open alongside for the screenshots.

  1. Open the registration form. In your org: Settings → Developer settings → GitHub Apps → New GitHub App. GitHub docs: registering a GitHub App ↗
  2. Name it and set a homepage. Any unique name works (e.g. acme-gatekeep); the homepage URL can be an internal wiki page or https://gatekeephq.pro.
  3. Configure the webhook. Tick Active, set the Webhook URL to https://<your-host>/webhook — your self-hosted instance, reachable from GitHub over HTTPS — and set a strong Webhook secret. Save that secret now; it becomes the GITHUB_WEBHOOK_SECRET env var later. GitHub docs: using webhooks with GitHub Apps ↗
  4. Set repository permissions exactly as in the table below — least privilege, nothing more. GitHub docs: choosing permissions ↗

The least-privilege permission set — and why each one

This is the part most guides get lazy about. Grant only these, and understand why the bot needs each — if a permission has no line of justification below, it should not be on the App.

Required GitHub App permissions for a self-hosted reviewer
PermissionAccessWhy the reviewer needs it
Pull requestsRead & WriteRead the PR metadata and diff; post the review and inline comments back.
ContentsReadFetch changed file contents so the model sees bounded neighboring context, not just the raw diff. Read-only — the bot never writes to your code.
ChecksRead & WriteCreate and update the mergegate/review Check Run. This is the gate — the status your branch protection rule blocks on.
IssuesRead & WriteReceive and act on the /mergegate override: comment. In the GitHub API, PR comments are issue comments, so gate overrides arrive through this permission.
MetadataRead (mandatory)Baseline repository access. GitHub requires it on every App.
MembersRead (optional)Only if you later put GitHub teams in the override allowlist. Skip it entirely if you allowlist by username.

Notice what is absent: no Administration, no Actions, no organization-level write, no broad account scope. A reviewer does not need to reconfigure your repo or your org, so it is not granted the ability to. That is the concrete meaning of least privilege — and it is exactly what a PAT cannot give you.

  1. Subscribe to events. Two are enough: Pull request (so a review fires when a PR opens or updates) and Issue comment (so the /mergegate override: command is delivered). Subscribing to only what you handle keeps webhook volume — and your attack surface — small.
  2. Restrict installation to Only on this account, then click Create GitHub App. This is a private, org-internal App, not a public Marketplace listing.
  3. Copy the App ID from the app's settings page. It is not a secret, but you will need it for config.
  4. Generate the private key. On the same page, scroll to Private keys → Generate a private key; a .pem file downloads. This is the sensitive credential — treat it like a root key, store it in your secrets manager, and never commit it. GitHub docs: managing private keys ↗
  5. Install the App. In the app's sidebar: Install App → your org → Only select repositories → pick the repos you want gated. Start with one or two. GitHub docs: installing your own GitHub App ↗

How the credentials meet the container

Three pieces connect the registered App to your running instance. In Gatekeep's config.yaml they map like this:

config.yaml — github blockyaml
github:
  app_id: 4363776                    # the App ID from the settings page
  private_key_path: /run/secrets/mergegate_app.pem
  webhook_secret_env: GITHUB_WEBHOOK_SECRET
  api_base_url: https://api.github.com   # override for GitHub Enterprise Server
  • App IDgithub.app_id. A plain identifier, safe to keep in config.
  • Private key PEM → mounted at github.private_key_path. The compose file mounts your ./secrets/app.pem read-only; nothing writes it back.
  • Webhook secret → the env var named by github.webhook_secret_env (default GITHUB_WEBHOOK_SECRET). Every incoming webhook is HMAC-verified against this secret; a bad or missing signature is rejected with 401. This is what stops a forged POST to your /webhook endpoint from triggering a review or a gate change.

At runtime, the flow is the payoff of choosing an App. Your instance signs a short-lived App JWT with the private key, exchanges it for a one-hour installation token scoped to your installed repos, and uses that token for every API call — reading the diff, posting the review, updating the Check Run. Tokens are cached in memory per installation and refreshed on expiry. No long-lived, broadly-scoped credential is ever used for repo data. That is the short-lived-token property from the top of this post, made concrete.

Running GitHub Enterprise Server?

Point github.api_base_url at your GHES API base instead of https://api.github.com, and App auth, diff fetch, review posting, and Check Run creation all route to your server. The same App model works on-prem.

Verify it worked — and troubleshoot if it didn't

Open a test pull request in a gated repo. Within about a minute you should see a Gatekeep review appear, along with the mergegate/review check on the PR. Once that check exists, add it to your branch protection rule as a required status check — that is the step that turns the review from advisory into a real merge gate.

If nothing arrives, work through the delivery path from GitHub outward:

  • Check Recent Deliveries. In the app's settings: Advanced → Recent Deliveries shows every webhook GitHub sent, the payload, and the HTTP response your instance returned. A 2xx means your instance received it; a 401 means the HMAC signature failed — your GITHUB_WEBHOOK_SECRET does not match the secret you set on the App. No delivery at all means GitHub could not reach your Webhook URL (DNS, TLS, or firewall). GitHub docs: viewing webhook deliveries ↗
  • Confirm the install target. If deliveries look healthy but a specific repo is silent, verify the App is actually installed on that repositoryOnly select repositories means exactly that.
  • Re-check permissions. A missing Checks permission means the review posts but no gate appears; a missing Issues permission means overrides are never received.

The Recent Deliveries tab is the single most useful debugging surface, because it collapses "is it GitHub, the network, or my app?" into one screen with response codes.

GitHub only, today GitLab / Bitbucket — Roadmap

To be plain about scope: Gatekeep supports GitHub only today (github.com and GitHub Enterprise Server via the configurable API base URL). GitLab is a roadmap item; Bitbucket and Azure DevOps are not planned for the MVP. If your platform is not GitHub, this walkthrough does not yet apply — do not assume the integration exists.

Frequently asked questions

Do I create my own GitHub App, or install yours?

On a self-hosted deployment you create your own App inside your own organisation, and the app_id in your config.yaml is yours. That is deliberate: the App's private key is a credential for your repositories, and it should never sit with a vendor. A managed cloud tier where you install our App is roadmap, not shipped.

Does this work with GitHub Enterprise Server?

The GitHub API base URL is configurable, so pointing the reviewer at an internal GitHub Enterprise Server instance is the intended path rather than a workaround. That matters for disconnected networks, where it is one of only two outbound connections the application makes. See running AI code review in an air-gapped environment for the rest of that setup.

What happens if our firewall blocks inbound webhooks?

Reviews will silently not start, which is the most common failure in a locked-down network and the least obvious. GitHub delivers pull request events to your container over a webhook, so that path has to be reachable from wherever your git host runs. Check GitHub's webhook delivery log first; it shows the response code and tells you immediately whether the request ever arrived.

Can I install it on only some repositories?

Yes. GitHub Apps are installed per organisation with a repository selection, so scoping to a subset is a standard install-time choice rather than something the reviewer has to implement. Starting with one repository is the sensible way to pilot.

Why a GitHub App rather than a personal access token?

A personal access token is tied to a human, inherits their access, and dies when they leave. An App is an identity of its own with scoped permissions and short-lived installation tokens. For anything that will appear in an audit as a control, tying it to an individual's credential is a finding waiting to happen.

The short version

For a self-hosted AI code reviewer, register a GitHub App in your own org — not a PAT (long-lived, impersonates a human, coarse) and not an OAuth app (built for on-behalf-of-user flows). Grant a tight permission set: Pull requests RW, Contents R, Checks RW, Issues RW, Metadata R. Subscribe to Pull request and Issue comment events. Feed the App ID, private key path, and HMAC webhook secret into your config, and let the container mint one-hour installation tokens at runtime. Install on selected repos, open a test PR, and watch the mergegate/review check land. The result is a bot with its own identity, least-privilege access, short-lived credentials, and org-owned custody — which is exactly the posture a control on your merge button should have.