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.