Documentation

Everything you need to run Gatekeep inside your own network: install it with docker compose up, connect your own GitHub App, point it at your own LLM, write your review policy, and turn every pull request into a blocking, audited merge gate. No code leaves your perimeter — the only outbound connections are to your GitHub and your LLM endpoint.

Applies to: Gatekeep MVP (self-host) · Last updated 2026-07-22 · Design-partner / early-access build.

A note on naming

The product is Gatekeep. The current self-host MVP container ships under the working name mergegate, so a few runtime identifiers you will configure use that string literally: the Check Run is mergegate/review, the override command is /mergegate override:, and the private-key mount path is mergegate_app.pem. Use these exact strings — they are what the software emits and matches. The PR ticket-context marker is already gatekeep:context. We call this out so your branch-protection and override configuration line up with reality rather than the brand name.

1 · Overview & how it works

Gatekeep is a governance-first, self-hostable AI code reviewer for GitHub. It runs a review on every pull request using your own LLM endpoint, turns that review into a blocking, compliance-grade merge gate, and records every decision — including overrides with mandatory written justification — in an append-only, hash-chained audit log. It runs as a single container stack inside your VPC or on-prem, and makes outbound connections to exactly two places: your GitHub API and the LLM endpoint you configure. No telemetry, no phone-home.

1 · PR event opened · sync 2 · Webhook HMAC · /webhook 3 · Review context → your LLM 4 · Gate mergegate/review 5 · Audit hash-chained log /mergegate override: … allowlisted · justification logged flips green
The review pipeline. Every stage — review, gate decision, and override (granted or denied) — is written to the audit log.

Step by step, what happens when a pull request moves:

  1. A pull request is opened, updated (synchronize), or reopened against a branch in scope.
  2. GitHub sends a signed webhook (X-Hub-Signature-256 HMAC) to your instance at POST /webhook. Bad signatures are rejected with 401.
  3. The receiver acknowledges fast and enqueues the event on an in-process worker pool. Rapid pushes to the same PR are coalesced so a synchronize storm doesn't fan out into duplicate reviews.
  4. The orchestrator builds a deterministic, size-bounded diff context — diff hunks plus bounded surrounding code — and injects your policy.md standards and any ticket context. No LLM runs in this step.
  5. It calls your configured OpenAI-compatible LLM once and parses strict-JSON findings, each carrying a severity (info / low / medium / high / critical) and a short rationale. Malformed JSON is repaired where possible.
  6. Findings at or above comment_threshold are posted as a consolidated review summary plus inline comments where a file+line anchor is available. Quieter findings are suppressed from the PR timeline but still recorded in the audit log — noise control without losing evidence.
  7. It creates or updates the mergegate/review Check Run on the head SHA: red if the highest severity is at or above gate_threshold, green otherwise. A reviewer error or timeout fails safe to red — silence never looks like approval.
  8. With branch protection requiring that check, GitHub programmatically disables the merge button while it is red.
  9. An authorized reviewer can comment /mergegate override: <justification>. If they are on the allowlist and the justification meets the minimum length, the check flips green and merge is unlocked.
  10. Every event — review posted, gate decision, override granted or denied — is appended to the hash-chained audit log (SQLite + JSONL), exportable as SOC 2 / ISO 27001 / HIPAA change-management evidence.

What "deterministic" means here Built

The deterministic part of the MVP pipeline is the diff parsing and context builder (step 4): the same PR always produces the same bounded prompt, with no model involved. The review itself (step 5) is the LLM's. A separate deterministic AST pre-pass that emits findings before the LLM runs (dual-layer review) is on the roadmap, not in the MVP. Roadmap

2 · Quickstart (self-host, Docker Compose)

Gatekeep is designed to reach a working, reviewing, gating instance in under 30 minutes. You provide three files — config.yaml, an optional policy.md, and your GitHub App private key — plus a handful of secrets via .env.

About the container image Early access

The image is built by CI and published to ghcr.io/gatekeephq/gatekeep. During early access the package is private: design partners receive pull access (a registry token) at onboarding, or the source bundle to build locally with build: .. Public, anonymous docker pull (GHCR + a Docker Hub mirror) ships with GA.

1. Provide your config, policy, and secrets

