Themis is a self-hosted GitHub PR review bot that runs on your own Codex or
Claude Max subscription. It reviews pull requests with inline findings and a structured
summary (verdict, scoring table, severity-ordered sections), answers
questions in review threads and PR conversation, can re-review pushed commits
as a scoped delta once a review exists (opt-in), and takes its review doctrine
from your own repository, under .themis/.
A GitHub App webhook delivers PR and comment events to Themis. Each event
becomes a job on an in-memory queue, processed one at a time by default
(THEMIS_CONCURRENCY runs up to 8 in parallel; the practical limit is your
engine subscription quota). The worker
shallow-clones the PR head, runs the configured engine (codex exec, or
claude -p β natively or in API mode for GLM, Kimi, and OpenRouter) against your repo's review doctrine, and posts findings and a
summary back to GitHub as the App. Issues and PRs the description references
(Fixes #12; same owner only, and only public sibling repos) are fetched
and handed to the reviewer as context. One image runs as an isolated controller
and agent; there is still no database, Redis, or message broker.
- Docker with the Compose plugin (Docker Desktop, or Docker Engine +
docker compose) - An OpenAI account with Codex access, or a Claude Max subscription (pick your engine): the matching CLI installed (
npm install -g @openai/codexornpm install -g @anthropic-ai/claude-code, Node 22+) andcodex loginorclaude setup-tokenworking on your machine. Claude Pro is not supported because Themis defaults to Opus. The glm engine needs no local CLI login: just aGLM_API_KEY(Z.ai GLM Coding Plan) in.env. The kimi and openrouter engines are the same: justKIMI_API_KEY(Moonshot platform, pay-as-you-go) orOPENROUTER_API_KEY(OpenRouter credits) in.env. - A GitHub account that can create a GitHub App (personal account or an org)
No clone or build needed: Themis ships as a prebuilt image,
ghcr.io/zaimwa9/themis.
| Path | GitHub App | Best for |
|---|---|---|
| GitHub Action | none | zero infrastructure: reviews on GitHub's runners |
| Quick start | auto-created via manifest, random name | trying Themis in minutes, throwaway deployments |
| Production setup | created by you once, stable name | long-term installs that survive redeployments |
| Headless mode | created by you once | teams with existing webhook infrastructure |
The three server paths run the same prebuilt image; all four support every engine.
No server, tunnel, or GitHub App: a workflow runs the same engine + prompt
pipeline on GitHub's runners and posts with the workflow's GITHUB_TOKEN.
Copy examples/github-actions/themis-review.yml
into .github/workflows/, add your engine credential as a repo secret
(e.g. CLAUDE_CODE_OAUTH_TOKEN), done. New PRs get reviewed;
@themis review and discussion replies work like in server mode.
Trade-offs (no per-repo learnings, no fork-PR reviews, runner minutes) and
the full input reference: docs/github-action.md.
The bootstrap uses GitHub's App Manifest flow to create the App, generate its private key and webhook secret, install it on the requested repository, and write a ready-to-run deployment. There are no GitHub settings to copy. GitHub still asks the account owner to approve App creation and repository access.
Why not long-term? Only the App name: the manifest flow generates a random one and it can't be reserved in advance. The App itself is created once β the credentials in the generated
.envstay valid across image upgrades, restarts, and machine moves, and the webhook URL re-registers itself at startup, so you never re-run the bootstrap for the same deployment. Re-running is only for a new App (fresh environment, key rotation, or a.envyou no longer have). For a stable name and@mention, use the Production setup below.
Copy the prompt below into Claude Code, Codex, or any agent that can run shell commands. It handles the entire bootstrap autonomously:
Set up Themis for this repository using the automated GitHub App Manifest bootstrap:
https://github.com/Zaimwa9/themis/blob/main/docs/bootstrap.md
Use the Claude engine and the bundled ngrok tunnel.
Do not manually create or configure a GitHub App. Run `python -m themis init` with
`--engine claude --tunnel`.
Handle everything autonomously. Only pause when GitHub requires my approval, or when
I need to provide an ngrok auth token or complete Claude authentication.
After setup, start the deployment, verify it works, and tell me the bot's @mention
and how to trigger a review.
Using the Claude engine instead? Pass --engine claude to the bootstrap:
it runs claude setup-token for you and writes the resulting long-lived
token into CLAUDE_CODE_OAUTH_TOKEN in the generated .env (set that
variable beforehand to skip the interactive flow). Using glm, kimi, or
openrouter? No CLI login needed: pass --engine <name> to the bootstrap
and put the provider key (GLM_API_KEY, KIMI_API_KEY, or
OPENROUTER_API_KEY) in the generated .env. Details in
Engines.
Install the CLI if you haven't already (Node 22+):
npm install -g @openai/codexMint a login chain dedicated to this deployment (a browser window opens):
CODEX_HOME="$PWD/themis-codex" codex loginThis writes $PWD/themis-codex/auth.json, which the bootstrap seeds into the
generated deployment with mode 0600. Do not reuse or mount your personal
~/.codex/auth.json: ChatGPT refresh tokens are single-use rotating, so two
installs sharing one chain invalidate each other β whichever refreshes first
kills the other, and reviews start failing with expired-credential errors.
Once the container owns its own chain, codex refreshes it indefinitely with
no further logins. (Running the bootstrap from a source checkout instead of
Docker? Omit --codex-auth entirely β it runs codex login against a
private scratch home for you.)
For an instance with an existing public HTTPS URL:
mkdir themis-deploy
docker run --rm -it \
--user "$(id -u):$(id -g)" \
-p 127.0.0.1:8976:8976 \
-v "$PWD/themis-deploy:/output" \
-v "$PWD/themis-codex:/host-codex:ro" \
ghcr.io/zaimwa9/themis:latest \
python -m themis init \
--repo OWNER/REPO \
--public-url https://themis.example.com \
--output /output \
--codex-auth /host-codex/auth.json \
--bind-host 0.0.0.0 \
--no-browserFor a local machine without a public URL, export an ngrok token and replace
--public-url ... with --tunnel:
export NGROK_AUTHTOKEN=<your-token>
# Add `-e NGROK_AUTHTOKEN` to docker run, then pass `--tunnel` to themis init.If the target is owned by an organization, also pass --organization OWNER.
The App must be created under that organization because the generated App is
private. The command prints a localhost URL. Open it, approve the pre-filled
App, and select OWNER/REPO on the installation screen. The private key never
leaves the bootstrap process and the generated .env. The success screen and
terminal show the bot's generated @mention; it is also saved in
themis-info.json for later reference.
The same command can run from a source checkout without Docker:
uv run python -m themis init \
--repo OWNER/REPO \
--public-url https://themis.example.com \
--output ./themis-deployDetailed options, the tunnel command, and recovery are in
docs/bootstrap.md.
cd themis-deploy
docker compose up -dUse docker compose --profile tunnel up -d when the bootstrap used --tunnel.
Themis discovers the tunnel URL and updates the App webhook automatically.
curl localhost:8000/healthz
# {"status":"ok"}Check docker compose logs themis for the startup line reporting the
resolved App slug. Open a test PR against a repo the App is installed on:
expect a π reaction on the trigger, then a π reaction on the PR once the
review starts, then the review itself.
Create the App yourself, once: it gets a stable name and @mention, and any
number of deployments can point at it without ever re-running a bootstrap.
Under Settings > Developer settings > GitHub Apps > New GitHub App, on the organization that owns the target repos or on your personal account:
| Setting | Value |
|---|---|
| GitHub App name | your choice, must be unique on GitHub; its slug becomes the bot's @mention |
| Homepage URL | any URL; https://github.com/Zaimwa9/themis works |
| Webhook URL | https://HOST/webhook; any placeholder works if you set THEMIS_PUBLIC_URL later, Themis re-registers it at startup |
| Webhook secret | a long random string, goes in THEMIS_GH_WEBHOOK_SECRET |
| Checks permission | Read-only |
| Contents permission | Read and write (write publishes learnings digest PRs) |
| Issues permission | Read and write |
| Pull requests permission | Read and write |
| Commit statuses permission | Read-only |
| Events | pull_request, issue_comment, pull_request_review_comment |
Actions permission is not required. Existing Apps must be updated with the Checks and Commit statuses permissions before Themis can include CI context in reviews, and with Contents write before it can open learnings digest PRs; each installation must then accept the permission upgrade GitHub sends it.
Then generate a private key (App settings > Private keys) and install the App on the target repositories (App settings > Install App).
Grab the Compose file and point it at the published image:
mkdir themis-deploy && cd themis-deploy
curl -fsSLO https://raw.githubusercontent.com/Zaimwa9/themis/main/docker-compose.yml
# edit: replace the two `build: .` lines with `image: ghcr.io/zaimwa9/themis:latest`Create .env next to it (.env.example documents every key):
THEMIS_GH_APP_CLIENT_ID=<App client id>
THEMIS_GH_APP_PRIVATE_KEY=<PEM, or base64 of it>
THEMIS_GH_WEBHOOK_SECRET=<the webhook secret>
THEMIS_AGENT_TOKEN=<any long random string>
THEMIS_PUBLIC_URL=https://your-host # optional: webhook self-registration
THEMIS_ENGINE=codex # or claude / glm / kimi / openrouter
CLAUDE_CODE_OAUTH_TOKEN=<token> # claude engine only
GLM_API_KEY=<key> # glm engine only
KIMI_API_KEY=<key> # kimi engine only
OPENROUTER_API_KEY=<key> # openrouter engine only
docker compose up -dFor the codex engine, mint a dedicated chain and seed the auth volume once
the agent is up β never your personal ~/.codex/auth.json (see the chain
warning above). The pipe runs as the container's unprivileged themis
user, so ownership and 0600 mode come out right (docker compose cp
would leave the file root-owned and unreadable to the agent):
scratch=$(mktemp -d)
CODEX_HOME="$scratch" codex login
docker compose exec -T agent sh -c 'umask 077; cat > /data/codex/auth.json' \
< "$scratch/auth.json"
rm -rf "$scratch"PaaS deployment, upgrades, and the full env reference:
docs/server-deploy.md and
docs/configuration.md.
Same checks as the quick start: curl localhost:8000/healthz, look for the
App slug in docker compose logs themis, open a test PR.
Already have webhook infrastructure? Set THEMIS_WEBHOOK_ENABLED=false to
remove Themis's inbound webhook route and drive it from your own handler
through two authenticated HTTP routes, POST /api/review and
POST /api/discuss. The GitHub App still has to exist and be installed,
created as in the Production setup.
Contracts and examples: docs/headless.md.
Copy the starter kit into the target repo. This needs a temporary shallow checkout of Themis (the deployment itself does not):
starter="$(mktemp -d)"
git clone --depth 1 https://github.com/Zaimwa9/themis.git "$starter"
cp -r "$starter/examples/themis" .themis.themis/review.md: the review doctrine, philosophy, severity calibration, a map of your codebase, house rules. Edit this; it is read straight from the PR branch on every review..themis/config.yaml: behavior knobs, every key optional.
Every review uses the full-dress presentation by default: a four-row numeric
/5 scorecard, collapsible walkthrough and verification/assumptions blocks,
product take, and an italic PR-specific sign-off. Empty categories carry a
short empty-state message; only explicit off configuration suppresses one.
If there is no
.themis/review.md, Themis separately falls back to a packaged default
doctrine; committing a doctrine replaces only that judgment guidance.
How the doctrine is consumed and how to write one that works:
docs/doctrine.md. This repo reviews itself with its own
.themis/review.md.
Themis also learns as you use it: correct it in a PR thread (or say
@themis remember <rule>) and, if you're a repo owner/member/collaborator,
it saves the convention and applies it to future reviews β landing it in
.themis/learnings.jsonl through a digest PR you review like any other.
See docs/learnings.md.
| Key | Default | Meaning |
|---|---|---|
engine |
instance THEMIS_ENGINE |
codex, claude, glm, kimi, or openrouter, overrides the instance's default engine for this repo |
web_access |
false |
toggles engine web tooling (WebFetch/WebSearch); glm/kimi/openrouter behave like claude here, and Claude's Bash may still egress unless the deployment enforces an external network policy β this caveat applies to all claude-harness engines |
model.name |
engine default | engine default: gpt-5.4 (codex), claude-opus-4-6[1m] (claude), glm-5.2 (glm), kimi-k3 (kimi), openrouter/auto (openrouter β any OpenRouter slug can be set; only Anthropic first-party models are guaranteed by OpenRouter's Claude Code integration) |
model.reasoning_effort |
high |
low | medium | high (codex only) |
limits.timeout_seconds |
1200 |
per agent attempt |
limits.max_attempts |
2 |
attempts before posting a failure comment |
limits.clone_depth |
50 |
shallow clone depth |
triggers.auto_review |
true |
false = mention-only, no auto-review when a PR opens or is marked ready for review |
triggers.delta_review |
false |
opt-in: true makes pushes to an already-reviewed PR trigger a scoped re-review of just the new commits, verifying open findings along the way β each non-coalesced push may then cost an engine run; see docs/configuration.md |
triggers.skip_titles |
[] |
case-insensitive wildcard patterns matched against the PR title (e.g. 'ci: *', '*[skip review]*'); a match skips the auto-review with a comment naming the rule, mentions still work β see docs/configuration.md |
learnings.enabled |
true |
false disables capturing, injecting, and digesting learnings for this repo |
learnings.digest_threshold |
10 |
pending learnings that trigger the digest PR (minimum 1) |
review.modules.<name> |
full-dress profile | always | auto | off per optional review section; all six presentation categories default to enabled and only off suppresses them; see docs/configuration.md |
review.modules.big_picture |
auto |
the Big picture: architecture/maintainability note; auto shows it only when the change provides concrete structural evidence (no empty-state filler), always pins it, off suppresses the note while structural defects still surface as findings |
agent.context |
false |
agent natively discovers CLAUDE.md/AGENTS.md, always resolved from the PR base revision so a PR can't steer its own review; see docs/configuration.md |
agent.skills |
false |
agent uses .claude/skills packages, same base-revision rule β natively on claude/glm/kimi/openrouter, via a synthesized index (skills bridge) on codex |
Can't commit .themis/config.yaml to the target repo (yet)? Set
THEMIS_DEFAULT_REPO_CONFIG on the controller to the same yaml (raw or
base64) and it applies to every repo that has no .themis/config.yaml of
its own; a committed file always wins. See
docs/configuration.md.
Talk to the bot in a PR: @<app-slug> review re-reviews on demand,
@<app-slug> review <focus> steers the review toward a given area (the
focus text is honored only from repo owners, org members, and
collaborators β anyone else gets a plain review), @<app-slug> <question>
asks a question, and replies inside a thread the bot already posted in are
answered automatically, no mention needed.
A standing finding doesn't hold the verdict hostage forever: resolve the
bot's thread on GitHub (the PR author or someone with write access can), or reply in it accepting the trade-off
(repo owners, org members, and collaborators only), and the next review moves
that finding to a βοΈ Acknowledged one-liner β naming who resolved or
accepted it β and the verdict is computed from what's still open. If the code
the finding covers changes materially later, it can come back as open.
Themis runs reviews through an agent CLI, using your Codex, Claude Max, or GLM Coding Plan subscription β or pay-per-token Moonshot/OpenRouter credentials:
| Engine | Auth | Setup |
|---|---|---|
codex (default) |
auth.json volume (CODEX_HOME) |
bootstrap mints a dedicated login chain (or seeds one you pass via --codex-auth); never share a chain between installs |
claude |
one env var | bootstrap runs claude setup-token and writes CLAUDE_CODE_OAUTH_TOKEN into .env |
glm |
one env var | set GLM_API_KEY in .env (Z.ai GLM Coding Plan key); reviews run through the claude CLI against Z.ai's Anthropic-compatible endpoint |
kimi |
one env var | set KIMI_API_KEY in .env (Moonshot pay-as-you-go platform key β not a Kimi Code subscription, whose terms exclude non-interactive use); reviews run through the claude CLI against Moonshot's Anthropic-compatible endpoint |
openrouter |
one env var | set OPENROUTER_API_KEY in .env (prepaid credits); reviews run through the claude CLI against OpenRouter's Anthropic-protocol gateway β model.name accepts any OpenRouter slug, though OpenRouter only guarantees Anthropic first-party models with Claude Code |
Pick the instance default with THEMIS_ENGINE in .env. A repo can override it
in .themis/config.yaml with engine: set to any of them; if that engine
has no credentials on the instance, Themis posts a comment saying so instead of
failing silently. The claude, glm, kimi, and openrouter paths need no
volume: key in .env, done.
| Symptom | Fix |
|---|---|
| Crashes at startup naming an env var | Set that variable; Themis fails fast on missing or invalid required config. |
Crashes at startup on a GET /app call |
Wrong THEMIS_GH_APP_CLIENT_ID or malformed THEMIS_GH_APP_PRIVATE_KEY. |
| Codex sandbox errors | Set THEMIS_CODEX_SANDBOX=danger-full-access; the container is the sandbox boundary on runtimes without Landlock. |
| PR comment says the usage limit was reached | The subscription of whichever engine ran the job has hit its usage window. Mention the bot again once it resets. |
| Auth that worked starts failing months later | Run codex login locally, then refresh the persistent agent credential using the command in Automated setup: Refreshing Codex authentication. |
| Review comment says engine credentials missing | Set CLAUDE_CODE_OAUTH_TOKEN (claude), GLM_API_KEY (glm), KIMI_API_KEY (kimi), OPENROUTER_API_KEY (openrouter), or seed the codex auth volume (codex), or change THEMIS_ENGINE / the repo's engine: key. |
| Webhook deliveries show 401 in the App's settings | THEMIS_GH_WEBHOOK_SECRET doesn't match the App's webhook secret. |
| Where are the logs | docker compose logs -f themis |
| A job queued right before a restart never ran | The in-memory queue doesn't survive restarts; mention the bot again to re-trigger. |
| Opened a PR, nothing happened | Check in order: PR is a draft (auto-review skips drafts until marked ready; an explicit review mention or /api/review call still runs, and questions are always answered); PR author is a bot account (ignored); auto_review: false in .themis/config.yaml; the GitHub App isn't installed on that repo; webhook deliveries are failing (App settings > Advanced > Recent Deliveries). |
docs/server-deploy.md: deploying to any Docker host or PaaS, upgrades.docs/local-tunnel.md: the ngrok tunnel profile in depth.docs/headless.md: bring your own webhook handler, the/api/reviewand/api/discusscontracts.docs/github-action.md: running Themis as a GitHub Action, inputs and trade-offs.docs/doctrine.md: the review doctrine, how it works and how to write a good one.docs/learnings.md: per-repo memory β how Themis learns conventions from PR discussions and lands them via digest PRs.docs/configuration.md: the full env and.themis/config.yamlreference.docs/security.md: the trust model and bot-side guardrails.docs/contributing-engines.md: adding a new engine / model provider.
Working on Themis itself needs uv and Python 3.12, no Docker:
uv sync --locked # install deps into .venv
uv run pytest # run the test suite
uv run ruff check . # lint
uv run python -m themis # run the server locally (reads THEMIS_* from the environment)CI runs the same pytest and ruff commands on every push and pull request.
MIT. See LICENSE.