commands · flags · codes

CLI reference

The full surface of reviewgate: what you can ask for, what comes back, and what to do about every error. If you have not installed the binary yet, or not decided where model access comes from, start with CLI, hook and MCP — the path is laid out there step by step.

the help is inside the binaryCommands, flags, exit codes and error codes are all in the binary — no network and no website required. This page holds the same list plus what does not fit in a terminal: the report schema, CI recipes and the boundaries. In a terminal reviewgate help is faster.
the language of the outputThe tool itself speaks English only: the help, the argument errors and the descriptions of the MCP tools are monolingual — a terminal and an AI agent read them, and they are printed before the repository is even known. The language of the review report is a different matter: it follows language in the project's .reviewgate/config.yml, exactly as it does for the bot — and it does so whole, from the findings to the verdict line, the run cost and the license footer.
terminal
reviewgate help              # overview: commands, flags, exit codes
reviewgate help review       # one command in detail
reviewgate review --help     # the same, if you already started typing
reviewgate help --json       # machine form (reviewgate.help/v1)
Help topicWhat it covers
reviewgate help initwhat init creates, where, and why existing files are never touched
reviewgate help migratemoving a 1.x config to the 2.0 canon: what migrates and how to apply it
reviewgate help reviewthe review scope, the depth, the threshold, what is read from the working copy
reviewgate help rulesyour team standards exactly as the engine sees them
reviewgate help doctorwhat is checked and what each outcome means
reviewgate help mcpthe tools for your agent and how to connect an MCP client
reviewgate help agent-setupinstructions FOR THE AGENT: show it the output and it configures its own environment
reviewgate help serverreview without a personal model key, through the team bot
reviewgate help configthe home config: every key, the precedence, the boundaries
reviewgate help hookthe gate before git push: the event, the threshold, the answer dialects
reviewgate help errorsthe error codes and the cure for each

Commands

CommandWhat it does
reviewgate reviewRuns the review. The default command: with no arguments this is what runs. The only command that spends tokens.
reviewgate rulesYour team standards: the stack preset, the rules from config.yml, the gate threshold, the ignore list. No model call — free. It reports separately whether the policy applied at all: an unreadable config.yml is never passed off as «there are no rules».
reviewgate doctorA self-check of the workstation: git, the repository, the home config and its permissions, the schema of the environment, access to the review server or readiness of the model — with a live probe of the endpoint and no tokens spent. It checks the road the run will actually take: with --local, the local one.
reviewgate mcpAn MCP server over stdio: the agent calls the review and the rules itself. It writes nothing to the repository.
reviewgate initCreates the missing config skeletons: inside a repository — .reviewgate/config.yml at the repository root (language, a preset detected from the manifests, severity_gate: off, commented rule examples) plus the home config; outside — the home config only, fully commented out and created with permissions 600 (not enforced on Windows). Existing files are never touched, no model is called, there is no --force. --lang ru|en picks the template language; without it init asks in an interactive terminal (not with --json or --quiet) and defaults to en elsewhere.
reviewgate migrateMoves a 1.x configuration to the 2.0 canon: the repository config.yml, the personal config and .env files. A dry run by default; --write applies it and leaves a <file>.bak backup next to the original. Secrets are neither printed nor touched, and YAML comments are preserved.
reviewgate versionThe review engine version, one line.
reviewgate helpThe built-in help, in full or by topic.

A typo in a command or a flag produces an error that lists the allowed values rather than silence: an unknown flag, a switch given a value, and a flag missing its required value are all rejected, because each of those quietly changed the meaning of the command.

Every flag

FlagWhat it does
--stagedonly what is staged (git add)
--refs base..headwhat the pull request will contain: diff from the merge base
--fullgenerator plus an independent judge. The default
--fastgenerator only: cheaper, findings are not confirmed by a judge
--jsona machine report on stdout; on failure, a machine error object
--quietno progress on stderr; error messages still appear
--fail-on levelthe threshold for this run: blocker|critical|major|minor|info|off
--config patha different policy file instead of .reviewgate/config.yml
--localcompute with your own key even when a review server is configured; doctor with this flag checks the local path rather than the server
--team-llmignore the LLM schema of the home config and run with the team roles (doctor too); personal env values still apply where the team is silent
--hook-stdinhook mode: the runner event is read from stdin
--hook-format cc|exitthe hook answer dialect: cc — a structure for Claude Code, exit — the decision as an exit code
--writemigrate: apply the changes (a dry run by default); a backup is left next to the file
--lang ru|eninit: the language of the generated configs — the language: key and the template comments; without the flag init asks in an interactive terminal (not with --json or --quiet) and defaults to en elsewhere
--help, -hthe help in full or by topic; with --json, in machine form
--version, -Vthe review engine version