shellbash
cp config.yaml.example config.yaml     # edit app_id, llm.base_url, model, allowlist...
cp policy.md.example   policy.md       # optional: your coding/compliance standards
cp .env.example        .env            # set GITHUB_WEBHOOK_SECRET (+ LLM_API_KEY if needed)
mkdir -p secrets && cp ~/Downloads/your-app.private-key.pem secrets/app.pem

2. The docker-compose.yml

This is the shipped compose file. Single app container plus a durable volume for the SQLite/JSONL audit store; Postgres is an optional scale-path profile. Only the Gatekeep application image: line is a placeholder — everything else is verbatim.

docker-compose.ymlyaml
# MergeGate — `docker compose up` and you're live.
# Default: single app container + a volume for SQLite/JSONL audit.
# Optional Postgres (scale path): `docker compose --profile postgres up`.

services:
  mergegate:
    build: .
    image: ghcr.io/gatekeephq/gatekeep:latest   # private during early access — pull access at onboarding; see note above
    ports:
      - "8080:8080"
    volumes:
      # Your single source of truth + optional coding standards, mounted read-only.
      - ./config.yaml:/config/config.yaml:ro
      - ./policy.md:/config/policy.md:ro
      # GitHub App private key (customer-provided; never baked into the image).
      - ./secrets/app.pem:/run/secrets/mergegate_app.pem:ro
      # Durable audit store.
      - mergegate_data:/data
    environment:
      - MERGEGATE_CONFIG=/config/config.yaml
      # Secrets are provided via env / .env — never committed to config.yaml.
      - GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET}
      - LLM_API_KEY=${LLM_API_KEY:-}
      - AUDIT_EXPORT_TOKEN=${AUDIT_EXPORT_TOKEN:-}
      # Uncomment to use Postgres instead of SQLite (with the profile below):
      # - AUDIT_DB_URL=postgresql+psycopg://mergegate:mergegate@postgres:5432/mergegate
    restart: unless-stopped

  # --- Optional scale path: Postgres. Enable with `--profile postgres`. ---
  postgres:
    image: postgres:16-alpine
    profiles: ["postgres"]
    environment:
      - POSTGRES_USER=mergegate
      - POSTGRES_PASSWORD=mergegate
      - POSTGRES_DB=mergegate
    volumes:
      - mergegate_pg:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  mergegate_data:
  mergegate_pg:

3. The config.yaml

This is the single source of truth, mounted read-only at /config/config.yaml. Secrets are referenced by env-var name or file path — never inlined. The example below is the shipped config.yaml.example, annotated; each section is documented in detail later on this page.

config.yamlyaml
# MergeGate configuration — the single source of truth (mounted at /config/config.yaml).
# Secrets are referenced by env var name / file path here, never inlined.

github:
  app_id: "123456"                                # your GitHub App's App ID
  private_key_path: /run/secrets/mergegate_app.pem  # mounted PEM (see compose)
  webhook_secret_env: GITHUB_WEBHOOK_SECRET       # env var holding the webhook secret
  api_base_url: https://api.github.com            # override for GitHub Enterprise Server

llm:
  # OpenAI-compatible endpoint. Works with vLLM, Ollama (/v1), Azure OpenAI,
  # Bedrock/LiteLLM gateways — no provider-specific code.
  base_url: http://vllm.internal:8000/v1
  api_key_env: LLM_API_KEY                         # optional; blank for keyless gateways
  model: qwen2.5-coder-32b-instruct
  temperature: 0.1
  max_tokens: 2000
  timeout_s: 90
  # extra_headers:                                 # e.g. for Azure/gateway auth
  #   api-key: ${LLM_API_KEY}

review:
  policy_file: /config/policy.md                   # optional NL standards injected into the prompt
  gate_threshold: high                             # info|low|medium|high|critical
  comment_threshold: medium                        # suppress noise below this from the timeline
  max_diff_bytes: 400000                           # context budget guardrail
  neutral_on_error: false                          # false = fail-safe (reviewer error => red gate)
  scope:
    branches: [main, master, "release/*"]          # only gate PRs targeting these
  # F8 — review the diff against the PRD/Jira intent pasted in the PR description.
  ticket_context_enabled: true                     # default on; just prompt text
  ticket_context_marker: "gatekeep:context"        # fenced-block marker in the PR body
  max_ticket_context_chars: 4000                   # cap so it can't blow the prompt budget

