---
title: Configuration
description: The lawbook.yaml format, the five rule kinds, and how files are selected.
sidebar:
  order: 2
---

`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.

```yaml
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
# 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](#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](/lawbook/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](/lawbook/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](#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](/lawbook/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](#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.

```yaml
  - 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](/lawbook/llm-rules#judging-files-together). |
| `llm`       | no       | Overrides for this rule: `provider`, `model`. See [LLM rules](/lawbook/llm-rules#per-rule-model). |
| `context`   | no       | Files, relative to the checked directory, sent with every request as reference material and never judged. See [LLM rules](/lawbook/llm-rules#reference-files). |
| `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](/lawbook/llm-rules#testing-a-standard). |

### 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])`:

```yaml
  - 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:

```yaml
  - 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`:

```yaml
  - 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](/lawbook/commands#checking-a-subset-of-files).

## 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:

```yaml
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](/lawbook/examples) 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:

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

```sh
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.