Switches accept --flag, --flag=true and --flag=false; flags with a value accept --flag value and --flag=value. Remember that a token without -- counts as the value of the preceding flag, so put the command first (reviewgate review --json, not reviewgate --json review).

What to review

Three scopes, mutually exclusive. Passing two at once is an error: silently picking one would review something other than what you asked for.

terminal
reviewgate review                     # uncommitted changes, new files included
reviewgate review --staged            # the index only (git add)
reviewgate review --refs main..HEAD   # what the pull request will contain
reviewgate review --refs origin/main  # head defaults to HEAD
ScopeWhat enters the review
the default everything uncommitted relative to HEAD, new files included even before they are staged — that is how this scope differs from git diff. The main mode while you work
--staged only what git add staged — a review of exactly what the commit will contain
--refs base..head the diff from the merge base — that is, «what the pull request will contain», not the difference between two tips. The form base...head means the same. Without a head it is HEAD
where the code is read from, and where the rules areThe code is taken from the revision under review — the judge sees exactly what it is judging. The policy (.reviewgate/config.yml), on the other hand, always comes from the working copy, even with --refs and --staged: an edit to the rules takes effect immediately, without committing it. If the working copy of the rules differs from the revision, the report says so plainly.

In every scope, deletions and pure renames are not reviewed — there is nothing in them to check. Binary files are skipped entirely.

If nothing is left in the scope — a clean tree, a range with no new commits, changes only in files under ignorethe model is not called at all: the report is honestly empty, the exit code is 0, and usage.calls holds zero entries. That is a normal outcome in an agent loop and in the hook, and you should not pay for it.

Depth and cost

terminal
reviewgate review            # generator plus judge (the default)
reviewgate review --fast     # one pass: cheaper and faster
ModeWhat happens
--full
the default
the generator finds, an independent judge confirms the findings against whole files and rejects the false ones. With the team schema — the same verdict the bot will give on the pull request; with a home LLM schema declared, the depth is yours and the verdict is not the bot's
--fast a single pass. Cheaper and faster, but the findings are not judged — right for the edit loop, not for deciding whether to merge

A judge does not appear out of thin air: it is set by the llm.judges panel in the repository config or by LLM_JUDGES in your own environment. If there is no judge anywhere, --full effectively runs as --fast — and the report says so with a notice, so that «a full run» is not a phrase without content. In review server mode the models are the bot administrator's business.

The threshold, the verdict and the exit codes

terminal
reviewgate review --fail-on major   # a threshold stricter than the team policy
reviewgate review --fail-on off     # fail nothing, just show
reviewgate review; echo "exit=$?"   # 0 clean · 2 threshold exceeded · 1 failure

Without --fail-on the severity_gate from the team config is used. The flag changes the threshold of this run only: it touches neither the team policy nor the bot's gate on the pull request — the bot makes its own decision, and a local flag cannot steer it.

CodeMeaning
0no findings above the threshold
2the threshold was exceeded — the gate did not pass
1 failure; with --json the error object goes to stdout

The levels from strictest to softest: blocker, critical, major, minor, info — plus off to fail nothing. What each of them means is on the configuration page.

a common surprise: always 0The product default is severity_gate: off: out of the box the bot blocks no merges. The same default applies here, so a run returns 0 whatever it finds until a threshold is set. If you are building a gate on the exit code, pass --fail-on explicitly or enable severity_gate in the team policy.

While the model is thinking

A full run takes minutes: the generator, then an independent judge, and with an ensemble an arbiter as well. So that the wait does not look like a hung process, the CLI shows which call the run is standing on and how long it has been going — ⠙ Judge claude-opus-5 · 1:47. The indicator is written to stderr and only in an interactive terminal: in a pipeline and in CI a plain line → Judge claude-opus-5: waiting for a response… is printed instead of the animation, so the log is not littered with control characters. --quiet removes both.

Machine output

With --json only the report goes to stdout; everything else — progress, warnings, logs — goes to stderr. That is why the output can be piped without filtering. --quiet removes the progress too.

