---
title: Commands
description: Every lawbook command and flag.
sidebar:
  order: 8
---

Running `lawbook` with no command prints the usage and exits `2`.
`lawbook --help` and `lawbook --version` exit `0`.

## `lawbook init [root]`

Writes a starter `lawbook.yaml` into `root` (default: the current directory)
and prints its path. Fails with exit code `2` when `lawbook.yaml`,
`lawbook.yml`, or `lawbook.json` already exists there, or when `root` does not
exist.

## `lawbook check [root]`

Runs the rules against `root` (default: the current directory) and prints one
line per rule followed by a summary.

| Flag                    | Meaning                                                                                      |
| ----------------------- | -------------------------------------------------------------------------------------------- |
| `-c, --config <file>`   | Read this config instead of looking for `lawbook.yaml`, `lawbook.yml`, or `lawbook.json` in `root`. Paths in the config stay relative to `root`. |
| `--format <format>`     | `text` (default), `json`, `github` (workflow commands for GitHub Actions), `sarif` (SARIF 2.1.0 for code scanning), or `gitlab` (a GitLab code quality report). |
| `--only <ids...>`       | Run only the rules with these ids, in config order. An unknown id is an error.                |
| `--no-llm`              | Report `standard` rules as skipped instead of asking a model. No provider client is built.    |
| `--files <paths...>`    | Check only these files. Paths are relative to the current directory, or absolute, like `root` itself. |
| `--changed`             | Check only files changed in the working tree against `HEAD`: staged, unstaged, and untracked. |
| `--since <ref>`         | Check only files committed since the merge base with `ref`, as `git diff <ref>...HEAD` lists them. |
| `--changed-lines`       | Judge `standard` rules on the lines `--changed` or `--since` touched, with the rest of each file as reference, and drop findings on untouched lines. Needs one of those selectors. See [LLM rules](/lawbook/llm-rules#changed-lines). |
| `--baseline <file>`     | Hide the findings this file records; see [Baseline](/lawbook/baseline). Relative to the current directory. |
| `--update-baseline`     | Write this run's findings to the `--baseline` file, then report with them hidden. |
| `--dry-run`             | List the files each rule would check and how many model requests the run would make, then exit `0` without reading a file or building a client. Text or JSON only. |
| `--explain`             | Print the model's reason for files that pass a `standard` rule, not only for findings. JSON carries them as `reasons` by path, or `reason` for a set. |
| `--max-requests <n>`    | Stop with exit code `2` before asking the model when the run would make more than `n` requests. Overrides `llm.maxRequests`. See [LLM rules](/lawbook/llm-rules#budget). |
| `--no-cache`            | Ask the model for every file even when a cached verdict exists. See [LLM rules](/lawbook/llm-rules#verdict-cache). |
| `--cache-dir <dir>`     | Where verdicts are cached. Relative to the current directory; default `node_modules/.cache/lawbook` under `root`. |

### Checking a subset of files

`--files`, `--changed`, and `--since` each name candidate files; given
together, their union is used. A `files` rule then selects only the
candidates its globs match, so a candidate no rule covers, such as a lockfile
or an image, is simply not checked. `exists` and `absent` rules ignore the
candidates, though `.gitignore` still applies to them. A rule whose globs
select no candidate passes.

`--changed` and `--since` ask git, so `root` must lie inside a repository
and `ref` must resolve, else `check` exits `2` with git's message. Paths git
reports are rebased onto `root`, so a `root` below the repository's top
level works and changes outside it are dropped.

`--changed-lines` narrows what a `standard` rule judges within each selected
file, not only which files it sees: the model judges the lines the change
touched and reads the rest as reference, and a finding on an untouched line
is dropped and counted. It needs `--changed` or `--since` to say what
changed, else `check` exits `2`. See [Changed lines](/lawbook/llm-rules#changed-lines).

`--files` and `--only` take lists, so put `root` before them. The repository
defines [pre-commit](https://pre-commit.com) hooks that append the staged
file names; prek reads the same file. `lawbook` skips `standard` rules, and
`lawbook-llm` runs them too:

```yaml
repos:
  - repo: https://github.com/drew-simmons/lawbook
    rev: v0.1.0
    hooks:
      - id: lawbook
```

pre-commit installs the hook with the Node.js it finds on the machine, so
add `default_language_version: { node: system }` or point `language_version`
at a Node.js 22.12 or newer. With `lawbook` already installed globally, a
local hook works too:

```yaml
- repo: local
  hooks:
    - id: lawbook
      name: lawbook
      entry: lawbook check . --no-llm --files
      language: system
```

A CI job on a pull request checks only what the branch changed:

```yaml
- uses: actions/checkout@v5
  with:
    fetch-depth: 0
- run: npx lawbook check --since origin/${{ github.base_ref }} --changed-lines
```

`github.base_ref` is set on `pull_request` events only; a job that also
runs on `push` needs another ref there. `--changed-lines` keeps a finding on
a line the pull request never touched from failing it.

### Dry run

`--dry-run` shows what a run would look at before it looks. Each rule lists
the files its globs select, after `ignore`, `exclude`, `.gitignore`, and any
`--files`, `--changed`, or `--since` selector; `exists` and `absent` list
their paths and globs as written, since nothing is read. A `standard` rule
leaves out empty files, which it tells by size alone, since `check` never
sends them, and under `--changed-lines` the files with no changed lines. The last line counts the distinct files and the model
requests: one per listed file of a `standard` rule, one for a `scope: set`
rule with any files, or none under `--no-llm`. Nothing is read, so binary,
oversize, and whitespace-only files still appear, and no provider client is
built.

```txt
PLAN no-console (forbid, 2 files)
  src/cli.ts
  src/init.ts
PLAN has-readme (exists, 1 file)
  README.md
PLAN errors-are-actionable (standard, 2 files)
  src/cli.ts
  src/init.ts

3 files, 2 model requests
```

`--format json` prints `{ "rules": [{ "id", "kind", "level", "files",
"requests" }], "requests" }`, with each rule's own request count. The `github`, `sarif`, and `gitlab` formats have nothing to annotate
and are rejected with exit code `2`.

### Text output

```txt
PASS no-console
WARN spdx-header
  src/cli.ts: does not match /^// SPDX-License-Identifier: /
FAIL has-readme
  README.md: missing

1 passed, 1 failed, 1 warned, 0 errored, 0 skipped
```

Findings under a `forbid` rule read `path:line: <the line>`; under `require`,
`path: does not match /pattern/`, or the rule's `message` in place of the
line or the pattern when it sets one; under `exists` and `absent`, `path: missing`
or `path: exists`; under `standard`, `path: <the model's reason> (noul
<probability>)`. A rule with `level: warn` prints `WARN <id>` instead of
`FAIL` and does not affect the exit code. Findings a [baseline](/lawbook/baseline)
hides are counted on one line, `  2 findings in baseline`, after the rule's
findings. A `standard` rule the provider
could not finish prints `ERROR <id>`, with the model's findings for the files
it did judge and a `path: <error>` line for each file it did not, and the run
exits `2` after the report. With `--explain`, a `standard` rule also prints
`path: passed, <the model's reason> (noul <probability>)` for each file that
passed, or one `passed, <reason>` line for a passing set, so a standard's
wording can be tuned against what the model actually saw. A `standard` rule lists files it left out, such
as one over `llm.maxBytes`, as `path: skipped, <why>` after its findings;
those lines do not change the rule's status. A skipped rule prints
`SKIP <id>` with no findings. The summary line always lists all five counts
in this order. When a `standard` rule ran, one more line says what the run
cost: `3 requests (1 cached), 4210 input tokens, 180 output tokens`, where
requests went to the provider, cached verdicts came from the
[verdict cache](/lawbook/llm-rules#verdict-cache), and input tokens count everything
the model read, including what the provider served from its own cache.

### JSON output

```json
{
  "results": [
    {
      "id": "spdx-header",
      "kind": "require",
      "level": "warn",
      "status": "warn",
      "findings": [{ "path": "src/cli.ts", "message": "does not match /^// SPDX-License-Identifier: /" }]
    },
    {
      "id": "errors-are-actionable",
      "kind": "standard",
      "level": "error",
      "status": "fail",
      "findings": [
        {
          "path": "src/cli.ts",
          "message": "\"unexpected state\" on line 41 says what broke but not what to do.",
          "decision": { "type": "noul", "noul": 0.12 }
        }
      ],
      "decisions": {
        "src/cli.ts": { "type": "noul", "noul": 0.12 },
        "src/init.ts": { "type": "noul", "noul": 0.97 }
      },
      "usage": {
        "inputTokens": 310,
        "outputTokens": 48,
        "cacheReadInputTokens": 1200,
        "cacheCreationInputTokens": 400,
        "requests": 2,
        "cached": 0
      }
    }
  ],
  "summary": {
    "passed": 0,
    "failed": 1,
    "warned": 1,
    "errored": 0,
    "skipped": 0,
    "usage": {
      "inputTokens": 310,
      "outputTokens": 48,
      "cacheReadInputTokens": 1200,
      "cacheCreationInputTokens": 400,
      "requests": 2,
      "cached": 0
    }
  }
}
```

`level` is the rule's configured `error` or `warn`. `baselined` counts the
findings a [baseline](/lawbook/baseline) hid, and appears only when there were any.
`status` is `pass`,
`fail`, `warn` (findings under a `warn` rule), `error` (a `standard` rule the
provider could not finish; its findings without a `decision` name the files
and reasons), or `skip` (a `standard` rule under `--no-llm`). `findings`
carry `path` and, for `forbid`, a 1-based `line`.

A judged `standard` rule adds `decisions`, every judged file's decision keyed
by path, and each of its findings carries its own `decision`. Under
`--explain` it also adds `reasons`, the model's reason for each passing file
keyed by path; failing files already carry theirs in `findings[].message`. A `scope: set` rule carries
one `decision` for the set instead, and its finding has no `path`; under
`--explain` a passing set carries one `reason`. When it
left files out, `skipped` lists each as `{ "path", "message" }`. Its `usage`, and
the summary's, count `requests` sent to the provider, verdicts served
`cached` from lawbook's cache, and the provider's token counts, with
`cacheReadInputTokens` and `cacheCreationInputTokens` for its prompt cache. A decision
follows the Jev decision schema: `{ "type": "noul", "noul": <0..1> }`, the
probability that the file meets the standard. See
[LLM rules](/lawbook/llm-rules#decisions).

### GitHub output

`--format github` prints one
[workflow command](https://docs.github.com/actions/reference/workflow-commands-for-github-actions)
per finding, so a GitHub Actions job annotates the pull request inline, then
the summary line as a notice. Findings under a `warn` rule annotate as
warnings; every other finding, including a provider error, annotates as an
error. `file=` and `line=` appear when the finding has them, and `title=`
carries the rule id.

```txt
::error file=src/cli.ts,line=12,title=no-console::console.log("starting");
::warning file=src/cli.ts,title=spdx-header::does not match /^// SPDX-License-Identifier: /
::error file=README.md,title=has-readme::missing
::notice title=lawbook::0 passed, 2 failed, 1 warned, 0 errored, 0 skipped
```

```yaml
- run: npx lawbook check --format github
```

### SARIF output

`--format sarif` prints a [SARIF 2.1.0](https://sarifweb.azurewebsites.net/)
log with one run: the tool, one rule per lawbook rule (its `description`
as the short description, or the id when it has none), and one result per
finding. Paths are relative to the `ROOT` base, which is the checked
directory as a file URL. Upload it to GitHub code scanning:

```yaml
- run: npx lawbook check --format sarif > lawbook.sarif
  continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: lawbook.sarif
```

```json
{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "lawbook",
          "version": "0.1.0",
          "informationUri": "https://drew-simmons.github.io/lawbook/",
          "rules": [{ "id": "no-console", "shortDescription": { "text": "Production code logs through the logger" } }]
        }
      },
      "originalUriBaseIds": { "ROOT": { "uri": "file:///home/me/project/" } },
      "results": [
        {
          "ruleId": "no-console",
          "level": "error",
          "message": { "text": "console.log(\"starting\");" },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": { "uri": "src/cli.ts", "uriBaseId": "ROOT" },
                "region": { "startLine": 12 }
              }
            }
          ]
        }
      ]
    }
  ]
}
```

`level` is `warning` for findings under a `warn` rule and `error` otherwise.
A finding without a path, such as an `exists` rule with several patterns
or a `scope: set` finding, has no `locations`.

### GitLab output

`--format gitlab` prints a
[code quality report](https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format):
a JSON array with one issue per finding. GitLab shows the issues inline on a
merge request when the job saves the file as a `codequality` artifact:

```yaml
lawbook:
  script: npx lawbook check --format gitlab > gl-code-quality-report.json
  artifacts:
    when: always
    reports:
      codequality: gl-code-quality-report.json
```

```json
[
  {
    "description": "console.log(\"starting\");",
    "check_name": "no-console",
    "fingerprint": "3f786850e387550fdab836ed7e6dc881de23001b4a0f3c7bd2e6a1c95f0e8a4d",
    "severity": "major",
    "location": { "path": "src/cli.ts", "lines": { "begin": 12 } }
  }
]
```

`severity` is `major` for findings under a failing rule, `minor` under a
`warn` rule, and `critical` for a provider error. The `fingerprint` hashes
the rule id, the path, and the message but not the line, so a finding keeps
its identity when the code above it moves. A finding without a path, such as
an `exists` rule with several patterns or a `scope: set` finding, is
located at the config file, line 1.

## `lawbook test [root]`

Judges each `standard` rule's [fixtures](/lawbook/llm-rules#testing-a-standard),
example files the standard must pass or must fail, and reports the ones the
model put on the wrong side of the rule's `threshold`. Rules without
`fixtures` are left out.

| Flag                  | Meaning                                                                          |
| --------------------- | -------------------------------------------------------------------------------- |
| `-c, --config <file>` | Read this config instead of looking for one in `root`.                           |
| `--format <format>`   | `text` (default) or `json`.                                                      |
| `--only <ids...>`     | Test only the rules with these ids. An unknown id is an error.                   |
| `--no-cache`          | Ask the model for every fixture even when a cached verdict exists.               |
| `--cache-dir <dir>`   | Where verdicts are cached; the same default as `check`.                          |

Each fixture is judged alone, as a `scope: file` request, with the rule's
`context` and `llm` settings, through the same verdict cache `check` uses.
Text output prints `PASS <id>` or `FAIL <id>` per rule, one line per
misclassified fixture, and a total:

```txt
FAIL errors-are-actionable
  fixtures/errors/vague.ts: expected fail, judged pass (noul 0.72): The messages name the failing step.

4 fixtures, 1 misclassified
```

`--format json` prints `{ "rules": [{ "id", "cases": [{ "path", "expected",
"actual", "decision", "reason" }] }], "summary": { "cases", "misclassified" } }`
with every fixture, not only the misclassified ones.

The exit code is `1` when any fixture is misclassified, `2` when a fixture
is missing or binary or the provider fails, and `0` otherwise.

## `lawbook schema`

Prints the JSON Schema for `lawbook.yaml`, drawn from the same definitions
`check` validates with, so it always matches the version installed. The
schema is also published at
`https://raw.githubusercontent.com/drew-simmons/lawbook/main/lawbook.schema.json`
for editors; see [Configuration](/lawbook/configuration#editor-support).
