drep
A local commit gate. It runs the linters your repository already configures, and sends the code you changed to an LLM for review.
|
What it does
On git commit and git push, drep checks your changes twice.
| Layer | Source | Blocks? |
|---|---|---|
| Deterministic | ruff, eslint, tsc, gofmt, go vet, clippy | Yes |
| Semantic | an LLM you point it at | No, unless you ask |
Your linters are precise, so their findings block. A model's opinion about
naming is not precise at any severity, so it informs instead. Splitting by
source rather than by severity is what makes the gate calibratable; opt the
LLM into blocking with --fail-on error when you want it.
drep is a single binary. It talks to no source-control platform, runs no server, and needs no drep account. The deterministic half needs no model and no API key at all. Semantic review can use an HTTP API or a separately installed Codex CLI authenticated through a ChatGPT subscription.
Install
# Shell installer (macOS and Linux, x86_64 and arm64)
|
# Homebrew
# crates.io
# Current main branch
The crate is drep-ai because drep was taken on crates.io. The binary is
drep.
Set up a repository
# choose from the models it actually serves
# Other presets: local, zai, minimax, kimi, openai, custom
drep init writes two things: a drep.toml naming your model, and a git hook.
The default is a pre-push hook; --hooks pre-commit or --hooks both if you
want the gate earlier, --hooks none for the config alone.
The interactive path fills in temperature and max_tokens for the model you
picked rather than for its provider, using a weekly-refreshed copy of
models.dev. It only ever removes a parameter or lowers a
required ceiling, never the reverse, so a model that refuses temperature gets
no temperature line and k3 gets its own 131,072 instead of a number chosen
for the endpoint. Offline, or for a model too new to be listed, the provider's
defaults are written and setup carries on. DREP_QUIRKS_PATH points drep at a
different cache.
It writes native git hooks rather than a pre-commit entry, and it handles
core.hooksPath: if you have a global hooks directory, a repository-local hook
would otherwise never fire, silently.
To adopt drep through pre-commit instead:
repos:
- repo: https://github.com/slb350/drep
rev: v2.3.0
hooks:
- id: drep-check-push # pre-push: what the push touches
# - id: drep-check # pre-commit: staged files
# - id: drep-lint-docs # markdown, rule-based
Commands
Exit codes, shared by check and lint-docs:
| Code | Meaning |
|---|---|
| 0 | Everything that should have run, ran, and found nothing blocking |
| 1 | Blocking findings |
| 2 | Something that should have run did not |
Exit 2 is the one that matters. An unreachable endpoint, a file too large for the model, a configured tool that is not installed: none of those are a pass, and a gate that reports them as one is worse than no gate.
What will run here
$ drep doctor
drep in /Users/you/your-repo
============================================================
Languages found:
Go: 12 file(s) Python: 48 file(s) TypeScript: 31 file(s)
Deterministic checks (these gate):
ruff: ready
gofmt: ready
go vet: ready
eslint: not configured (add one of: eslint.config.js, ...)
tsc: configured but NOT INSTALLED - these checks will not run
LLM analysis (required):
1. deepseek/deepseek-v4-pro-0813 at https://openrouter.ai/api/v1
Three rules decide what runs:
- Repository-local before PATH, so a project is checked by the version its CI runs.
- Only where the project configured it. No eslint config means no eslint opinion.
- A configured tool that is missing makes the run exit 2. A check that did not run is never reported as a pass.
Languages
| Language | Extensions | Tools |
|---|---|---|
| Python | .py |
ruff |
| JavaScript | .js .jsx .mjs .cjs |
eslint |
| TypeScript | .ts .tsx .mts .cts |
eslint, tsc |
| Go | .go |
gofmt, go vet |
| Rust | .rs |
clippy |
The LLM half reads any of them. It parses nothing, so it needs no grammar per language; it is told which language it is reading and which conventions that language's ecosystem expects.
Markdown
drep lint-docs is rule-based only: no LLM, no network, no config file. Ten
checks, and their severity answers one question, which is whether the finding
changes how the document renders.
| Severity | Checks |
|---|---|
| error | unclosed_code_fence |
| warning | empty_heading, missing_space_after_heading, link_syntax_invalid |
| info | bare_url, long_line, tab_character, trailing_whitespace |
| info | trailing_blank_lines, multiple_blank_lines |
An unclosed fence turns every line below it into code, so it alone blocks by
default. Whitespace renders identically, so it does not. --strict is the
shorthand for --fail-on info, which blocks on everything; over a real
repository that is dominated by line length, and a hook that blocks a commit
over a long line is a hook that gets deleted.
Configuration
drep.toml, written by drep init:
[[]]
= "https://openrouter.ai/api/v1"
= "deepseek/deepseek-v4-pro-0813"
= "${OPENROUTER_API_KEY}"
= 1800
HTTP is the default backend, so existing configurations remain valid without a
backend field. OpenAI API usage is the openai preset and uses per-token API
billing:
ChatGPT/Codex subscription usage is a separate backend. Install the official
Codex CLI, follow the
Codex authentication guide to run
codex login, then select the codex preset:
[[]]
= "codex"
= "gpt-5.6-sol"
= "high"
= 1800
= 1
This mode consumes the ChatGPT/Codex plan allowance, not OpenAI API credits.
drep never reads or stores the subscription tokens: Codex owns login and token
refresh. Every review is an ephemeral, non-interactive, read-only Codex run in
an empty directory with tools, apps, MCP, hooks, memories, user configuration,
and project instructions disabled. drep doctor verifies the installed CLI
and ChatGPT-managed authentication without printing account details.
drep init does not write your key into this file. Keys go to a per-machine
store (~/.config/drep/auth.toml, mode 0600, keyed by endpoint), so drep.toml
carries only the provider choice:
api_key = "${VAR}" still works and takes precedence over a stored key, which
is what CI wants — there is nobody to paste anything there. DREP_AUTH_PATH
points drep at a different store.
By default drep init also adds drep.toml to .gitignore; pass
--no-gitignore to commit it instead and share the provider choice with the
repository.
[[llm]] is an array of tables, and the order is a failover chain. Each entry
is tried in turn: a timeout, a refused connection, a 429, a 5xx or an empty
answer falls through to the next one. A 401 or 403 does not, because that is a
broken key and falling back would hide it. Set enabled = false on an entry to
park it without deleting it.
# Local model first, cloud when it is not running.
[[]]
= "http://localhost:1234/v1"
= "qwen3-30b-a3b"
[[]]
= "https://openrouter.ai/api/v1"
= "deepseek/deepseek-v4-pro-0813"
= "${OPENROUTER_API_KEY}"
Subscription coding plans
drep init has presets for three of them, so a plan you already pay for can run
the gate instead of per-token API billing:
Two of those endpoints expose the Anthropic messages API rather than chat
completions, which is what protocol selects:
[[]]
= "https://api.minimax.io/anthropic/v1"
= "MiniMax-M3"
= "${MINIMAX_API_KEY}"
= "anthropic"
protocol defaults to openai, so an existing file needs no change. doctor
tags a non-default protocol in its listing.
Check the plan's own terms before pointing a commit gate at it. They are not uniform, and some restrict a subscription to a named list of client tools or to interactive use.
Other keys
temperature (unset means the parameter is not sent at all, which is what some
models require — k3 and gpt-5.6-sol reject any value), max_tokens (unset
by default, so a reasoning model is never truncated mid-thought; a few endpoints
refuse a request without it), max_retries, max_concurrent.
Both are properties of the model rather than the endpoint, which is why the wizard resolves them per model. Editing either by hand always wins: drep reads the file as written and never revisits it.
Development
docs/technical-design.md is the architecture. CLAUDE.md carries the
invariants, each with the defect that produced it.
History
Versions up to 1.3.0 were a Python package (drep-ai on PyPI) that also ran a
webhook server, posted reviews to Gitea, GitHub and GitLab, generated
docstrings and kept a SQLite cache. 2.0 is a rewrite in Rust and keeps only the
local gate. The PyPI releases stay up; nothing new is published there.
License
MIT. See LICENSE.