terminal
# the verdict in one line
reviewgate review --json --quiet | jq -r .verdict.gate

# blocking findings only, file and line
reviewgate review --json --quiet \
  | jq -r '.findings[] | select(.severity=="blocker" or .severity=="critical")
           | "\(.file):\(.line) \(.title)"'

# what the run cost
reviewgate review --json --quiet | jq '.usage.cost, .usage.calls'

The useful fields of the reviewgate.report/v1 report:

FieldWhat is inside
schemathe contract id: reviewgate.report/v1 — how a consumer recognises the format
engineVersionthe engine version that produced the run
run.llmSourcehome | team — whose LLM schema ran; home does not predict the bot review. Absent in reports built by the review server (always team)
summarythe prose summary of the run; secrets are already masked by the pipeline
verdict.gatepass · fail · off — with the team schema, the same decision the bot will make on the pull request
verdict.failOnthe effective threshold of the run: --fail-on or severity_gate
verdict.countshow many findings of each level (muted ones excluded)
findings[]file, line, endLine, severity, title, body, ruleId
findings[].suggestedCodea ready replacement for the lines; committable: true means it applies as is
findings[].judgethe judge verdict: confirmed, or downgraded with a reason; null in --fast mode
findings[].mutedbelow min_severity: present in the list, excluded from the gate
dropped[]findings the judge rejected, with the reason — the transparency of the run
questions[]❓ non-blocking questions to the author (questions: true): file, line, text — engineering doubts requalified from the judge rejects; never affect verdict.gate
run.scope, run.modewhat was reviewed and how: uncommitted|staged|refs, full|fast
run.refsthe base and head of the comparison in refs mode; null otherwise
run.filesReviewed, run.durationMshow many files entered the run and how long it took
run.configthe path of config.yml and whether it was found, the preset, the rule count, the language of the report texts, the notices
run.partialwhy coverage is partial: ignored_files, marker_files, context_budget, empty_diffs, scope_reduced, binary_files; markedFiles lists the paths excluded by an in-file marker together with their reasons
usage.calls[]per call: the role (generator|validator|arbiter|extra:<name>), the model, the tokens; failed: true means the call happened but produced nothing, and its tokens are unknown; retries: N means the call was repeated after a stream interruption (OpenAI-compatible providers) — the interrupted attempt's tokens are not counted
usage.costthe amount and the currency, or null if no price is configured for the model
diagnosticsa ready markdown 🔬 block (the same one as in the pull request summary): the calls, the judge decisions, the budget; null means diagnostics are off
budgeta snapshot of the spending cap: barrier (none·approaching·exceeded·stop), stopped, and spend against limit per backend; null means no limits are set
licensethe plan, the organisation, and the reason if a key did not apply; donateUrl is the «support the project» link (community only, null otherwise)
compatibilityThe enumerations in the report are open: a consumer must survive an unknown severity, call role or partial-coverage reason rather than fall over on it. The schema name will only change together with a major version.

Errors and what to do about them

With --json a failure is machine-readable too: parse the code, not the message text — we refine the wording, we do not change the codes.

terminal
$ reviewgate review --json
{
  "schema": "reviewgate.error/v1",
  "code": "server_forbidden",
  "message": "no access to project group/app: a Reporter role or above is required"
}
CodeCure
invalid_argsa typo in the command or a flag — check the help
not_a_reporun it inside a git repository
git_missinginstall git and check the PATH
revision_not_foundthe revision from --refs is missing — a typo, or a shallow clone (GIT_DEPTH: 0)
config_not_foundthe file from --config was not found — check the path
config_unreadablethe home config exists but was not applied — permissions (chmod 600), the path is not a file, or the YAML is broken
home_llm_configthe home config declares an LLM schema without generators — add llm.generators to ~/.config/reviewgate/config.yml, remove the role keys, or bypass once with --team-llm
server_unreachableno network route to the review server — VPN, address, whether the bot is up
server_forbiddenno access: the GitLab token, group membership, or repository rights
server_busyevery CLI slot is taken — retry later
server_disabledthe server runs no CLI reviews at all — the administrator needs REVIEW_CONCURRENCY ≥ 2; retrying will not help
quota_exceededthe installation limit is used up — ask the bot administrator
llm_unavailablethe provider key is missing, revoked, or out of credit
llm_context_overflowthe change is larger than the model window — narrow the scope or use ignore
llm_output_truncatedthe answer was cut by max_tokens — narrow the scope
llm_max_tokens_configLLM_MAX_TOKENS is above the model ceiling — lower it
llm_refusedthe model refused to answer — retry or switch models
llm_overloadedthe provider is overloaded — retry later
llm_networknetwork or proxy to the provider — check LLM_PROXY
llm_endpointendpoint or model not found — check LLM_BASE_URL (it needs /v1) and LLM_MODEL
write_failedthe file could not be written (migrate --write, init) — a backup already exists, or no permission
internalan unforeseen failure — the error text is in the message

