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.
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.
Step by step, what happens when a pull request moves:
- A pull request is opened, updated (
synchronize), or reopened against a branch in scope. - GitHub sends a signed webhook (
X-Hub-Signature-256HMAC) to your instance atPOST /webhook. Bad signatures are rejected with401. - The receiver acknowledges fast and enqueues the event on an in-process worker pool. Rapid pushes to the same PR are coalesced so a
synchronizestorm doesn't fan out into duplicate reviews. - The orchestrator builds a deterministic, size-bounded diff context — diff hunks plus bounded surrounding code — and injects your
policy.mdstandards and any ticket context. No LLM runs in this step. - 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. - Findings at or above
comment_thresholdare 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. - It creates or updates the
mergegate/reviewCheck Run on the head SHA: red if the highest severity is at or abovegate_threshold, green otherwise. A reviewer error or timeout fails safe to red — silence never looks like approval. - With branch protection requiring that check, GitHub programmatically disables the merge button while it is red.
- 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. - 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
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.
# 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.
# 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
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.
- 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 ↗
- Name it and set a homepage. Any unique name works
(e.g.
acme-gatekeep); the homepage URL can be your internal wiki page orhttps://gatekeephq.pro. - 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 becomesGITHUB_WEBHOOK_SECRETin step 10 of the quickstart. GitHub docs: using webhooks with GitHub Apps ↗ - Set repository permissions exactly as in the table below — least privilege, nothing more. GitHub docs: choosing permissions ↗
| Permission | Access | Why |
|---|---|---|
| Pull requests | Read & Write | Read PR metadata/diff; post the review + inline comments. |
| Contents | Read | Fetch changed file contents for bounded context. |
| Checks | Read & Write | Create/update the mergegate/review Check Run — the gate. |
| Issues | Read & Write | Receive/act on the /mergegate override: comment (PR comments are issue comments in the API). |
| Metadata | Read (mandatory) | Baseline repo access. |
| Members | Read (optional) | Only if you later use GitHub teams in the override allowlist. Skip it for usernames. |
- Subscribe to events: Pull request and
Issue comment (the
/mergegate override:command arrives as an issue comment). - Restrict installation to Only on this account, then click Create GitHub App.
- Copy the App ID shown on the app's settings page — it goes into
github.app_idinconfig.yaml. - Generate the private key. On the same settings page, scroll to
Private keys → Generate a private key; a
.pemfile downloads. Move it to./secrets/app.pemnext to your compose file, and never commit it. GitHub docs: managing private keys ↗ - 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 ID →
github.app_idinconfig.yaml. - Private key PEM → mount the file at
github.private_key_path(default/run/secrets/mergegate_app.pem). The compose file mounts./secrets/app.pemread-only. - Webhook secret → set the env var named by
github.webhook_secret_env(defaultGITHUB_WEBHOOK_SECRET). Every webhook is HMAC-verified against it; a bad signature returns401.
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
/v1path. - Azure OpenAI — via
base_urlplusextra_headersfor theapi-keyheader. - AWS Bedrock — through a LiteLLM proxy / access gateway that exposes the OpenAI contract.
Configuration keys
| Key | Purpose |
|---|---|
base_url | The OpenAI-compatible endpoint (e.g. http://vllm.internal:8000/v1). |
api_key_env | Name of the env var holding the API key. Leave the value blank for keyless gateways. |
model | Model identifier your endpoint serves (e.g. qwen2.5-coder-32b-instruct). |
temperature | Sampling temperature. Default 0.1 for stable, review-grade output. |
max_tokens | Max completion tokens. Default 2000. |
timeout_s | Per-request timeout in seconds. Default 90. A timeout is a reviewer error → fail-safe red gate. |
extra_headers | Optional custom headers, e.g. an api-key header for Azure or a gateway auth token. |
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
# 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
| Key | Default | Effect |
|---|---|---|
review.gate_threshold | high | The maximum finding severity that fails the gate. Any finding at or above this turns the check red. |
review.comment_threshold | medium | Findings 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_error | false | Fail-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_len | 40 | Minimum 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:
/mergegate override: False positive — value is a test fixture, confirmed with security. Ticket SEC-1290.
On that comment, Gatekeep:
- Authorizes the commenter against
override_allowlist. Not allowlisted → a rejection comment is posted, anoverride_deniedrecord is written, and the gate stays red. - Validates the justification against
min_justification_len. Too short or empty → rejection comment,override_deniedrecord, gate stays red. - Grants on success: it writes an
override_grantedrecord (actor, PR, head SHA, timestamp, full justification) and updates the Check Run tosuccess, 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):
```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— defaulttrue; toggles the whole feature.review.ticket_context_marker— defaultgatekeep:context; the recognized fenced-block marker.review.max_ticket_context_chars— default4000; 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.
-
review PR reviewed — 2 findings
-
gate_decision BLOCKED — max severity high ≥ threshold high
-
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):
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
| Method | Path | Purpose |
|---|---|---|
| GET | /healthz | Liveness. |
| GET | /healthz/llm | Pings the configured LLM endpoint. |
| POST | /webhook | GitHub webhook receiver (HMAC-verified; 401 on bad signature). |
| GET | /audit/export | Hash-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.
# 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_allowlist — usernames 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.