Skip to content
lawbook
Esc
↑↓navigate↵open⌘Jpreview
On this page

Configuration

The lawbook.yaml format, the five rule kinds, and how files are selected.

lawbook check reads the first of lawbook.yaml, lawbook.yml, or lawbook.json it finds in the directory it checks, unless --config names a file. JSON is valid YAML, so both formats share one schema.

version: 1
ignore:
  - "**/node_modules/**"
  - "**/.git/**"
llm:
  provider: claude-code
rules:
  - id: no-console
    description: Production code logs through the logger
    files: ["src/**/*.ts"]
    forbid: 'console\.(log|debug)\('
  - id: spdx-header
    level: warn
    files: ["src/**/*.ts"]
    require: '^// SPDX-License-Identifier: '
  - id: has-readme
    exists: README.md
  - id: no-env-file
    absent: .env
  - id: errors-are-actionable
    files: ["src/**/*.ts"]
    threshold: 0.7
    standard: Every error message says what went wrong and what to do next.

Editor support

The repository publishes a JSON Schema for the config file, and lawbook init writes the comment that points an editor at it, so keys autocomplete and a typo is underlined before the run rejects it:

# yaml-language-server: $schema=https://raw.githubusercontent.com/drew-simmons/lawbook/main/lawbook.schema.json
version: 1

The comment works with the YAML language server that VS Code’s YAML extension and most other editors use. The same file ships in the package as node_modules/lawbook/lawbook.schema.json, and lawbook schema prints it.

Top level

Key Required Meaning
version yes Always 1.
extends no A config file, or a list of them, whose rules run before this file’s own. See Sharing rules.
ignore no Globs no rule looks at. Defaults to **/node_modules/** and **/.git/**; setting it replaces the defaults.
gitignore no Whether files .gitignore covers are left out when the checked directory is inside a git work tree. Defaults to true.
llm no The model that judges standard rules. See Providers.
rules yes The rules, run in order.

Unknown keys anywhere in the file are errors, so a typo cannot silently disable a rule.

Rules

Every rule has an id, which must be unique, and may have a description and a level. The rule’s kind is the one remaining key. A comment in a file can turn a rule off for one line or the whole file; see Suppressions.

Key Required Meaning
id yes Names the rule in the output and in --only.
description no A note for readers of the config. JSON output carries it, and SARIF output uses it as the rule’s short description.
level no error (default) fails the run when the rule has findings. warn prints them as WARN and leaves the exit code alone, so a new rule can run in CI while its wording settles.
files for forbid, require, and standard Globs, relative to the checked directory, that select the files the rule reads. See Selecting files.
exclude no forbid, require, and standard only. Globs the rule leaves out of its selection, on top of the top-level ignore.
message no forbid and require only. What each finding says instead of the matched line or does not match /pattern/, such as what to do instead.
Kind Needs files Fails when
forbid yes Any line of a selected file matches the pattern. One finding per line, carrying the line.
require yes A selected file does not match the pattern anywhere. One finding per file.
exists no None of its paths or globs matches anything.
absent no Any of its paths or globs matches something. One finding per match.
standard yes A model’s probability that a selected file meets the prose is below threshold. One finding per file, carrying the model’s reason and decision. See LLM rules.

exists and absent take a path or glob relative to the checked directory, or a list of them. A plain path matches a file or a directory with that name. A glob follows the rules under Selecting files and matches files only. Both leave out the top-level ignore and, inside a git work tree, whatever .gitignore covers, so a local .env that git ignores does not count as present; gitignore: false turns that off. Candidates from --files, --changed, or --since do not narrow them, since these rules are about the tree, not the change set. exists passes when anything matches; absent reports every match.

  - id: has-docs
    exists: ["README.md", "docs/**/*.md"]
  - id: no-keys
    absent: ["**/*.pem", ".env"]

Standard rules

Key Required Meaning
standard yes The standard in prose.
threshold no The probability, 0 to 1, a file must reach to pass. Defaults to 0.5.
scope no file (default) judges each selected file in its own request. set sends every selected file in one request and gets one verdict for the set. See LLM rules.
llm no Overrides for this rule: provider, model. See LLM rules.
context no Files, relative to the checked directory, sent with every request as reference material and never judged. See LLM rules.
fixtures no Example files for lawbook test, relative to the checked directory: pass, ones that must meet the standard, and fail, ones that must not. See LLM rules.