The first step with any puzzling failure is reviewgate doctor: it answers the question «my machine or the server» and names the source of every value. Its report goes to stderr and has no machine format — only the exit code is usable in scripts (0 means you can work), and with --quiet it prints nothing at all. Common cases on the bot side are covered in troubleshooting.

A call in usage.calls[] may carry truncatedRetry: the model's answer hit the LLM_MAX_TOKENS ceiling and the request was repeated at a lower reasoning effort. There is no error — the rescue worked — but it is worth knowing: the analysis was shallower than ordered, and the tokens on that line sum up both attempts, so the run cost noticeably more than usual. The cause and the cure are repeated as a line in run.config.notices.

The same run.config.notices carries everything the personal config (~/.config/reviewgate/config.yml) failed to apply: a key with a typo, a role list written the wrong way, an api_key_command that returned nothing. Such lines start with personal config and name the file. They also go to stderr, but the report is the reliable channel: under --quiet there is no stderr, and an agent driving the CLI reads the JSON. A green verdict with a line like this means the run was made by something other than what you configured — read it before trusting the result.

Readiness of the model is checked not by the presence of a key but by whatever the run will actually use: a local model needs no key, while an OpenAI-compatible provider requires LLM_BASE_URL — without it the request has nowhere to go. The endpoint is then probed live (the model list; no tokens are spent): that is how a revoked key, a typo in the address and a dead proxy become visible. An endpoint that does not answer this method is a warning, not a refusal — the method is optional. The marks in the report: passed, ⚠️ a warning (you can still work), blocking, not checked because its premise had already failed.

in review server modeA failure that happened on the bot side keeps its own codellm_unavailable, llm_context_overflow and the rest of the table above — so the cure is the same as for a local run: the code is computed by the same engine, only on the server. The exceptions are three session codes, and they are about the connection rather than the review: client_gone, server_timeout and server_shutdown all reach you as server_unreachable. internal is left for a code this CLI could not recognise at all.

Environment variables

VariableWhat it does
REVIEWGATE_SERVERthe review server address; empty means computing with your own key
REVIEWGATE_GITLAB_TOKENyour personal GitLab token for the review server
REVIEWGATE_CONFIGa path to the home config instead of the standard one
REVIEWGATE_LICENSEthe license key
LLM_API_KEYthe provider key; ANTHROPIC_API_KEY is accepted too
LLM_PROVIDERanthropic (the default) · yandex · ollama · openai · openrouter · vllm
LLM_MODELthe generator model
LLM_JUDGESyour default judge panel (`[backend:]model[@effort]`, comma-separated, at most 2); it applies when llm.judges is not set in the repository config
LLM_BUDGET_TOKENSthe spending cap of a run for the main provider (tokens; the 80/100/300 thresholds are fixed); per backend, LLM_BACKEND_<NAME>_BUDGET_TOKENS
LLM_BASE_URLthe endpoint of a corporate gateway or a local model
LLM_FOLDER_IDthe Yandex Cloud folder — required when the provider is yandex
LLM_PROXYa proxy to the provider (a CONNECT tunnel)

The full reference of the bot's variables is on the installation page; choosing a model and running one locally is in models and your own key. The LLM_* variables are named identically in the CLI and in the bot — a provider setup carries over between them as is. The bot's other variables (GitLab, Redis, the database) mean nothing to the CLI.

The rest of the LLM_* variables (LLM_MAX_TOKENS, LLM_GENERATORS, LLM_ARBITER, LLM_DIFF_CHAR_BUDGET and the others) are read by the engine exactly as the bot reads them — their reference is on the installation page. Note that a run does not validate the schema of the variables — only reviewgate doctor will point out a typo in a name or a value.

The home config

To avoid setting variables in every terminal session, put them in a file. The path is $REVIEWGATE_CONFIG, otherwise ~/.config/reviewgate/config.yml ($XDG_CONFIG_HOME is respected), and on Windows %APPDATA%\reviewgate\config.yml.

