Configure .lgtmaybe.yml¶
Put a .lgtmaybe.yml file at the root of your repository to set how lgtmaybe
reviews it. lgtmaybe reads the file from the directory it runs in. For both the
CLI and the GitHub Action, that is the repo root. To read a different file, pass
--config PATH (or the Action's config_path input). An explicit path must
exist.
The file holds non-secret settings only. API keys stay in the environment or your CI secrets.
Contents¶
- Full Example
- Field Reference
- provider
- model
- fallback_model
- min_severity
- include_paths / exclude_paths
- max_files
- max_input_tokens
- preset
- categories
- context_lines
- function_context
- timeout
- structured_output
- reflect
- min_confidence
- mid_review_retrieval
- incremental
- static_analysis
- triage_model
- auto_diagram
- auto_describe
- high_impact
- risk
- pr_labels
- finding_rules
- summary_template
- resolve_fixed
- extra_lenses
- lens_paths
- directory_rules
- project
- Precedence and CLI Overrides
- Upgrading from 1.14
Full Example¶
provider: openai
model: gpt-5.5
min_severity: low
include_paths:
- "src/**"
- "lib/**"
exclude_paths:
- "**/__pycache__/**"
- "**/*.min.js"
max_files: 30
max_input_tokens: 80000
categories:
- security
- correctness
- tests
Field Reference¶
This page covers the fields most teams set. Reference: Config
lists every field with its type and default, including the tuning knobs not
described here (max_tokens, reasoning_effort, temperature, language,
fail_on, max_review_seconds, max_review_tokens and others).
Every key on this page is a top-level key. Some also belong to a group
(batching, limits or request), and you may nest them under it instead.
Both forms load the same config, so existing files need no change. The
reference lists each group's keys.
provider¶
Which LLM backend, or provider, to use. One of openai, openrouter, anthropic, zai,
chatgpt, bedrock, vertex, azure, ollama, openai-compatible.
provider: anthropic
There is no built-in provider or model. lgtmaybe looks for both in its flags,
.lgtmaybe.yml and the user config (written by lgtmaybe config init). If
those leave either one unset, lgtmaybe stops before doing any work and tells you
how to configure it. In the GitHub Action, set the provider and model inputs
instead.
model¶
The model identifier for the chosen provider. The format varies by provider:
| Provider | Example model IDs |
|---|---|
| openai | gpt-5.5 |
| anthropic | claude-sonnet-4-6, claude-haiku-4-5 |
| openrouter | anthropic/claude-sonnet-4.6 |
| zai | glm-4.6, glm-4.7, glm-4.5-air (GLM / Zhipu AI; newer glm-5.x IDs pass through too) |
| chatgpt | gpt-5.5, gpt-5.3-codex (models your ChatGPT plan can use in Codex - see Review with a ChatGPT subscription) |
| bedrock | us.anthropic.claude-sonnet-4-6, us.anthropic.claude-haiku-4-5-20251001-v1:0 (prefer the cross-region inference profile; another provider's name like gpt-6-sol is invalid, Bedrock's is openai.gpt-6-sol - see Review with Bedrock) |
| vertex | gemini-3.8-flash, gemini-3.1-pro-preview |
| azure | your deployment name, e.g. my-gpt-4o-deployment, not the upstream model ID - see Review with Azure |
| ollama | qwen3.6:27b, gemma4:e4b |
| openai-compatible | the served model name, e.g. deepseek-chat or meta-llama/Llama-3.1-8B-Instruct (also set api_base - see Use a custom OpenAI-compatible endpoint) |
Write the bare ID. lgtmaybe adds the provider's litellm prefix itself, so
bedrock/us.anthropic.claude-sonnet-4-6 would end up double-prefixed.
fallback_model¶
A second model for a review call the primary model cannot finish. It is off by default and adds no calls to a healthy review.
Both models go through the same provider, api_base and credentials. An
OpenRouter review can fall back from a Google model to a Qwen model, because
OpenRouter serves both. An openai review cannot fall back to an Anthropic
account. Cross-provider failover needs a separate review run.
provider: openrouter
model: google/gemini-3.8-flash
fallback_model: qwen/qwen3.8-max
When the fallback runs depends on the failure:
| Primary model result | What lgtmaybe does |
|---|---|
| Transient provider failure, such as a connection error, capacity rate limit, or 5xx | Exhausts the primary model's bounded retries, then tries the fallback. |
| Permanent provider failure, such as an unknown model, rejected request, or exhausted quota | Skips further primary attempts and tries the fallback. A shared credential, quota or endpoint failure usually rejects both. |
Output hits the max_tokens ceiling, or the reply stops mid-JSON |
First retries on the primary with a smaller payload or lower reasoning_effort, as the token counts suggest. Tries the fallback only if that also fails. |
The fallback does not judge the primary's answer. A clean review, a weak finding or zero findings never trigger it. Unparseable output has its own bounded repair path.
The fallback gets a fresh request and retry budget. Its time and tokens add to
what the primary already spent, and its usage counts towards
max_review_tokens once the call returns. The review summary names every
lens the fallback answered. --profile prints a models: line, and
--profile-json records the model on each call (see
Profile Reference).
If the fallback also fails, lgtmaybe marks the lens incomplete. It does not walk a list of models or go back to the primary. A review with no usable results fails rather than posting a clean result. If a lens keeps needing the second model, make that model the primary.
The --fallback-model flag and the Action's fallback_model input override
the file:
lgtmaybe review --fallback-model qwen/qwen3.8-max
See Choose a Review Model for current model results.
min_severity¶
The lowest severity to report. lgtmaybe drops findings below
it. From low to high:
info, low, medium, high, critical.
min_severity: medium # drops info and low findings
Default: low, which drops only info findings. A project.stage of
prototype raises the default to medium (see project).
include_paths / exclude_paths¶
Glob patterns that limit which changed files lgtmaybe reviews. include_paths
is an allowlist. exclude_paths is a denylist applied after it, so an exclude
always wins. Both default to empty, which reviews every file.
Patterns match the full repo-relative path. A **/ prefix also matches at the
repo root, so **/*.lock covers a root-level lockfile. The built-in skip
filter for generated, vendored and binary files runs first, so an
include_paths entry can't bring back a lockfile.
include_paths:
- "src/**"
exclude_paths:
- "src/generated/**"
- "**/*.lock"
max_files¶
The maximum number of changed files to review. lgtmaybe skips files past the
cap, and the summary says Reviewed the top N of M changed files. With
triage_model set, the cap drops the lowest-risk files.
Otherwise files keep their diff order.
max_files: 30
Default: 50. A separate per-file cap, max_file_diff_lines (default 2000,
0 disables), skips any single file whose diff is longer than that. The
summary names the skipped file. This catches generated data files that the
name-based skip filter can't recognise.
max_input_tokens¶
The token budget for each model call. When the diff exceeds it, lgtmaybe splits
the review across batched calls. With recursive on (the
default), lgtmaybe walks a single file larger than the budget
hunk by hunk, instead of cutting it short.
max_input_tokens: 80000
Default: 100000. If you leave it unset, lgtmaybe lowers it to fit a model
with a smaller context window. A value you set is used as is.
preset¶
How many model calls the review spends. A preset groups the
review's lenses into calls. fast (the default) covers the ten everyday
categories in five calls, one per concern:
| Call | Covers |
|---|---|
| security | security |
| correctness | correctness, plus stated intent when the PR states one |
| code health | performance, complexity, ponytail, deprecation |
| artefacts | tests, documentation |
| architecture | architecture |
The same five run on every provider. Worker count only changes whether they
overlap. A sixth spec call runs when the repository commits a specification
that matches the PR (see categories).
full runs one focused call per category, for release branches and deep
audits.
preset: full
Default: fast. CLI: --preset fast|full (--full is shorthand); Action
input: preset. An explicit categories list overrides the grouping.
categories¶
Which review lenses to run. Narrowing the list turns off the preset grouping. Each listed category runs as its own concurrent call, and lgtmaybe merges the findings. Fewer categories means fewer calls and fewer tokens, at the cost of coverage.
Choose from security, correctness, deprecation, tests, documentation,
performance, complexity, intent, ponytail, architecture and spec.
categories:
- security
- correctness
Default: all eleven. Two of them only run when there is something to check:
intentchecks the diff against the PR's stated intent: title, description and commit names on GitHub, or yourgit logcommit names on the CLI. With no stated intent (for example, no commits beyond the base branch yet) it is skipped and costs nothing. Underfastit shares the correctness call.specchecks the diff against a committed specification (OpenSpec, GitHub Spec Kit or Kiro). With no matching spec it is skipped. Turn it off withspec_review: false,--no-spec, or the Action inputspec_review.
The PR title, description and commit names reach the provider only through the
intent lens and the change overview (auto_diagram). To keep that text from
being sent at all, drop intent from categories and set auto_diagram: false.
ponytail is the "lazy senior dev" check: the best code is the code you never
wrote. It flags code that needn't exist (YAGNI, reinventing the standard
library, code that could be far shorter). See
What gets reviewed.
architecture flags design decisions in the change that will be expensive to
reverse:
- coupling across boundaries
- dependencies pointing the wrong way
- subclasses that break their base class's contract
- misuse-prone APIs
Each finding has to open with the change it would make painful. See What gets reviewed.
context_lines¶
The most unchanged lines lgtmaybe adds around each changed hunk, so the model
sees a change in the context of its surrounding code. lgtmaybe reads these lines
from the head revision of the file. The full budget goes before the hunk, where
the enclosing signature and setup are. A quarter of it (at least one line) goes
after. lgtmaybe uses the smaller of this value and what the token budget allows, so
the padding shrinks on large PRs. Set it to 0 to review the bare diff.
context_lines: 10 # up to 10 lines before each hunk and 2 after; 0 disables
Default: 20. CLI: --context-lines.
function_context¶
Extends each hunk's leading padding up to the enclosing function or class
signature, when that signature sits above the context_lines window. lgtmaybe
finds the boundary with ast-grep (parsing only, never running code) for Python,
JavaScript, TypeScript/TSX, Go, Rust, Java and Ruby. Its reach is bounded, so a
distant definition can't swamp the diff. Other languages, and any ast-grep
failure, fall back to the fixed-line padding.
function_context: false # fixed-line padding only
Default: true.
timeout¶
The per-request timeout, in seconds, for each model call. Left unset, lgtmaybe picks a default by provider:
| Provider | Default |
|---|---|
| ollama, openai-compatible | 1800 s, multiplied by max_concurrency (calls queue on a single-slot local server) and capped at max_review_seconds, but never below 1800 s. With the defaults that is 3600 s. |
| openrouter | 1800 s (it can route to slow reasoning models) |
| other cloud providers | 600 s |
An explicit value is used as is and never scaled. Raise it for a large local model:
timeout: 3600 # an hour per call, e.g. for a big model on CPU
CLI: --timeout; Action input: timeout. See
Run locally.
structured_output¶
Asks the provider to return the findings JSON schema through its native JSON
mode (litellm response_format). This stops models, especially local ones, from
replying with prose instead of findings. Leave it on unless you hit one of two
failures:
- The gateway rejects
response_formatoutright (someopenai-compatiblegateways return a400). The review fails loudly; turn this off. - The gateway accepts JSON-schema mode but returns replies the parser reads
as empty. This fails silently:
--profileshowsfindings: 0 parsed / 0 returned, the run exits clean, and a capable model looks like one that finds nothing. We saw this withanthropic/claude-fable-5.1behind OpenRouter on lgtmaybe 2.3.0 and 2.7.0. With this off, the same diff returned 11 findings. If a model is unexpectedly silent, re-run one diff with--no-structured-outputbefore concluding it finds nothing.
Either way, the lenient parser still strips code fences and pulls JSON out of surrounding prose.
structured_output: false # gateway rejects JSON-schema mode, or a model returns nothing under it
Default: true. CLI: --no-structured-output; Action input:
structured_output. See
Use a custom OpenAI-compatible endpoint.
reflect¶
Runs the self-reflection pass. It audits the merged findings and drops the ones the model marks low-confidence, before anything is posted. It trims false positives, so leave it on for most models. Turn it off for a weaker or local model that drops valid findings during the audit.
reflect: false # keep every finding; skip the false-positive audit
Default: true. CLI: --no-reflect.
To audit a weak reviewer with a stronger model instead of turning the pass off,
set reflect_model. It uses the same provider and credentials as model.
Under the fast preset, unless you set reflect_model:
- an
anthropicSonnet or Opus review audits withclaude-haiku-4-5; - an
openaiGPT-5 review (other than a mini or nano model) audits withgpt-5-mini; - every other combination audits with the review model.
min_confidence¶
During reflection, the auditor gives each kept finding a
confidence score from 0 (certainly a false positive) to 10
(certainly real). It reaches the score by trying to disprove the finding against
the diff and the file text. lgtmaybe drops findings scored below
min_confidence.
The score appears in the CLI output, the JSON output and, as a percentage, in
the posted comment's title next to the lens that raised it:
**[HIGH · security · 80%] Title**. A kept finding with no score always passes
the threshold. Its comment leaves out the percentage, as every comment does with
reflect: false.
min_confidence: 5 # drop findings the auditor scores 0-4
Default: 0, no numeric filtering. Reflection then prunes only through its
keep/drop verdicts. CLI: --min-confidence.
mid_review_retrieval¶
Lets a review lens ask once for files it can't see.
By default every lens is told the diff is only a slice of the codebase. If a finding depends on unseen code (a guard, a base class, a helper), the lens must hedge, lower the severity, or say nothing. That keeps out confident cross-file mistakes. It also hides a real bug whose evidence is one file away.
With this on, a lens can name the files or symbols it needs alongside its findings. lgtmaybe fetches them read-only, through the same API or worktree read the reflection auditor uses, never a checkout of PR code. It redacts them and re-runs that one lens with them. lgtmaybe keeps and de-duplicates the findings from both calls, so a deferral can only add findings.
mid_review_retrieval: true # trade tokens for cross-file recall
Default: false, because it can cost up to one extra model call per batch and
lens, carrying the fetched text too. The other bounds are fixed:
- one hop: the re-run can't ask again;
- at most five files, within a quarter of
max_input_tokens; - a deferral that arrives past
max_review_secondsormax_review_tokensis skipped and reported in the summary.
Read Reduce Review Cost
first, and set max_review_tokens if the worst case matters. CLI:
--mid-review-retrieval; Action input: mid_review_retrieval.
incremental¶
Commit-scoped review on GitHub, also called incremental review. On a re-run, lgtmaybe reads a hidden watermark from its own summary comment. The watermark is the head SHA its last completed review covered. lgtmaybe then reviews only the commits pushed since, instead of the whole PR.
New findings post as inline comments. Findings on files outside the increment stay open. Only a run that re-reviewed their file can auto-resolve them.
It falls back to a full review when there is no watermark yet, after a
force-push or rebase, or if the compare call fails. A failed review never moves
the watermark, so no commit is skipped. Comment /review full on the PR to
force a full re-review.
incremental: false # every run reviews the whole PR
Default: auto. Incremental on a synchronize push (new commits on a PR that was
already reviewed); full everywhere else, including open/reopen, slash commands
and the local CLI. Action input: incremental.
static_analysis¶
Runs fast, deterministic tools over the changed files (static-analysis fusion). It is on by default. Each tool feeds the review in one of two modes:
hint(the default for every tool): the tool's findings become hints the model confirms, puts in context, or discards. This catches mechanical bugs models miss, without posting raw linter noise. lgtmaybe reports only findings the model confirms.finding: lgtmaybe posts the tool's findings directly, with no model call. This is free and gives the same result every run. You opt each tool in withtool_mode.
Every tool takes one path by default, so the model sees each hit next to the
code it is about. Lockfiles are never reviewed line by line, so a hint about
one (such as an osv-scanner advisory against uv.lock) goes to the security
lens once per review. A finding the model confirms there shows in the review
body, because a lockfile has no reviewed line to comment on.
The trade-off: in hint mode the model can discard a real hit, such as a
committed credential. Set tool_mode to make a tool post directly, with no
model call:
static_analysis:
tool_mode:
gitleaks: finding # post committed secrets directly
osv-scanner: finding # post dependency advisories directly
| Tool | Checks |
|---|---|
| ruff, bandit, mypy | Python lint, security and type checks |
| gitleaks | secrets, any language |
| zizmor | GitHub Actions workflow security (template injection, unpinned uses, broad permissions); runs only when the PR changes a workflow |
| ast-grep | your own structural rules, set with ast_grep_rules (skipped when unset) - the deterministic sibling of extra_lenses |
| osv-scanner | known vulnerabilities in your dependencies |
| semgrep | multi-language SAST |
static_analysis:
enabled: true
tools: [ruff, bandit, mypy, gitleaks, zizmor] # default: all supported tools
min_severity: low # floor on mapped tool severity (default info)
tool_min_severity: # per-tool overrides of the floor
ruff: medium # only medium+ from ruff; bandit keeps low
tool_mode: # per-tool overrides of hint vs finding
gitleaks: finding # post secrets directly, with no model call
# semgrep_rules: .semgrep.yml # replace the bundled MIT rule pack
# ast_grep_rules: .ast-grep/ # your own structural rules
Default: enabled: true, with every tool in hint mode. A tool that is not
installed is skipped, so a minimal install runs nothing. To turn it off, set
enabled: false, pass --no-static-analysis, or set the Action input
static_analysis: false.
Installing the tools. A tool that isn't installed is skipped silently.
pip install lgtmaybe[static-analysis] installs ruff, bandit, mypy, semgrep and
zizmor. ast-grep ships with lgtmaybe. gitleaks and osv-scanner are Go
binaries you install separately. The GitHub Action image includes all eight.
Sandboxing. The tools run on the already-fetched file texts in a throwaway directory. That is never a checkout, and PR code never runs. Each tool runs in a subprocess with no network, a scrubbed environment and a hard timeout. lgtmaybe treats their output as untrusted: it is redacted and wrapped before it reaches the model.
Direct findings. Tools read whole files, but only the diff is under review.
So lgtmaybe drops a finding on a line the PR didn't change, and counts it in the
summary. Otherwise a fake credential in a test fixture would post on every PR
that touches the file. Direct findings are also capped per review, most severe
first. They carry a scan:<tool> category, so finding_rules can drop or
re-grade one scanner without turning it off:
finding_rules:
- match: {category: "scan:gitleaks", path: "tests/fixtures/**"}
action: {drop: true}
osv-scanner answers a question a model can't: does a pinned dependency
have a published advisory? That depends on what was disclosed this week, past
any model's knowledge cutoff. osv-scanner reads the manifests and lockfiles the
PR changes through a scan-only channel. The file text never enters the diff, a
prompt or reflection. By default its advisories go to the security lens as
hints, or to the correctness lens when the run has no security lens. With
neither lens, the hints are dropped and the summary says how many. An advisory
the model confirms shows in the review body. With
tool_mode: {osv-scanner: finding} the advisories post directly instead, and
only high and critical ones show (unanchored_min_severity).
It needs a local vulnerability database, since the sandbox has no network. The
Action image bakes one in at build time. On the CLI, point
OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY at a directory. Then seed it once from a
project that has your manifests:
export OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY="$HOME/.cache/osv-db"
osv-scanner scan source --offline-vulnerabilities --download-offline-databases .
Keep the variable set when you run lgtmaybe. It is the one variable passed into the sandbox. Without a database the scanner reports nothing, and the review says so rather than calling it clean.
What the model stops being asked. When osv-scanner posts findings, the review prompt stops asking the model two things: whether a version has a known advisory, and whether a package is abandoned. A knowledge cutoff can't answer either. The model keeps deprecated APIs, end-of-life runtimes, typosquats and licence conflicts.
Likewise, when gitleaks posts findings, the model is no longer asked for
hardcoded secrets. lgtmaybe redacts matched secrets before the diff leaves for
the provider, so the model couldn't see them anyway. gitleaks reads the
unredacted text. Secrets written to a log are a different defect that no secret
scanner reports, so the model keeps that one. Set either tool to hint and the
model takes those checks back.
semgrep runs out of the box with a small, high-precision MIT rule pack that
ships with lgtmaybe. semgrep_rules replaces it. Rules are always read from a
local path, because semgrep's registry configs need the network. lgtmaybe
doesn't bundle the well-known semgrep-rules or opengrep-rules collections. They
are LGPL-2.1 plus a Commons Clause, which an MIT package can't redistribute. Point semgrep_rules at a fuller pack you have obtained yourself.
mypy catches unguarded Optional bugs that a review lens reads straight
past, such as a dict.get() result dereferenced without a None check. It runs
with --ignore-missing-imports --follow-imports=skip, because the sandbox holds
only the changed files. So it reports what it can prove from one file and stays
quiet about the rest.
triage_model¶
Two-stage routing, so routine PRs don't pay frontier prices while risky ones
still get the strong model. When set, this cheap model runs first over the
per-file diffs. It skips files that plainly need no review (pure formatting,
trivial renames, generated churn). It scores the rest 0-10 by risk. The strong
model then reviews only the survivors, riskiest first. The review summary
lists skipped files, and /review full reviews everything.
Some files always go to the strong model, whatever triage says:
- security-relevant paths (auth, crypto, session code, migrations, IaC (infrastructure as code), CI workflows, dependency manifests);
- patches containing security-relevant tokens;
- files with static-analysis hits;
- large hunks.
Any triage failure, such as an unparseable verdict or a provider error, reviews everything.
triage_model, model and reflect_model share one provider and set of
credentials, so pointing them all at one ollama model costs nothing. The
trade-off is cheaper, faster reviews against the risk of triage under-rating a
subtle change. The always-review rules bound that risk. For maximum recall,
leave triage off.
triage_model: claude-haiku-4-5 # cheap gatekeeper; unset = no triage
Default: unset (every file gets the full review). CLI: --triage-model; Action
input: triage_model.
auto_diagram¶
Posts the change overview as one comment, updated in place on every push. The comment holds:
- a description of the change;
- a Risk of Change section;
- High Impact Areas;
- a Mermaid flowchart of the components the PR touches;
- a Mermaid sequence diagram, when the change alters a run-time flow.
/diagram posts the same overview on demand.
This is the switch for the whole comment. With it off, lgtmaybe posts only the
review itself automatically. The overview also stands down when the PR
description or another comment already has a diagram. The review summary says
so, and /diagram still works.
auto_diagram: false
Default: true. Action input: auto_diagram. See
Generate a change overview.
auto_describe¶
Heads the change overview with a structured description. It contains:
- a suggested title;
- the change type;
- a short summary;
- a per-file walkthrough table;
- when the PR states an intent, a "does it do what it says" check.
The description comes from its own model call, one of the overview's calls. It
is separate from the review's lenses, so this intent check is not the intent
lens. It is best-effort: a failure leaves a short note in its place and never
blocks the rest of the comment.
Because it rides the overview comment, it refreshes on every push. /describe
posts a description as a separate comment whether or not this is on.
auto_describe: false
Default: true. Action input: auto_describe.
high_impact¶
Includes the High Impact Areas section in the change overview: changes a reviewer must not miss. It covers ten areas: infrastructure, security posture, availability (anything that could cause an outage), data migrations, backups and recovery, compatibility, observability, dependencies, cost and compliance.
Path patterns ground the model call as untrusted hints. They also set a floor on
its output, so a sensitive file is named even when the model says nothing about
it. Examples are a *.tf, a workflow, a migration, a backup policy or a
lockfile. A failed call falls back to that floor instead of dropping the
section. An empty result says what was checked.
high_impact: false
Default: true. Action input: high_impact. See
Generate a change overview.
risk¶
Rates each PR's risk of change as low, medium, high or critical, with
the reasons. It is deterministic and makes no model call. It combines:
- how many workspace files import each changed file (Python and TS/JS, via ast-grep);
- the High Impact path signals;
- the size of the change;
- code changed without any test changing.
The level and its top reason appear on the review summary line. The change
overview explains every reason. A hidden <!-- lgtmaybe-risk:<level> -->
marker lets a workflow read it. With pr_labels on, the PR also gets a
risk/<level> label.
core_paths names central modules that the importer count can't recognise as
central, such as a config loader or a plugin registry. A changed file matching one is at least
high.
risk:
core_paths:
- src/shared/**
- "**/settings.py"
Default: on, with no core_paths. Turn it off with risk: {enabled: false},
--no-risk, or the Action input risk: false. See
Risk of Change for how the level is decided
and how to gate on it.
pr_labels¶
Adds labels derived from the finished review, with no extra model calls:
review-effort/1toreview-effort/5: a size estimate from the changed lines.possible-security-issue: the security lens or a security scanner (gitleaks, bandit, semgrep, zizmor, osv-scanner) posted a high or critical finding.consider-splitting: the diff spans many unrelated top-level directories.risk/<level>: the risk of change verdict, whenriskis on. Only one level is applied at a time.
lgtmaybe reconciles labels on each run: a stale review-effort/2 is removed
when the score changes. It touches only its own label families. A labelling
failure never fails the review.
pr_labels: true
Default: false. Action input: pr_labels.
finding_rules¶
Declarative rules applied to findings right before posting. Rules can only drop
or re-grade findings. No user code runs. Each rule has a match (every field
given must match) and an action. Rules apply in order.
Match fields:
path: a glob; a**/prefix also matches at the repo root.category: the lens that produced the finding (security,correctness, a custom lens ID, orscan:<tool>).title_contains: a case-insensitive substring.min_severity: matches findings at or above this severity.
Actions: drop: true or set_severity: <level>.
finding_rules:
# complexity nits in tests aren't worth a comment
- match: {path: "tests/**", category: complexity}
action: {drop: true}
# documentation findings are informational for this repo
- match: {category: documentation}
action: {set_severity: info}
Default: no rules.
summary_template¶
A custom template for the review summary line. Placeholders:
{count}: findings posted{provider}{model}{version}: the lgtmaybe release that produced the review{risk}: the risk segment, empty whenriskis off{stack}: the pull request's layer in a stack, such aslayer 2 of 3 in a stack; empty outside a stack{temperature}:temperature not sentwhen the model's route dropped the configured temperature, so the review may not be reproducible; empty otherwise
A template that fails to format falls back to the built-in line.
summary_template: "🤖 {count} finding(s) · {model} · lgtmaybe {version}"
Keep {version} if you can. The same model on the same provider reviews
differently across releases, so the version is what makes a surprising review
traceable.
Default: unset, which uses the built-in
N findings · risk … · provider X · model Y · lgtmaybe Z line.
resolve_fixed¶
Resolves a review conversation once its finding is fixed. On a re-run, two conditions must both hold:
- a finding lgtmaybe raised is no longer produced;
- GitHub marks its thread outdated (the code under it changed).
lgtmaybe then replies ✅ Looks resolved. and resolves the conversation.
Because both must hold, a thread is never closed only because nearby lines
moved. Set it to false to resolve conversations by hand.
GitHub resolves threads through its GraphQL API. The default GITHUB_TOKEN
with pull-requests: write, which posting already needs, is enough. The public
lgtmaybe[bot] App cannot resolve threads, so under github_identity: lgtmaybe
they stay open (see
What the public App cannot do).
Other forges (code hosts) work differently:
- GitLab has no "outdated" signal, so there lgtmaybe resolves only threads its re-run validated as fixed.
- Azure DevOps has no "outdated" signal either. lgtmaybe compares the file at the iteration where it posted the thread against the file now. It marks the thread Resolved only when the finding is no longer reported and the flagged line changed. It never does so after a run that failed or finished only partly.
- Gitea 1.26 or later works like Azure DevOps: lgtmaybe compares the file at the comment's commit against the file now. Forgejo and older Gitea have no endpoint to resolve a comment, so there the comments stay open.
The local CLI has no conversations, so it ignores this setting too.
resolve_fixed: false # leave fixed conversations open for manual resolution
Default: true. Action input: resolve_fixed.
extra_lenses¶
Your own review lenses, run alongside the built-in categories. Each one runs as
its own focused model call, and its findings merge into the same review. A lens
needs an id and instructions. The id must be unique, must not be a
built-in category name, and must not start with scan:. A title and a worked
example (example_diff and example_finding, supplied together) are optional.
The example noticeably improves a small model's output.
extra_lenses:
- id: simplify
title: Simplify or delete
instructions: |
Flag code that should not exist at all. The best code is the code you never
wrote: prefer the standard library, an existing dependency, or one line over
a new abstraction. Call out needless wrappers, premature generality, and
"just in case" code with no caller.
example_diff: |
--- a/util.py
+++ b/util.py
@@ -4,1 +4,3 @@
def get_name(user):
+ name = user.name
+ return name
example_finding:
path: util.py
line: 5
severity: low
title: Needless local variable
body: The temporary adds nothing; return user.name directly.
suggestion: " return user.name"
Lens text reaches the model as trusted instructions. So define lenses only in
files you control (the committed .lgtmaybe.yml or repo skill files), never
from PR-author content. See Add a custom review lens
for a walkthrough.
Default: none.
lens_paths¶
Loads extra_lenses from separate skill files instead of inlining them. This
helps when sharing a lens across repos or wiring lgtmaybe into an agent harness.
Each entry is one of:
- a YAML file (one lens or a list of lenses);
- a directory of
*.yml/*.yamllens files; pack:<name>for a lens pack bundled with lgtmaybe.
Paths resolve relative to where lgtmaybe runs (your repo root). Loaded lenses
are appended to any inline extra_lenses.
lens_paths:
- .lgtmaybe/skills # a directory of one-lens-per-file skill files
- team-lenses/house-style.yml # or a single file
- pack:robustness # a bundled pack
Default: none. See Bundled lens packs.
directory_rules¶
Scopes extra review instructions and reference files to part of the repo, for
when payments/** and tests/** deserve different treatment. Each rule has:
paths: globs. A**/prefix also matches at the repo root. An empty or missing list applies the rule everywhere.instructions: free text.context_files: files read from the checked-out workspace.
A rule reaches every lens call whose batch contains a file it matches.
directory_rules:
- paths: ["payments/**", "billing/**"]
instructions: |
Money-handling code. Treat rounding, currency-conversion and retry
changes as high severity, and flag any write path that is not idempotent.
- paths: ["tests/**"]
instructions: |
Test code. Do not flag duplication, long functions, or missing
docstrings here.
- paths: ["src/**"]
instructions: Check changes against the architecture described below.
context_files: [ARCHITECTURE.md]
Context files are redacted like the diff and share an eighth of
max_input_tokens. They are capped at five files per review. A missing or
over-budget file is skipped silently. Instructions and context are trusted
config: on pull_request_target they come from the base branch, never the PR
head. See
Scope review instructions to a directory.
Default: no rules.
project¶
Tells the reviewer what kind of project this is, so a weekend prototype and a regulated payments service don't get the same review. See Calibrate reviews to your project for the walkthrough.
project:
stage: regulated # prototype | production | regulated
domain: payments # a short label, up to 80 characters
notes: | # up to 1,000 characters
PCI-scoped card flows live under payments/. Most traffic is batch.
stage changes some defaults. A value you set yourself always wins.
| Stage | Default it changes | Review guidance |
|---|---|---|
prototype |
min_severity defaults to medium instead of low |
Report correctness and security; skip hardening, observability, scalability and documentation polish |
production |
none | The standard bar |
regulated |
Any PR that isn't only docs and tests is at least medium risk, with a regulated workload reason |
Rate data handling, auditability, access control and personal-data exposure one severity step higher where the domain makes them material |
All three fields also add one short paragraph to the system prompt every lens
shares. With prompt caching, that paragraph is paid for once
per run rather than per lens.
Leave the block out and the prompt is unchanged. Like directory_rules, this is
trusted config, read from the base branch on pull_request_target.
lgtmaybe doesn't read AGENTS.md, CLAUDE.md or SOUL.md for this. Those
files are written for coding agents, and most of their text is noise to a
reviewer. The larger ones don't fit the context-file budget either. To put one in front of
the reviewer anyway, name it in a directory_rules entry's context_files.
Default: no profile.
Precedence and CLI Overrides¶
Settings are merged from four layers, highest first:
- CLI flags, or the GitHub Action's inputs.
.lgtmaybe.yml(or the file named by--config/config_path).- The user config at
~/.config/lgtmaybe/config.yml, managed withlgtmaybe config(localreviewanddiagramcommands only). - Built-in defaults.
lgtmaybe review \
--provider anthropic \
--model claude-sonnet-4-6 \
--min-severity high
Most single-value settings have a flag. Run lgtmaybe review --help-all for
the list (--help alone shows only the everyday ones). List and nested settings
are YAML-only: include_paths, exclude_paths,
categories, finding_rules, extra_lenses, lens_paths, directory_rules,
project, spec_paths, risk.core_paths and the static_analysis sub-keys.
The --static-analysis and --risk flags switch those features on or off and
keep the rest of their YAML settings.
Upgrading from 1.14¶
Remove prompt_cache from .lgtmaybe.yml and --prompt-cache /
--no-prompt-cache from scripts. lgtmaybe 2.0 and later reject them. Prompt
caching is now automatic wherever the provider supports it.