Hermit Emits Reviews for Merge-requests In Total-isolation
H.E.R.M.I.T is a configurable code-review bot designed to work with on-premise GitLab and GitHub instances. It reviews pull requests (PR) and merge requests (MR) by running the opencode agent against a dedicated, self-hosted vLLM model endpoint.
It is built to run fully airgapped: no external SaaS, no cloud services, no model inference outside your network.
- Webhook-driven — listens on GitLab/GitHub webhooks and automatically publishes a review on every PR/MR the webhook is configured for.
- On-demand reviews — commenting
@hermiton a PR/MR also triggers a review. - On-premise friendly — works with self-hosted GitLab and GitHub instances, including fork pull requests and cross-project GitLab merge requests.
- Horizontally scalable — durable job state + deterministic job ids let you run multiple masters and restart them freely.
- Powered by opencode — review logic is driven by the opencode agent.
- Self-hosted inference — reviews are generated by a dedicated vLLM endpoint serving your own model.
- Airgapped — fully self-contained; no data leaves your network.
- Never merges or commits — its only write action is posting a review comment.
H.E.R.M.I.T has two roles running the same container image:
| Role | Entrypoint | Deployment | Responsibility |
|---|---|---|---|
| master | python -m hermit.main |
long-running Deployment | receive webhooks, validate them, spawn one reviewer pod per change, collect the review, submit it to the PR/MR |
| slave (reviewer pod) | python -m hermit.slave |
one-shot Pod, one per review | clone the repository, compute the diff, run opencode against vLLM, report the review back to the master |
Data flow: webhook → master → slave pod → vLLM → report → master → review posted.
- A webhook is configured on your on-premise GitLab or GitHub instance, pointing at the master.
- When a PR/MR event (or a comment mentioning
@hermit) is received, the master validates it against the configured secret. - The master spawns a one-shot reviewer pod in Kubernetes and waits for its report.
- The pod clones the repository, diffs base against head, and reviews the diff with opencode against your vLLM endpoint.
- The pod reports the review back to the master over
/internal/report/<job_id>(authenticated with a per-job secret). - The master publishes the review on the PR/MR and cleans up the pod.
- An on-premise GitLab or GitHub instance with webhooks enabled.
- A vLLM instance serving the model you want to use for reviews.
- The opencode agent available to the pods.
- A Kubernetes cluster to run the master and reviewer pods.
The master is configured through environment variables prefixed with HERMIT_ (a .env file is also supported). Secrets are handled as SecretStr and never logged.
| Variable | Required | Description |
|---|---|---|
HERMIT_GIT_PROVIDER |
yes | github or gitlab |
HERMIT_GIT_HOST_URL |
yes | URL of your on-premise GitLab/GitHub instance. For GitHub Enterprise this must include /api/v3 (e.g. https://ghe.corp/api/v3); for self-hosted GitLab include /api/v4 (e.g. https://gitlab.corp/api/v4). Git clone operations automatically strip the API path. |
HERMIT_GITHUB_TOKEN |
for GitHub | write token used to post reviews |
HERMIT_GITLAB_TOKEN |
for GitLab | write token used to post reviews |
HERMIT_GIT_READ_TOKEN |
yes | read-only token handed to reviewer pods for cloning |
HERMIT_WEBHOOK_SECRET |
yes | secret used to validate inbound webhooks (must be at least 16 characters) |
HERMIT_JOB_ID_SIGNING_KEY |
no | key used to derive deterministic, horizontally-shared job ids (defaults to the webhook secret) |
HERMIT_VLLM_ENDPOINT |
yes | URL of the dedicated vLLM inference endpoint |
HERMIT_MODEL |
yes | name of the model served by the vLLM endpoint |
HERMIT_VLLM_API_KEY |
no | optional API key forwarded to the vLLM endpoint |
HERMIT_REVIEW_RULES |
no | extra output instructions appended to the bot's hardcoded default review rules (never replaces the internal operating prompt) |
HERMIT_POLICY_FILE_PATH |
no | project policy file extracted from the base commit (default AGENTS.md) |
HERMIT_OPCODE_BIN |
no | path to the opencode binary (default opencode) |
HERMIT_OPCODE_ARGS |
no | whitespace-separated opencode arguments (default run) |
HERMIT_OPCODE_INIT_IMAGE |
no | init container image providing opencode binary (default ghcr.io/anomalyco/opencode:latest; empty to disable) |
HERMIT_OPCODE_INIT_BIN_PATH |
no | path to opencode binary inside init image (default /usr/local/bin/opencode) |
HERMIT_WORKSPACE |
no | working directory opencode runs in (default /workspace) |
HERMIT_MASTER_URL |
yes | URL reviewer pods use to reach /internal/report |
HERMIT_POD_IMAGE |
yes | container image for reviewer pods |
HERMIT_POD_NAMESPACE |
no | namespace for pods/secrets (default: in-cluster namespace) |
HERMIT_POD_SERVICE_ACCOUNT |
no | ServiceAccount for reviewer pods |
HERMIT_REPORT_TIMEOUT_SECONDS |
no | max wait for a review before failing the job (default 1800) |
HERMIT_OPCODE_TIMEOUT_SECONDS |
no | max runtime for the opencode subprocess inside the reviewer pod (default 900) |
HERMIT_MAX_CONCURRENT_JOBS |
no | max reviewer pods a single replica may have in flight at once (default 20) |
HERMIT_RATE_LIMIT_PER_IP |
no | max requests per source IP per window (default 60) |
HERMIT_RATE_LIMIT_GLOBAL |
no | max requests overall per window (default 600) |
HERMIT_RATE_LIMIT_WINDOW_SECONDS |
no | sliding window for rate limiting (default 60) |
HERMIT_TRUST_X_FORWARDED_FOR |
no | trust X-Forwarded-For header for client IP (default false; only enable when behind a trusted reverse proxy) |
HERMIT_MAX_BODY_BYTES |
no | max webhook/report body size (default 10 MiB; enforced at ASGI level for chunked encoding too) |
HERMIT_POD_CPU_REQUEST, HERMIT_POD_MEMORY_REQUEST |
no | reviewer pod resource requests (default 200m / 512Mi) |
HERMIT_POD_CPU_LIMIT, HERMIT_POD_MEMORY_LIMIT |
no | reviewer pod resource limits (default 1 / 2Gi) |
HERMIT_POD_SPAWNER |
no | k8s (default) or fake for local development/tests |
HERMIT_KUBE_CONFIG |
no | path to a kubeconfig (outside the cluster only) |
HERMIT_CA_BUNDLE_PATH |
no | path (inside pods) to a PEM CA bundle; when set, reviewer pods mount it and honour it for HTTPS (airgapped private PKI) |
HERMIT_POD_CA_CONFIGMAP, HERMIT_POD_CA_SECRET, HERMIT_POD_CA_MOUNT_PATH |
no | source (ConfigMap/Secret name) and mount directory for the private CA bundle |
HERMIT_HOST |
no | bind address (default 0.0.0.0) |
HERMIT_PORT |
no | HTTP port (default 8080) |
HERMIT_LOG_LEVEL |
no | Uvicorn log level (default info) |
HERMIT_VERSION is set automatically from the running code (hermit.__version__)
and exposed through /healthz and the app.kubernetes.io/version pod label; it
cannot be overridden by an environment variable, so it always reflects the code
actually running.
The master fills these from the originating change event; the pod only reads them from its own environment. Secrets come from a per-job Kubernetes Secret mounted by the master.
| Variable | Description |
|---|---|
HERMIT_JOB_ID |
job id used for the report callback |
HERMIT_GIT_PROVIDER, HERMIT_GIT_HOST_URL |
where to clone from |
HERMIT_GIT_READ_TOKEN |
read-only token (from the job Secret) |
HERMIT_REPO, HERMIT_HEAD_SHA/REF, HERMIT_BASE_SHA/REF |
what to diff |
HERMIT_SOURCE_REPO |
source (fork) project path for cross-project GitLab MRs |
HERMIT_VLLM_ENDPOINT, HERMIT_MODEL, HERMIT_REVIEW_RULES |
opencode configuration |
HERMIT_VLLM_API_KEY |
optional API key forwarded to the vLLM endpoint (from the job Secret) |
HERMIT_POLICY_FILE_PATH |
project policy file to extract from the base commit |
HERMIT_OPCODE_BIN, HERMIT_OPCODE_ARGS |
opencode invocation |
HERMIT_WORKSPACE |
working directory inside the pod |
HERMIT_MASTER_URL |
where to report the review |
HERMIT_REPORT_SECRET |
per-job report secret (from the job Secret) |
HERMIT_VERSION |
running H.E.R.M.I.T version |
- GitHub: point the webhook at
https://<master>/webhook/github. H.E.R.M.I.T validates requests using GitHub's HMAC signature (X-Hub-Signature-256) computed withHERMIT_WEBHOOK_SECRET. - GitLab: point the webhook at
https://<master>/webhook/gitlab. H.E.R.M.I.T validates requests using theX-Gitlab-Tokenheader, which must matchHERMIT_WEBHOOK_SECRET.
The docker-build pipeline job builds the image and pushes it to your GitLab Container Registry once $CI_REGISTRY_IMAGE is set, so there is no need to build it locally:
- every commit is pushed as
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA; - tagged releases (
vX.Y.Z) are also pushed as$CI_REGISTRY_IMAGE:X.Y.Z(the leadingvis stripped).
For example:
docker pull registry.gitlab.com/hermit-bot/hermit:97c2d1aSet image.repository and image.tag in the Helm values to the registry image and tag accordingly (this repo is registry.gitlab.com/hermit-bot/hermit).
The image does not bundle the opencode binary by default; reviewer pods get it from the init container.
The chart never stores tokens. Create Kubernetes Secrets for them in the same namespace (or reuse existing ones) and reference each by name and key:
| Values key | Holds | Example existing Secret |
|---|---|---|
secrets.gitReadToken |
read-only token handed to reviewer pods for cloning | git-read / token |
secrets.gitRWToken |
read/write token the master uses to post reviews | git-rw / token |
secrets.webhookSecret |
secret validating inbound webhooks | webhook / token |
secrets.vllmApiKey |
optional API key forwarded to your vLLM endpoint | vllm / token |
secrets.jobIdSigningKey |
optional key deriving horizontal job ids | report-signing / token |
kubectl create secret generic git-read --from-literal=token=read-only-token
kubectl create secret generic git-rw --from-literal=token=write-token
kubectl create secret generic webhook --from-literal=token=your-webhook-secret
kubectl create secret generic vllm --from-literal=token=your-vllm-api-key # optional
kubectl create secret generic report-signing --from-literal=token=$(openssl rand -hex 32) # optionalThe helm-package pipeline job packages the chart and pushes it to
oci://$CI_REGISTRY_IMAGE/charts (here oci://registry.gitlab.com/hermit-bot/hermit/charts). Authenticate to the registry if it is private, then install from the OCI reference (supply the chart version to pick a specific release):
helm registry login registry.gitlab.com -u <username> -p <password> # only if the registry is private
helm install hermit oci://registry.gitlab.com/hermit-bot/hermit/charts/hermit \
--version 0.1.0 \
--set image.repository=registry.gitlab.com/hermit-bot/hermit \
--set image.tag=97c2d1a \
--set config.gitProvider=gitlab \
--set config.gitHostUrl=https://gitlab.example.com \
--set config.vllmEndpoint=http://vllm.example.com:8000/v1 \
--set config.model=llama-3.1-8b-instruct \
--set secrets.gitReadToken.name=git-read \
--set secrets.gitReadToken.key=token \
--set secrets.gitRWToken.name=git-rw \
--set secrets.gitRWToken.key=token \
--set secrets.webhookSecret.name=webhook \
--set secrets.webhookSecret.key=token \
--set secrets.vllmApiKey.name=vllm \
--set secrets.vllmApiKey.key=token \
--set secrets.jobIdSigningKey.name=report-signing \
--set secrets.jobIdSigningKey.key=tokenHorizontal scaling: the master keeps its durable job state in Kubernetes (the per-job Secret), so you can raise
replicaCountand restart the deployment freely. Duplicated webhook events are collapsed onto one job via a deterministic job id, and a background sweep reclaims orphaned reviewer pods and secrets. Set the samesecrets.jobIdSigningKeyvalue on all replicas so they agree on job ids; when unset the webhook secret is used as the default.
To use the init container for providing the opencode binary (recommended for airgapped environments), the defaults work out of the box. To use a private registry mirror, override:
helm install hermit oci://registry.gitlab.com/hermit-bot/hermit/charts/hermit \
... \
--set config.opencodeInitImage=registry.example.com/opencode:latest \
--set config.opencodeInitBinPath=/usr/local/bin/opencodeTo disable the init container and bundle opencode in the reviewer pod image instead:
helm install hermit oci://registry.gitlab.com/hermit-bot/hermit/charts/hermit \
... \
--set config.opencodeInitImage=""The chart creates the master Deployment and Service, two ServiceAccounts (a
privileged one for the master with a Role that allows creating/deleting reviewer
pods and their Secrets, and an unprivileged one for reviewer pods), plus the
ConfigMap. All tokens are injected from your existing Secrets via secretKeyRef;
nothing sensitive is rendered into the manifests. Set rbac.enabled to false
if you manage RBAC yourself.
When your GitLab/GitHub instance or vLLM endpoint is signed by a private
Certificate Authority (common in fully airgapped environments), the master
and every reviewer pod must trust it. Provide a PEM bundle of your private
root CA(s) in an existing ConfigMap or Secret and reference it through the
caBundle values:
caBundle:
configMap: ca-bundle-cm # or: secret: ca-bundle-secret
configMapKey: ca.pem # key holding the PEM bundle
mountPath: /etc/hermit/caThe bundle is mounted read-only into the master and into each reviewer pod and
becomes the trust store for HTTPS (Git host API, vLLM endpoint) and for
git clone, via SSL_CERT_FILE, REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO,
CURL_CA_BUNDLE and NODE_EXTRA_CA_CERTS. Only the volume source name is
referenced — the certificate content never appears in the manifests. configMap
takes precedence when both configMap and secret are set.
kubectl create configmap ca-bundle-cm --from-file=ca.pem=./private-root-ca.pem
helm install hermit oci://registry.gitlab.com/hermit-bot/hermit/charts/hermit \
--version 0.1.0 \
--set ... \
--set caBundle.configMap=ca-bundle-cm \
--set caBundle.configMapKey=ca.pemPoint GitLab/GitHub webhooks at the master Service (http://<release-name>-hermit:8080/webhook/...), and use the same webhookSecret on both sides.
kubectl get pods— the master should beRunning.curl http://<service>/healthzreturns{"status": "ok", "version": "0.1.0"}.- Open a PR/MR or write
@hermitin a comment: a reviewer pod is spawned and a review is posted.
Requires Python 3.11+, the Kubernetes dependencies are optional for the fake spawner.
python -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/pytest # run tests
HERMIT_GIT_PROVIDER=github \
HERMIT_GIT_HOST_URL=https://git.example.com \
HERMIT_GITHUB_TOKEN=ghp_... \
HERMIT_GIT_READ_TOKEN=... \
HERMIT_WEBHOOK_SECRET=... \
HERMIT_VLLM_ENDPOINT=http://vllm.example.com:8000/v1 \
HERMIT_MODEL=llama-3.1-8b-instruct \
HERMIT_MASTER_URL=http://127.0.0.1:8080 \
HERMIT_POD_IMAGE=hermit \
HERMIT_POD_SPAWNER=fake \
.venv/bin/hermit # or: .venv/bin/python -m hermit.main- The model runs on your own vLLM infrastructure; no code or review data is sent to external services.
- Inbound webhooks are validated against the configured secret (GitHub HMAC, GitLab token).
- The bot authenticates to your Git host with two separated, scoped tokens: a write token to post reviews and a read-only token handed to reviewer pods.
- Reviewer pods report back over
/internal/report/<job_id>authenticated with a per-job secret, never with the Git host tokens. - The bot never merges or commits; its only write action is posting a review comment.
H.E.R.M.I.T is under active development. The master, reviewer pods, providers, opencode integration and deployment assets are implemented; review behavior is refined through the HERMIT_REVIEW_RULES setting.
Apache License 2.0 — see LICENSE. Copyright (c) 2026 Aymeric (aplu.fr).
This project was written with the assistance of AI-assisted tooling.