~/.config/reviewgate/config.yml
server:                       # needed if the team bot computes the review
  url: https://bot.your-company.com
  gitlab_token: glpat-...
  # gitlab_token_command: pass show work/gitlab

llm:                          # needed if you compute with your own key
  provider: anthropic
  api_key: sk-...
  # api_key_command: op read op://dev/anthropic/credential
  model: claude-sonnet-5
  # Your own schema for local runs. Declaring generators/judges/arbiter here
  # replaces the TEAM schema entirely: it is taken as a whole and must carry
  # generators. Such a run does not predict the bot review (--team-llm does).
  # A role without backend: runs on the provider above — name a backend from
  # llm.backends to use another vendor.
  # generators:
  #   - model: claude-haiku-4-5
  # judges:
  #   - model: claude-sonnet-5
  # max_tokens: 64000        # answer ceiling; reasoning models (DeepSeek v4) spend it on thinking too
  #                          #   (64000 stays within the Sonnet 5 ceiling)
  # effort: medium           # reasoning depth: low | medium | high | xhigh | max
  # timeout_ms: 600000       # timeout of a single request
  # json_mode: none          # DeepSeek as the main provider: it rejects json_schema, and json_object
  #                          #   returns an empty content on a real review run (a long prompt) —
  #                          #   the prompt and the parser hold the shape
  # base_url: https://llm.your-company.com/v1
  # proxy: http://proxy.your-company.com:3128
  # backends:                 # a second vendor for the ensemble
  #   deepseek:
  #     provider: openai
  #     api_key: sk-...
  #     # api_key_command: pass show work/deepseek
  #     base_url: https://api.deepseek.com/v1
  #     model: deepseek-v4-pro
  #     json_mode: none         # rejects json_schema; json_object returns empty content on long prompts
  #     max_tokens: 64000       # the reasoning eats the same ceiling as the answer

license: eyJhbGciOi...

The process environment beats the file for connection and execution values (the key, base_url, max_tokens…). That is how CI and an agent runner control the settings without editing files on the machine, and LLM_API_KEY=... reviewgate review stays a working workaround when the file is out of date. The one exception is the home LLM schema below: the role keys of this file win over everything, while the role variables (LLM_GENERATORS, LLM_JUDGES, LLM_ARBITER) keep their usual place below the repository config.

Your own LLM schema for local runs

Declaring llm.generators / llm.judges / llm.arbiter in the home config sets your own schema for local runs — a cheap check in the moment, while the merge request still gets the expensive one. The schema replaces the team roles entirely: it is taken as a whole from one source, so declaring only judges refuses the run (home_llm_config) instead of borrowing generators from the team — a mixed schema nobody designed is worse than a refusal. Such a run does not predict the bot review, and the report says so in run.config.notices and in run.llmSource (home | team). To run exactly as the bot would, pass --team-llm. The schema applies to local runs only — a run through the review server uses the team one; policy (rules, ignore, thresholds) always stays with the repository.

No secret has to be stored in the clear: instead of api_key and gitlab_token, give api_key_command / gitlab_token_command — a command from your password manager. This works for the keys of extra backends in llm.backends too: a ban on plaintext secrets covers every key, not just the main vendor's. The command's output is stored nowhere and never reaches the logs. If the file is readable by others, doctor and the run both say so: it holds secrets and needs chmod 600.

A missing file is normal; an unreadable one is not. With no file the settings come from the environment, and that is a perfectly ordinary case. But when the file exists and cannot be applied — permissions, a directory on the path, an unexpanded ~ in REVIEWGATE_CONFIG (nothing expands it inside MCP client configs), or broken YAML — the run stops with the config_unreadable code and names the cure for that particular cause. Continuing silently is not allowed: the file might have held the review server address and the token, and the work would have gone to your personal key instead of the deployment's — invisibly, right up to the bill.

One small thing that costs time: the name of an extra backend in llm.backends must be latin letters and digits without underscores, otherwise the backend is skipped with a warning. Warnings like that do not stop the run — but they no longer hide in stderr either: every one of them is a line in run.config.notices of the report.