Patterns

forbid and require are JavaScript regular expressions compiled with the m and u flags, so ^ and $ match at line boundaries and the pattern must be valid Unicode-mode syntax. Quote patterns with single quotes in YAML so backslashes survive. An invalid pattern stops the run with exit code 2.

The two rules see different text. A forbid pattern is tested against one line at a time, so it can never match across lines: \n, and a lookbehind or lookahead that reaches into the previous or next line, match nothing. A require pattern is tested against the whole file. A rule that needs to see two lines, such as “every exported function has a doc comment above it”, is therefore a require with a negative lookahead over the file, anchored to its start with (?<![\s\S]):

  - id: exports-are-documented
    files: ["src/**/*.js"]
    require: '(?<![\s\S])(?![\s\S]*?(?<!\*/)\nexport (async )?function)'
    message: an exported function has no doc comment above it

It passes when no export function line follows anything but the end of a /** ... */ comment. A require finding names the file, not a line.

By default a forbid finding quotes the matching line and a require finding names the pattern. Set message to say what the contributor should do instead; the line number still locates a forbid finding:

  - id: no-console
    files: ["src/**/*.ts"]
    forbid: 'console\.(log|debug)\('
    message: log through the logger in src/log.ts

Selecting files

files is a list of globs relative to the checked directory. Globs follow the usual conventions: * matches within a path segment, ** crosses directories, and {a,b} lists alternatives. Dotfiles and dot directories are included, so .github/**/*.yml works; the default ignore keeps .git out.

exclude is an optional list of globs a files rule leaves out, on top of the top-level ignore:

  - id: no-console
    files: ["src/**/*.ts"]
    exclude: ["src/**/*.test.ts", "src/generated/**"]
    forbid: 'console\.(log|debug)\('

A rule whose globs select no files passes. Selected files are read as UTF-8 text. A file with a NUL byte in its first 8 KiB is treated as binary and skipped without a finding, so a glob that catches an image or an archive costs nothing.

Inside a git work tree, files that .gitignore covers are left out as well: one git ls-files call lists the tracked files and the untracked files git does not ignore, and files rules select only from those. Set gitignore: false to look at ignored files too. Outside a repository, or when --files, --changed, or --since names the files, this listing is not used.

lawbook check --files, --changed, and --since narrow every files rule to the files they name, so a pre-commit hook or a pull request check looks only at what changed. See Commands.

Sharing rules

extends pulls the rules of other config files in ahead of the file’s own, so a team can keep one rule set and have every repository extend it:

version: 1
extends:
  - ./rules/base.yaml
  - "@acme/lawbook-rules"
rules:
  - id: has-readme
    exists: README.md

An entry that starts with . or is an absolute path is resolved relative to the file that names it. Any other entry is a package specifier resolved from node_modules the way require would: the package’s main can point at its lawbook.yaml, or an exports entry can expose a subpath such as @acme/lawbook-rules/strict.yaml. There is no network access; install the package first. The lawbook package itself exports its examples directory, so lawbook/examples/clean-code.lawbook.yaml extends the clean-code example the installed version ships.

Every extended file is a full config with version: 1, and may extend others in turn. Rules are collected depth-first, each file’s inherited rules before its own, and run in that order. Only rules are inherited: ignore, gitignore, and llm always come from the file lawbook check reads. Globs in an inherited rule are relative to the checked directory, not to the file the rule came from.

Because llm is not inherited, a second config can run the same rules through a different judge, which is how to try a stronger model or another subscription without touching the rules:

# lawbook.sonnet.yaml: the same rules, judged by Sonnet
version: 1
extends: ./lawbook.yaml
llm:
  provider: claude-code
  model: claude-sonnet-5-5
rules: []
lawbook check . --config lawbook.sonnet.yaml
lawbook test . --config lawbook.haiku.yaml

rules is required even when empty. A per-rule llm on an inherited rule still applies; drop it from the rule to let the top-level judge decide.

A rule id that appears in two files, a file that extends itself through any chain, and an entry that cannot be found are errors with exit code 2, each naming the files involved.

Last updated on October 9, 2026

Was this page helpful?