governance:
  override_allowlist: ["alice", "bob"]             # usernames (teams post-MVP; see README)
  min_justification_len: 40

audit:
  sqlite_path: /data/audit.db
  jsonl_path: /data/audit.log.jsonl
  export_token_env: AUDIT_EXPORT_TOKEN             # bearer token for GET /audit/export

4. Bring it up and verify health

shellbash
docker compose up -d
curl localhost:8080/healthz          # -> {"status":"ok",...}
curl localhost:8080/healthz/llm      # -> pings your configured LLM endpoint

Once both probes are green, add mergegate/review as a required status check in branch protection (see Merge gate & overrides) and open a PR with a seeded issue to prove the gate blocks. There is also an offline demo that runs the whole pipeline against a sample diff with a mock GitHub client and a canned mock LLM — zero credentials, zero network — useful for a first look (python -m mergegate.demo from the source bundle).

3 · Connect GitHub

Gatekeep authenticates as a GitHub App you register in your own organization — not an OAuth app, not a personal access token. This is the crux of the data-sovereignty story: the App ID, private key, and webhook secret are generated inside your GitHub org and handed to your self-hosted instance. They never transit a Gatekeep-controlled server, because there is no Gatekeep server in the path.

Register the App — step by step (~5 minutes)

You can register from the setup manifest we provide during onboarding (it pre-fills everything below), or by hand in about five minutes. Each step links to GitHub's official documentation, which carries current screenshots of every screen — keep it open side by side.

  1. Open the registration form. In your GitHub organization: Settings → Developer settings → GitHub Apps → New GitHub App. Register it in the org that owns the repos — not under a personal account. 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 your internal wiki page or https://gatekeephq.pro.
  3. Configure the webhook. Tick Active, set Webhook URL to https://<your-host>/webhook (your Gatekeep instance, reachable from GitHub over HTTPS), and set a strong Webhook secret — save it now; it becomes GITHUB_WEBHOOK_SECRET in step 10 of the quickstart. 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 ↗
Required GitHub App permissions
PermissionAccessWhy
Pull requestsRead & WriteRead PR metadata/diff; post the review + inline comments.
ContentsReadFetch changed file contents for bounded context.
ChecksRead & WriteCreate/update the mergegate/review Check Run — the gate.
IssuesRead & WriteReceive/act on the /mergegate override: comment (PR comments are issue comments in the API).
MetadataRead (mandatory)Baseline repo access.
MembersRead (optional)Only if you later use GitHub teams in the override allowlist. Skip it for usernames.
  1. Subscribe to events: Pull request and Issue comment (the /mergegate override: command arrives as an issue comment).
  2. Restrict installation to Only on this account, then click Create GitHub App.
  3. Copy the App ID shown on the app's settings page — it goes into github.app_id in config.yaml.
  4. Generate the private key. On the same settings page, scroll to Private keys → Generate a private key; a .pem file downloads. Move it to ./secrets/app.pem next to your compose file, 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. GitHub docs: installing your own GitHub App ↗

Verify it worked: open a test PR in a gated repo — a Gatekeep review should appear within about a minute, along with the mergegate/review check. If nothing arrives, open the app's Advanced → Recent Deliveries tab to see every webhook delivery and response code. GitHub docs: viewing webhook deliveries ↗

Wire the credentials into config

Three pieces connect the App to your instance:

  • App IDgithub.app_id in config.yaml.
  • Private key PEM → mount the file at github.private_key_path (default /run/secrets/mergegate_app.pem). The compose file mounts ./secrets/app.pem read-only.
  • Webhook secret → set the env var named by github.webhook_secret_env (default GITHUB_WEBHOOK_SECRET). Every webhook is HMAC-verified against it; a bad signature returns 401.

At runtime, Gatekeep signs a short-lived App JWT with the private key, exchanges it for a 1-hour installation token scoped to your repos, and uses that for all API calls. Tokens are cached in memory per installation and refreshed on expiry. No global credential is ever used for repo data.

GitHub Enterprise Server