what will never be in this fileReview policy: rules, ignore, the preset, severity_gate, min_severity. Policy belongs to the team and is versioned together with the code. Letting it into a personal file would give every developer their own, and a local run would stop predicting the bot's verdict. Invisibly, at that: you are green, the bot fails the gate on the pull request, and the bot looks like the culprit. The rule in one line: the home config sets where to go, not what counts as a problem. A rule or a threshold written here has no effect — and reviewgate doctor says so plainly instead of leaving you to guess.

The same place names the typos: an unrecognised key, and above all server: https://… written as a single value instead of a section with url:. That last one costs money: the team server is not configured that way, and the run goes off to compute on your personal key while you assumed the company's.

Which setting wins

A setting can arrive from four places: the run flags, the process environment, your home config and the repository's .reviewgate/config.yml. The rule is simple: the repository sets the policy, you set the connection and the keys, and a flag changes the current run only.

what you configurewhere it may come fromwhich wins
rules, ignore, the preset, min_severityonly .reviewgate/config.ymlthe repository — there is nowhere else
severity_gate (the verdict threshold) the repository, --fail-onthe flag — but for this run only; it does not change the team policy
the model, the judge, the effort the repository, the environment, the home config the repository; if the team pinned nothing — the environment, then your file
the key, base_url, the proxy, the license the environment, the home configthe environment, then your file. Never from the repository
the review server address and token the environment, the home config, --local--local switches the server off; otherwise the environment, then your file
the scope and the depth (--staged, --refs, --fast) flags only the run flag; in MCP the call argument overrides the process flag
when your run differs from the botIf the team pinned neither a model nor a judge and you have them set, the run uses yours, and the report says so in a line: on the pull request the bot will check the same code with its own settings, so the set of findings may differ. This is not forbidden — checking yourself more strictly is useful — but you need to know about it, otherwise «green for me, failed by the bot» looks like the bot's mistake.

Your own key, or the team server

Server mode switches on by itself as soon as an address is set — there is no separate command — and only --local switches it off. In that mode your local LLM_* and REVIEWGATE_LICENSE take no part in the run: the model, the license and the cost are the server's.

If the config (or the environment) holds a review server address, the run goes to the company bot: you need no model key and present your personal GitLab token instead. The --local flag brings the run back to your own key — outside the company network, while experimenting with a model, or when the server is busy. An address without a token is an error, not a quiet fall back to local mode: otherwise the run would slip onto your key unnoticed.

How it is built and what the administrator sees is in review server for the team.

MCP: the same review as a tool for your agent

reviewgate mcp starts an MCP server over stdio — the connection and the lines for your project instruction file (without them the agent never learns the tools exist) are shown on CLI, hook and MCP. The tool arguments:

MCP tools
review_changes
  scope: "uncommitted" | "staged" | { base, head }   # uncommitted by default
  mode:  "full" | "fast"                             # full by default
  # any other value is a tool error, not a silent fallback to the default

get_team_rules
  no arguments

Both tools go through the same run, so the review server switches on for them automatically, and the repository is decided by the working directory of the process — the MCP client must be started from inside the repository. A tool returns an error with the isError marker rather than by breaking the protocol: the text holds a reviewgate.error/v1 object with the same code the command prints, so an agent branches on the code instead of parsing the message with regexps.

Runs go one at a time — a second call waits for the first rather than sharing its budget and the provider limits with it; the short protocol methods still answer instantly. Cancelling a call stops the review: no further model calls are started (the judge, the ensemble, the arbiter) and no answer is sent for the cancelled call. An answer already in flight will be billed by the provider either way — promising a saving there would be untrue.

The flags the reviewgate mcp process itself was started with become the defaults for calls: --config changes the policy path (a missing file is an error, not a quiet fall back to the defaults) and --fail-on sets the verdict threshold. The scope and the mode arrive in the call arguments and override only those. Unrecognised arguments are rejected: an unknown mode or scope is a tool error, not a quiet fall back to the default — otherwise the agent would receive «clean» instead of the review it asked for. Arguments sent as a JSON string (which is what bridges over function-calling do) are parsed.

Recipes

Migrating a 1.x configuration to 2.0

After an upgrade from the 1.x schema, the removed keys (validate_model, extra_generators, the LLM_VALIDATE_* environment knobs and the rest) are met by both the bot and the CLI with named migration errors carrying a ready replacement line. To apply every replacement at once:

moving to the 2.0 canon
reviewgate migrate            # dry run: repository config + personal config
reviewgate migrate --write    # apply it; a <file>.bak backup stays next to the original
reviewgate migrate deploy/.env.bot --write   # an explicit file (.yml or .env)

