Skip to content

Add a Custom Review Lens (BYO Skills)

A lens is one focused review question for the model. lgtmaybe ships eleven built-in review lenses:

  • security, correctness, deprecation, tests, documentation, performance, complexity, intent and spec;
  • ponytail, the "lazy senior dev", write-less-code lens;
  • architecture, for design decisions that will be expensive to reverse.

A custom lens adds your own. It runs alongside the built-ins as its own focused model call, and its findings merge into the same review. Use one to enforce a house style or team convention the built-in lenses don't cover.

A custom lens can't reuse a built-in id. So a lens you named architecture before it became built-in needs a new id.

Contents

How a Lens Works

Every lens, built-in or custom, has the same shape: a focused set of instructions and one worked example. Each custom lens is sent as its own model call, so the model concentrates on a single concern. (Under the default fast preset, the built-in lenses share five calls, plus a sixth when a committed spec matches. Under full, each built-in gets its own call.) A custom lens has these fields:

Field Required What it is
id yes A unique short name. It can't be a built-in category name or start with scan:.
instructions yes What to look for, in plain language. This is the lens.
title no A human-readable heading (defaults to id).
example_diff + example_finding no (together) A worked example: a hunk and the finding the model should return for it. Without one, the lens gets a generic example. Supplying your own noticeably improves smaller models.

Findings from the lens carry its id as their category. So finding_rules can drop or re-grade them like any built-in lens.

Inline in .lgtmaybe.yml

The quickest way is to define the lens in your repo config:

provider: ollama
model: qwen3.6:27b
extra_lenses:
  - id: house-style
    title: House style
    instructions: |
      Enforce our team conventions on changed code. Flag new public functions
      that return bare dicts instead of a typed dataclass, and any logging at
      WARNING or above that doesn't include a correlation id.
    example_diff: |
      --- a/api.py
      +++ b/api.py
      @@ -10,1 +10,2 @@
       def make_user(name):
      +    return {"name": name, "active": True}
    example_finding:
      path: api.py
      line: 11
      severity: low
      title: Public function returns a bare dict
      body: House style returns a typed dataclass from public functions, not a dict.
      suggestion: "    return User(name=name, active=True)"

The Ponytail lens is built in

The "lazy senior dev / write less code" instinct from Ponytail ships as the built-in ponytail lens, so you don't need a custom lens for it. Use extra_lenses for something the built-ins don't cover, like the house-style rule above.

Custom lenses share the never-cut rule

Every lens, custom and bundled ones included, runs under one shared rule. No lens may recommend removing trust-boundary validation, data-loss error handling, security controls, or accessibility support.

Custom lenses sit outside the deterministic conflict backstop. That backstop only arbitrates lens pairs lgtmaybe declares opposed. So write instructions that don't contradict the built-ins. See When Lenses Disagree.

As Reusable Skill Files

You may want to share lenses across repos, or let an agent harness add its own. Put each lens in its own file, and point lens_paths at the file or its directory:

# .lgtmaybe.yml
provider: ollama
model: qwen3.6:27b
lens_paths:
  - .lgtmaybe/skills
# .lgtmaybe/skills/house-style.yml
id: house-style
title: House style
instructions: |
  Flag new public functions that return bare dicts instead of a typed dataclass,
  and WARNING+ logging without a correlation id.

A skill file holds one lens (a mapping) or several (a list). A directory entry loads every *.yml and *.yaml file in it. Paths resolve relative to where lgtmaybe runs, normally the repo root.

Lenses from lens_paths are appended to any inline extra_lenses. Every id must be unique across the whole set.

Bundled Lens Packs (pack:<name>)

lgtmaybe ships an opt-in library of extra lenses, called lens packs. They are distilled from the wider engineering-review ecosystem: Ousterhout, Metz, Carmack, the NASA Power of Ten / TigerStyle, Google's eng-practices, and several skill and rule collections. See ATTRIBUTION.md.

The packs are off by default for two reasons. They are more opinionated than the built-ins. And every lens you enable adds a model call per batch.

Enable a pack by name with the pack: scheme in lens_paths. This works for a pip install, which has no repo-relative path to point at:

# .lgtmaybe.yml
provider: ollama
model: qwen3.6:27b
lens_paths:
  - pack:design       # combine as many as you like
  - pack:robustness
Pack Lenses Use it when
pack:design wrong-abstraction, shallow-module, information-leakage, errors-out-of-existence, hidden-state, naming You want review of structure and taste at a finer grain than the built-in architecture lens: the right abstraction, deep modules, explicit state.
pack:robustness assertions, bounded, idempotency, migrations, portability, observability Operational safety: defensive invariants, bounded work, retry-safe side effects, safe migrations.
pack:interface api-design, type-safety, magic-values, comment-why Contracts and clarity: backward compatibility, tight types, named constants, comments that explain why.
pack:frontend accessibility, i18n The diff touches UI or user-facing copy.

Each pack is a directory of skill files inside the package. You can browse or copy any single lens from src/lgtmaybe/lenses/. An unknown pack: name fails with a list of the packs that exist.

Run It

Once custom lenses are in your config, they run on every review, CLI or GitHub Action. On the CLI, their findings appear alongside the built-ins:

lgtmaybe review --provider ollama --model qwen3.6:27b --api-base http://localhost:11434

categories controls only the built-in lenses. You can narrow it and your custom lenses still run.

Security: Lenses Are Trusted Input

A lens's instructions and example reach the model as trusted reviewer instructions. So treat a lens like code you run:

  • Define lenses only in files you control, such as your committed .lgtmaybe.yml or repo skill files.
  • Never source them from a pull request.

On pull_request_target, the Action reads config from the base branch, not the PR head. So a fork PR can't add or change a lens. The diff itself is always treated as untrusted data, separately from your lenses.

See Also