Running GHES rather than github.com? Override the API base URL with github.api_base_url (default https://api.github.com). Point it at your GHES API base and the App auth, diff fetch, review posting, and Check Run creation all route to your server. (Beyond this configurable base URL, deeper GHES-specific hardening is a fast-follow. Roadmap)

GitLab / Bitbucket Roadmap (Phase 2)

Gatekeep supports GitHub only today (cloud and GHES via the configurable API base URL). GitLab is a Phase 2 roadmap item; Bitbucket and Azure DevOps are not planned for the MVP. If GitLab is a dealbreaker for your pilot, tell us so we can factor it into sequencing — but do not assume it exists yet.

4 · Bring your own LLM

Gatekeep speaks the OpenAI Chat Completions contract, so it works unchanged against any OpenAI-compatible endpoint — no provider-specific SDKs, no bundled model. Point it at what you already trust:

  • vLLM — a local open-weight model (e.g. Qwen2.5-Coder, Llama-3-70B) on your own GPUs.
  • Ollama — use its OpenAI-compatible /v1 path.
  • Azure OpenAI — via base_url plus extra_headers for the api-key header.
  • AWS Bedrock — through a LiteLLM proxy / access gateway that exposes the OpenAI contract.

Configuration keys

LLM configuration keys
KeyPurpose
base_urlThe OpenAI-compatible endpoint (e.g. http://vllm.internal:8000/v1).
api_key_envName of the env var holding the API key. Leave the value blank for keyless gateways.
modelModel identifier your endpoint serves (e.g. qwen2.5-coder-32b-instruct).
temperatureSampling temperature. Default 0.1 for stable, review-grade output.
max_tokensMax completion tokens. Default 2000.
timeout_sPer-request timeout in seconds. Default 90. A timeout is a reviewer error → fail-safe red gate.
extra_headersOptional custom headers, e.g. an api-key header for Azure or a gateway auth token.
config.yaml — llm blockyaml
llm:
  base_url: http://vllm.internal:8000/v1
  api_key_env: LLM_API_KEY          # optional; blank for keyless gateways
  model: qwen2.5-coder-32b-instruct
  temperature: 0.1
  max_tokens: 2000
  timeout_s: 90
  # extra_headers:                  # e.g. for Azure/gateway auth
  #   api-key: ${LLM_API_KEY}

The endpoint is validated at startup and via the GET /healthz/llm probe (a cheap ping), so a misconfigured model surfaces immediately rather than on the first PR.

Air-gap posture

Because Gatekeep only ever calls the endpoint you configure, the only egress is to your gateway. Point base_url at a model on your LAN (local vLLM/Ollama) and the whole system functions with no public internet egress at all — a real air-gap, and a supported configuration. Your source code and inference never leave your trust boundary.

5 · Review policy (customize the AI review)

This is the differentiator. Gatekeep reviews against your standards, not a generic checklist. Drop a natural-language policy.md next to your config and its contents are injected into the review prompt as a clearly labeled standards section. It is prompt text, not a rulebook the tool parses — keep it concise and concrete. No code to write, no rules engine to learn.

The shipped policy.md.example

policy.mdmarkdown
# Engineering Review Policy (example)

This file is optional. If present, its contents are injected into the review
prompt so the model reviews against *your* standards. Keep it concise and
concrete — it is prompt text, not a rulebook the tool parses.

## Security
- No secrets, tokens, or credentials in source. Use the secret manager.
- All SQL must use parameterized queries. Never build SQL by string formatting.
- No `eval`/`exec` on request-derived input. No `shell=True` with interpolation.
- TLS verification must never be disabled (`verify=False` is prohibited).

## Correctness & reliability
- Public functions handle error paths; do not swallow exceptions silently.
- Money is handled in integer minor units (cents), never floats.
- New external calls must set explicit timeouts.

## Change hygiene
- No commented-out code or leftover debug prints in changed lines.
- TODO/FIXME left in changed code is a low-severity finding, not a blocker.

## Severity guidance for this team
- Treat auth, payments, and PII-handling code paths one severity level higher.

What it can and can't change

It can: steer what the model looks for and how it rates severity — the standards it enforces, the practices it flags, and per-team severity guidance (e.g. "treat auth and payments one level higher"). Because severity is what the gate keys on, tuning your policy directly shapes what blocks a merge.

It can't (in the MVP): replace the core prompt template or the strict-JSON finding schema. That template is fixed on purpose — a stable, auditable prompt shape is what keeps the gate deterministic and its findings machine-parseable. Your policy.md is layered into that fixed template alongside any ticket context; it does not rewrite it.

Enable it by pointing review.policy_file at the mounted file (default /config/policy.md). Full template override is a roadmap item for partners who need it. Roadmap

6 · Merge gate & overrides

The gate is enforced by GitHub's own branch protection, driven by Gatekeep's Check Run. This is the difference between an advisory bot and a real gate.

Make the gate blocking (one-time, per repo)

In each repo's Settings → Branches → Branch protection, require the mergegate/review status check to pass before merging (and typically "require branches to be up to date"). GitHub then programmatically disables the merge button while the check is red. There is no in-product checkbox to bypass it — the only sanctioned path past a red gate is a recorded override.

Thresholds & scope

Gate configuration keys
KeyDefaultEffect
review.gate_thresholdhighThe maximum finding severity that fails the gate. Any finding at or above this turns the check red.
review.comment_thresholdmediumFindings below this are suppressed from the PR timeline but still recorded in the audit log. Raise it to cut noise.
review.scope.branches[main, master, "release/*"]Only gate PRs targeting these branches (glob patterns allowed).
review.neutral_on_errorfalseFail-safe. With false, a reviewer error/timeout produces a failing gate, never a false green. Set true only if you deliberately want reviewer failures to not hard-block.
governance.override_allowlist[]GitHub usernames allowed to override. (Teams are post-MVP — see note.)
governance.min_justification_len40Minimum characters for an override justification. Empty/whitespace is rejected.

The override flow

When the gate is red, an authorized reviewer overrides it with a PR comment:

PR commentcommand
/mergegate override: False positive — value is a test fixture, confirmed with security. Ticket SEC-1290.

On that comment, Gatekeep:

  1. Authorizes the commenter against override_allowlist. Not allowlisted → a rejection comment is posted, an override_denied record is written, and the gate stays red.
  2. Validates the justification against min_justification_len. Too short or empty → rejection comment, override_denied record, gate stays red.
  3. Grants on success: it writes an override_granted record (actor, PR, head SHA, timestamp, full justification) and updates the Check Run to success, with the title "Overridden by @actor — justification recorded" and the justification summarized in the check output.

GitHub re-enables merge because the required check is now green. The override is bound to that head SHA — a subsequent push runs a fresh review and re-gates, so an override never blanket-approves future commits.

Allowlist uses usernames today Roadmap

In the MVP, override_allowlist holds GitHub usernames. Team-slug authorization (e.g. @acme/eng-leads) needs the optional Members: read permission and a membership lookup — that is a roadmap item. Use usernames for now.

7 · Ticket context

Gatekeep can review a diff against the intent it is supposed to implement, not just for generic quality. Paste the PRD / Jira-ticket text into the PR description and Gatekeep extracts it and injects it into the review prompt as a clearly labeled Product/ticket context section — right next to your policy.md standards. There is no Jira integration to set up; it is just prompt text, and your code still never leaves your boundary.

How to supply it

The primary convention is a fenced marker block in the PR body (the marker is gatekeep:context by default, configurable via review.ticket_context_marker):

PR descriptionmarkdown
```gatekeep:context
JIRA FIN-1290 — charge endpoint must load credentials from the secrets
manager, verify TLS on all outbound calls, and use parameterized queries.
```

A ## Ticket heading (captured until the next heading) or an inline Ticket: label line (captured until the next blank line) also work. If both a fenced block and a heading are present, the fenced marker wins.

Controls

  • review.ticket_context_enabled — default true; toggles the whole feature.
  • review.ticket_context_marker — default gatekeep:context; the recognized fenced-block marker.
  • review.max_ticket_context_chars — default 4000; caps the extracted text so it can't blow the prompt budget.

8 · Audit log & export

Every governance-relevant event is written to an append-only store: PR reviewed, findings and severities, gate decision (pass/fail + threshold used), override requested, override granted or denied. The MVP store is a SQLite table with an application-enforced append-only contract (no UPDATE/DELETE paths in code), mirrored to a JSONL file for easy export or streaming to a SIEM.

Each record carries a monotonic sequence id, timestamp, actor, repo, PR number, head SHA, event type, and payload — plus a hash chained to the prior record's prev_hash. That chain makes any deletion or edit detectable on export.

Audit export — hash-chained JSONL chain valid
  • review PR reviewed — 2 findings

    acme/payments · PR #481 · head 9f3a…

  • gate_decision BLOCKED — max severity high ≥ threshold high

    threshold high · tripping: auth.py:88 hardcoded-secret

  • override_granted by @alice — gate flipped green

    "False positive: value is a test fixture, confirmed with security. Ticket SEC-1290."

    hash e4d5… · prev b1c2…

Exporting

Fetch the full log as hash-chained JSONL from the authenticated export endpoint. The bearer token is the value of the env var named by audit.export_token_env (default AUDIT_EXPORT_TOKEN):

shellbash
curl -H "Authorization: Bearer $AUDIT_EXPORT_TOKEN" localhost:8080/audit/export

The response body is JSONL; the X-Audit-Chain-Valid header confirms the chain verifies end-to-end. This is the artifact you hand your auditor: a complete, tamper-evident record of who overrode which gate, when, and why — SOC 2 / ISO 27001 / HIPAA change-management evidence generated as a byproduct of shipping. (Full cryptographic signing and WORM storage are a Phase 2 compliance-pack upgrade. Roadmap)

HTTP endpoints

HTTP endpoints
MethodPathPurpose
GET/healthzLiveness.
GET/healthz/llmPings the configured LLM endpoint.
POST/webhookGitHub webhook receiver (HMAC-verified; 401 on bad signature).
GET/audit/exportHash-chained JSONL audit log (bearer-auth if a token is set).

9 · Deployment reference

Volumes & secrets

The compose file mounts exactly what the container needs and nothing more:

  • ./config.yaml → /config/config.yaml:ro — the single source of truth, read-only.
  • ./policy.md → /config/policy.md:ro — optional standards, read-only.
  • ./secrets/app.pem → /run/secrets/mergegate_app.pem:ro — your GitHub App private key, read-only, never baked into the image.
  • mergegate_data → /data — the durable audit store (SQLite + JSONL).

Secrets are provided via env / .env and never committed to config.yaml: GITHUB_WEBHOOK_SECRET, LLM_API_KEY (blank for keyless gateways), and AUDIT_EXPORT_TOKEN. The container runs as a non-root system user (uid 10001) and ships a /healthz healthcheck. Postgres is an optional swap-in via docker compose --profile postgres up — same models, no rewrite — for teams that outgrow SQLite.

Resource expectations

The app is deliberately small: target ≤ 1 vCPU / 1 GB RAM idle for the Gatekeep container itself. The heavy resource — the LLM — is your own gateway's concern. Review latency depends on your model; the design target is ≤ 2 minutes for a typical sub-500-line diff, tunable via timeout_s, max_tokens, and max_diff_bytes.

Air-gap installation

Because the image is delivered as a file today (not pulled from a registry), the offline install is standard Docker save/load: export the image to a tarball on a connected machine, transfer it into the air-gapped environment by whatever media your policy allows, load it, and bring up compose.

shell — offline installbash
# On a connected machine (image provided during onboarding):
docker save gatekeep-image -o gatekeep.tar

# Transfer gatekeep.tar into the air-gapped network, then on the target host:
docker load -i gatekeep.tar
docker compose up -d          # config.yaml points llm.base_url at your LAN model

With llm.base_url pointed at a model on your LAN, the whole system runs with no public internet egress.

What "zero telemetry" means, concretely

There is no phone-home: no analytics, no crash reporting to us, no license check that calls out. It is an architectural invariant, not a setting. The only outbound connections the app ever makes are to (a) your configured GitHub API base URL and (b) your configured LLM base_url. You do not have to take our word for it — verify it yourself: run the container with an egress firewall that allows only those two hosts and exercise the full flow (review, gate, override, audit export). Everything works with zero blocked outbound attempts to any Gatekeep-owned host, because there are none. (This egress-restricted acceptance test is a release sign-off item; today it is run by hand, not yet automated in CI. Roadmap)

Operations: upgrades, backups, model sizing

Upgrades. An upgrade is: load/pull the new image → stop the app container → run alembic upgrade head (migrations ship in the image; the Alembic baseline is included today) → start the new container. Migrations are not run automatically on boot, so a half-rolled deploy can't mutate your schema. The same migration set covers both SQLite and the Postgres profile. Pin image versions; treat the audit database as the asset you are protecting during any upgrade.

Backup & restore. All state lives in one database (reviews, gate decisions, overrides, the hash-chained audit log). On the default SQLite setup that is a single file on the data volume — snapshot it on your normal schedule. On the Postgres profile, use standard pg_dump/restore. After any restore, hit /audit/export and check the X-Audit-Chain-Valid header — the hash chain doubles as a restore-integrity check: if the chain verifies, your audit evidence survived intact.

Model sizing (guidance, not benchmarks). If your gateway fronts a hosted API (Anthropic, OpenAI, Gemini, Bedrock, Azure), there is nothing to size — pick a current general or code model and tune timeout_s/max_tokens. For fully local inference, review quality tracks model capability: a code-tuned model in the ~30B+ class (e.g. Qwen2.5-Coder-32B under vLLM on a single 80 GB GPU, or split across two 48 GB cards) is our working recommendation; 7–8B models run fine operationally but produce noticeably shallower findings. We have not published formal benchmarks yet — pilot feedback is how we're calibrating this guidance. Benchmarks: roadmap

Licensing. During early access, the container image and source access are provided under design-partner terms with your subscription (see pricing). If your procurement or platform team needs specific terms reviewed before a pilot, ask — that conversation is part of onboarding.

10 · FAQ & troubleshooting

The mergegate/review check never appears or stays pending

Almost always webhook delivery. Check the GitHub App's Advanced → Recent Deliveries: the Webhook URL must be https://<your-host>/webhook and reachable from GitHub over HTTPS. A 401 means the delivered signature didn't match — your GITHUB_WEBHOOK_SECRET doesn't equal the secret set on the App. Confirm the App is subscribed to Pull request and Issue comment events and installed on the repo.

Reviews are slow or the gate goes red with a timeout

Your LLM is the bottleneck. Raise llm.timeout_s, lower llm.max_tokens, or reduce review.max_diff_bytes to shrink the prompt. Use a capable model — very small local models struggle with review quality and latency. Remember the fail-safe: with neutral_on_error: false (default) a timeout yields a red gate, never a false green. Verify the endpoint with curl localhost:8080/healthz/llm.

The PR timeline is too noisy

Raise review.comment_threshold (e.g. to high). Findings below the threshold are suppressed from the timeline but still recorded in the audit log, so you lose nothing for compliance — only the chatter.

Developers say the gate blocks too aggressively

Tune review.gate_threshold (default high — raise to critical to block only the worst, or lower to catch more). If overrides are frequently "false positive," that's a signal to tune your policy.md or threshold. The override flow exists by design for genuine exceptions.

An override isn't lifting the gate

Three checks: (1) the commenter must be in governance.override_allowlistusernames only in the MVP, not team slugs; (2) the justification must be at least min_justification_len characters (empty/whitespace is rejected); (3) the exact command is /mergegate override: <justification>. Denied attempts are logged as override_denied and the gate stays red.

We run GitHub Enterprise Server

Set github.api_base_url to your GHES API base (default is https://api.github.com). App auth, diff fetch, review posting, and Check Run creation then route to your server. Deeper GHES-specific hardening is a roadmap item, but the configurable base URL is the supported path today.

/healthz/llm is failing

Your llm.base_url is wrong or unreachable from the container, the model name doesn't exist on that endpoint, or auth is missing. For gateways that need a key, set the env var named by api_key_env; for Azure and some gateways, add the auth header under extra_headers (e.g. api-key). Keyless local gateways should leave LLM_API_KEY blank.

How do I prove nothing is phoning home?

Run the container with egress restricted to only your GitHub API host and your LLM base_url, then exercise a full PR review, gate, override, and /audit/export. All succeed with zero blocked outbound to any Gatekeep host. Independently, confirm the audit export returns X-Audit-Chain-Valid to verify the log wasn't tampered with.

Ready to run it inside your network?

Gatekeep is onboarding design partners now, with a hands-on install and direct access to the founding team.

Join the design-partner program