The mapping table is in Config → migrating from the 1.x schema.

A gate before push, without an agent

An ordinary git hook: it refuses to push a branch that carries blocking findings. The recipe distinguishes the exit codes: only 2 (the gate did not pass) blocks, while 1 — a failure of the review itself — is let through with a warning, because a broken reviewer must not lock a developer inside their branch (the same principle as --hook-stdin below). The same text works in a husky hook (.husky/pre-push): husky runs hooks with set -e, which is why the exit code is captured with || code=$?. Under set -e any non-zero exit of reviewgate ends the hook before a bare case $? is reached: a blocker still stops the push (husky prints «script failed (code 2)» instead of our message), but code 1 — a failure of the review itself — stops it too, and a broken reviewer locks the developer inside their branch. It is the soft branch that gets lost there, not the block.

.git/hooks/pre-push
#!/bin/sh
# .git/hooks/pre-push — an ordinary git hook, no agent involved.
# Replace main with your own default branch.
# ONLY exit code 2 (the gate did not pass) blocks the push. Code 1 is a failure
# of the review itself (no network, no key, no model): we warn and let it through —
# a broken reviewer must not lock a developer inside their branch.
# The code is captured with `|| code=$?` on purpose: under husky (set -e) any non-zero
# exit of reviewgate ends the hook before `case` — code 2 still blocks, but code 1 (a
# failure of the review itself) blocks too, and the soft branch below is lost.
code=0
reviewgate review --refs origin/main --fail-on blocker --quiet || code=$?
case $code in
  0) ;;
  2) echo "reviewgate: blocking findings — push stopped" >&2; exit 1 ;;
  *) echo "reviewgate: the review did not run (failure) — push allowed unchecked" >&2 ;;
esac

The variant for an AI agent runner (Claude Code and compatible ones) is --hook-stdin, described on CLI, hook and MCP. Its threshold is the same as the command's: without --fail-on the hook blocks by severity_gate — it forbids nothing that the bot would let through on the pull request. There is one exception: if the team gate is off (and that is the default), the hook applies its own major and says so — a hook that never blocks is pointless.

you cannot test the hook by handreviewgate review --hook-stdin typed into a terminal reviews nothing: the event is read from stdin, and an interactive console has none — the command honestly answers {} and exits with zero. An empty event is let through as well. To test it for real, feed it something that looks like the real thing — the command is below — or simply ask your agent to push.
testing the hook
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git push"}}' | reviewgate review --hook-stdin

In GitLab CI

.gitlab-ci.yml
review:
  stage: test
  variables:
    GIT_DEPTH: 0   # a merge base cannot be computed on a shallow clone
    RANGE: "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME..$CI_COMMIT_SHA"
  script:
    - reviewgate review --refs "$RANGE" --fail-on major --json > review.json
  artifacts:
    paths: [review.json]
    when: always
do you need the CLI in CI when you have the botUsually not: the bot puts the gate on the pull request, and a second copy of the review in the pipeline simply doubles the token spend. Such a job makes sense when there is no bot yet, or when a review is needed in a pipeline without a pull request — on a protected branch, for example. Note that exit code 1 is a failure, not findings, and it will fail the job as well.

What only works on a pull request

The policy is one, but part of it leans on pull request metadata that a local diff does not have. So in a local run the following do not apply: integration_branches (the «review only into these branches» rule), skipping drafts, and incrementality by new commits. Placeholders such as {mr:author} in rules and in review_prompt have nothing to expand from locally — the metadata block never reaches the prompt. All of that stays the bot's work on the pull request; verdict differences from this are rare, but worth knowing about.

Boundaries: what the CLI does not do

Does not doWhy
never writes to GitLab neither comments nor statuses: a checkpoint before the pull request must not stand in for the independent gate. That is the bot's job
never changes your files fixes are handed over as text (suggestedCode); applying them is a decision for a human or an agent
never logs code or diffs the report and the output carry run metadata and the finding texts, not the contents of your files
never takes network or keys from the repository the endpoint and the secrets come only from the environment and your home directory: a clone of somebody else's repository must not steer your diff to somebody else's endpoint
WindowsVerified on Windows Server 2019. Run chcp 65001 in the console, otherwise non-ASCII characters in the report — the emoji markers, accented text in findings — come out garbled. It does not affect --json: stdout is always UTF-8.