# CLI Reference
For command-line flag documentation directly in your terminal, run `blockwatch --help`.
## Quick Options Reference
[//]: # (<block name="cli-docs">)
- **Read a Diff**: `git diff --patch | blockwatch --diff` marks which blocks the diff changed.
- **Only Changed Blocks**: `git diff --patch | blockwatch --diff --only-changed` narrows the run to them, instead of
every block in the repository.
- **List Blocks**: `blockwatch list` outputs a JSON report of all discovered blocks.
- **Custom Extensions**: Map custom file extensions: `blockwatch -E cxx=cpp`
- **Disable Validators**: `blockwatch -d check-ai`
- **Enable Validators**: `blockwatch -e keep-sorted`
- **Ignore Files**: `blockwatch --ignore "**/generated/**"`
- **Report What Ran**: `blockwatch --verbosity summary` (or `full` for JSON on stdout)
- **Suppress Violations**: `blockwatch --suppress FILE[:BLOCK[:VALIDATOR[:HASH]]]` reports them but stops them failing
the run
- **Violation Format**: `blockwatch --format sarif` writes a SARIF log instead of the JSON diagnostics
[//]: # (</block>)
## Selecting Files
By default, `blockwatch` scans every file in the repository, respecting `.gitignore`. Paths are reported relative to the
repository root, whichever directory you run from. Linked worktrees and submodules are supported too: a run inside one
checks that repository only.
VCS directories like `.git`, `.hg`, `.jj` and `.svn` are skipped.
```shell
# Check everything in the repository
blockwatch
# Restrict checks to specific glob patterns
blockwatch "src/**/*.rs" "**/*.md"
# Exclude specific paths
blockwatch "**/*.rs" --ignore "**/generated/**"
```
Note: Quote glob patterns to prevent shell expansion before passing arguments to `blockwatch`.
Globs **intersect** with whatever the run mode selected, in every mode. They only ever narrow a run: passing
`"src/**/*.rs"` alongside a diff checks the changed blocks under `src/`, and never adds an unchanged file back.
Globs choose which blocks are **validated**, not which files a rule may **resolve a reference against**. A rule such as
[`affects`](validators/affects.md) still finds its target in a file the globs left out, so narrowing a run to one
language does not turn every cross-language rule into a failure. An excluded file is read to answer the reference and
for nothing else: it is never validated, and never appears in a run report.
## Run Modes
Which files are parsed and which blocks are validated are two separate decisions, and each has its own flag.
| `blockwatch` | Every file in scope | Every block found; none counts as changed |
| `blockwatch --diff` | Every file in scope | Every block found; the diff marks which ones changed |
| `blockwatch --diff --only-changed` | Only the files in the diff | Only the blocks the diff touched |
```shell
# Check every block in the repository
blockwatch
# Check every block, and enforce the rules that need a diff
# Check only the blocks the diff changed
# The same, for staged changes
# Changed blocks under specific globs only
Language detection relies on file extensions. Use `-E` to map unrecognized or custom extensions to a supported grammar:
```shell
blockwatch -E cxx=cpp -E c++=cpp
```
Files with extensions that do not map to any supported grammar are ignored.
## Enabling and Disabling Validators
Control which validators run using `-e` (enable only) or `-d` (disable):
```shell
# Run all validators except check-ai
blockwatch -d check-ai
# Run only keep-sorted and keep-unique
blockwatch -e keep-sorted -e keep-unique
```
Note: `-e` and `-d` cannot be combined in a single invocation.
## Suppressing a Violation
`--suppress` takes the address of a violation and stops it failing the run. The violation is still reported, at its
declared severity, marked `"suppressed": true`; only the exit code changes.
```shell
blockwatch --suppress docs/cli.md:cli-docs:keep-sorted
```
Use it when a rule is wrong at one particular site and you do not want to edit the source or turn the validator off
everywhere with `-d`. Repeat the flag to suppress several violations.
### The Address of a Violation
FILE[:BLOCK_NAME[:VALIDATOR[:HASH]]]
Every violation of a named block carries its full address in the diagnostics, so the usual way to write a `--suppress`
flag is to copy one from the output.
- `FILE:BLOCK_NAME` is the same `file:name` grammar [`affects`](validators/affects.md) and
[`same-as`](validators/same-as.md) use, and identifies exactly one block. As there, a file path containing a `:`
cannot be addressed.
- `VALIDATOR` is the rule that reported the violation, spelled as in `-d` and `-e`.
- `HASH` is an opaque hex string telling one violation of a block from its siblings, emitted by the five validators that
can report several: `keep-sorted`, `keep-unique`, `line-pattern`, `affects` and `same-as`.
**Every length is valid, and the shorter it is, the more it covers.** Only `FILE` is required:
| `FILE` | every violation in that file, named blocks or not |
| `FILE:BLOCK_NAME` | every violation of that block |
| `FILE:BLOCK_NAME:VALIDATOR` | every violation that validator reports on the block |
| `FILE:BLOCK_NAME:VALIDATOR:HASH` | exactly one violation |
**A block with no `name` can only be suppressed file-wide.** Its violations carry no `address` in the diagnostics,
because there is nothing narrower to point at, but a `FILE` address covers the whole file and so covers them too.
An invalid address that covers nothing is ignored.
## SARIF Output
`--format sarif` writes the violations as a [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
log instead of the JSON diagnostics. It goes to the same place they do, **stderr**, so the run report keeps stdout to
itself:
```shell
blockwatch --format sarif 2> blockwatch.sarif
```
Nothing else about the run changes: the same violations are found, and the exit code is decided the same way.
```json
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "blockwatch",
"version": "0.5.2",
"semanticVersion": "0.5.2",
"informationUri": "https://github.com/mennanov/blockwatch",
"rules": [
{
"id": "keep-sorted",
"name": "keep-sorted",
"shortDescription": { "text": "Requires the lines of a block to stay in order." },
"helpUri": "https://github.com/mennanov/blockwatch/blob/v0.5.2/docs/validators/keep-sorted.md"
}
]
}
},
"columnKind": "unicodeCodePoints",
"results": [
{
"ruleId": "keep-sorted",
"ruleIndex": 0,
"level": "error",
"message": { "text": "Block fruits.py:fruits defined at line 2 has an out-of-order line 4 (asc)" },
"locations": [
{
"physicalLocation": {
"artifactLocation": { "uri": "fruits.py" },
"region": { "startLine": 4, "startColumn": 5, "endLine": 4, "endColumn": 12 }
}
}
],
"partialFingerprints": { "blockwatchAddress/v1": "fruits.py:fruits:keep-sorted:bbd61689" },
"properties": {
"address": "fruits.py:fruits:keep-sorted:bbd61689",
"data": { "order_by": "asc" }
}
}
]
}
]
}
```
What to expect from the log:
- **A clean run still writes one**, with an empty `results` array. A service that reads SARIF treats a missing log as a
run that never happened, rather than as a run that found nothing.
- **Only the rules that fired are described.** Each carries a `helpUri` to its documentation, pinned to the version that
produced the log.
- **`level` follows [severity](validators/README.md#severity)**: `error` and `warning` keep their names, and both `info`
and `hint` become `note`, the weakest level SARIF consumers display.
- **A [suppressed](#suppressing-a-violation) violation is reported like any other**, with
`"suppressions": [{"kind": "external"}]` alongside it — SARIF's own way of saying a reviewer accepted it. The
justification is left out: it lives wherever the suppression itself is recorded.
- **`partialFingerprints` carries the violation's [address](#the-address-of-a-violation)**, so a service can match a
violation against the same one in an earlier run. It is absent for a violation on an unnamed block, which has no
address. The address is repeated in `properties`, where it is easier for a person to find and copy into a `--suppress`
flag.
- **Paths are repository-relative**, the same paths the JSON diagnostics are keyed by, percent-encoded where a character
would otherwise change how the path reads as a URI (a `#` or a space, say).
- **Lines and columns are 1-based, with an exclusive end column.** A column counts characters, which the run declares as
`"columnKind": "unicodeCodePoints"` so that a consumer does not count UTF-16 code units instead.
`--format` cannot be combined with the `list` subcommand, which reports blocks rather than violations. To upload a log
to GitHub code scanning, see [CI Integration](ci.md#github-code-scanning).
## The `list` Command
The `list` command outputs details on all discovered blocks in JSON format without running validation. It mirrors the
[run modes](#run-modes) exactly, so `list` and the default command always agree on which blocks exist.
```shell
# List all blocks under the current directory
blockwatch list
# Restrict block listing to specific globs
blockwatch list "src/**/*.rs" "**/*.md"
# List all blocks, marking the ones the diff changed
# List only the blocks the diff changed
Like the default command, `blockwatch list` reads stdin only when `--diff` is explicitly provided, preventing blocking
during non-interactive scripts or pipeline commands (e.g. `blockwatch list "src/**/*.ts" | jq`).
Each block entry carries an `is_content_modified` boolean field. Without a diff nothing marks a block as changed, so it
is `false` throughout; under `--diff` it identifies the blocks the diff touched.
Lines and columns are 1-based, and a column counts characters rather than bytes, so a multi-byte character such as `é`
or an emoji advances it by one. The `range` of a violation follows the same convention.
### Output Example
[//]: # (<block name="list-output-example">)
```json
{
"src/lib.rs": [
{
"attributes": {
"affects": "README.md:supported-langs",
"name": "languages-code"
},
"column": 4,
"is_content_modified": false,
"line": 12,
"name": "languages-code"
},
{
"attributes": {
"keep-sorted": "asc"
},
"column": 8,
"is_content_modified": false,
"line": 40,
"name": "(unnamed)"
}
]
}
```
[//]: # (</block>)
A block with no `name` attribute is reported as `(unnamed)`. Within a file, blocks appear in source order and each
block's attributes are sorted by name; the files themselves are not ordered, and two runs over an unchanged tree may
emit them differently. Sort by key downstream if you need to diff one run against another — or use [
`--verbosity full`](#full-reports), which does order its files.
## Run Reports
By default `blockwatch` prints nothing when a run succeeds. That makes a check that passed look exactly like a check
that never ran. Use `--verbosity` to see what was actually checked.
| `none` (default) | Nothing. |
| `summary` | One line of counts. |
| `full` | A JSON report of every block and the validators that checked it. |
The report goes to **stdout**. Violations go to **stderr**. A run can print both, and each one can be piped and parsed
on its own.
```shell
blockwatch --verbosity summary
blockwatch: mode=all, 34/240 files, 61 blocks (3 unchecked, 2 needs --diff), 73 checks, 0 violations
```
Reading that line:
- `mode=all` — which [run mode](#run-modes) this was: `all`, `all+diff`, or `only-changed`. Each name is a single token,
so the line stays tokenizable on whitespace.
- `34/240 files` — 240 files were read, and 34 of them contain blocks.
- `61 blocks (3 unchecked, 2 needs --diff)` — 61 blocks were in scope, no validator checked 3 of them, and 2 carry a
rule that cannot fire at all without a diff. Fixing that means supplying one, so the figure is a prompt to change how
you invoked `blockwatch`.
- `73 checks` — validators ran 73 times in total, once per block they applied to.
- `0 violations` — nothing failed.
**`needs --diff` appears only under `mode=all`.** Every other field is present in every mode. Once a diff is supplied
those rules *can* fire, so the question the figure answers no longer arises — and the obvious substitute, counting the
blocks the diff did not happen to reach, would just measure the size of your change. On a repository with fifty
`affects` blocks, a one-line commit would report forty-nine, every time, with nothing wrong. So the clause is left out
rather than reported as zero or as noise.
A parser should therefore read `mode=` first and expect the clause only for `all`; the remaining fields keep a fixed
shape in every mode.
A block goes unchecked for one of three reasons:
- **It is only a reference target.** A block that carries nothing but a `name` exists so that other blocks can point at
it with `affects` or `same-as`. It declares no rule of its own, so nothing checks it. This is normal and needs no
fixing.
- **The validator does not apply to this run.** `affects` only compares blocks that a diff has touched, so it checks
nothing without `--diff`. These are the blocks the `needs --diff` figure counts. Under a diff the same block goes
unchecked whenever the diff did not reach it, which is normal for an incremental run and is not counted.
- **The attributes do not add up to a rule.** A modifier such as `keep-sorted-pattern` only refines the validator it
belongs to; on a block with no `keep-sorted`, it has nothing to modify and no validator claims the block. A `full`
report lists every attribute as it was written, which is usually enough to see what is missing.
### Reports Under a Diff
Under `--diff --only-changed` the diff scopes the report exactly as it scopes the run: only the blocks the diff touched
are described. A block the diff never reached is *absent* from the report rather than listed with an empty `checks`
array, so `blocks_unchecked` counts only blocks that were in scope and that nothing checked. This is the same rule
`blockwatch list --diff --only-changed` follows, so the two commands always agree on which blocks exist. Under `--diff`
alone the report covers the whole tree, exactly as a run without a diff does.
Reference targets are reported by the run's scope, even though they are not resolved by it. When a block declares
`affects` or `same-as`, its target is read from disk and compared wherever it lives — but under `--only-changed` the
target appears in the report only if the diff touched it as well. A diff that changes the source alone therefore reports
a single file, even though two were involved:
```shell
```
Nothing about the failure is hidden by this. Violations are printed to stderr as JSON, keyed by the file the violating
block lives in, and the message names both sides:
```json
{
"fileA.py": [
{
"address": "fileA.py:a:affects:1d7a4c02",
"code": "affects",
"data": {
"affected_block_file_path": "fileB.py",
"affected_block_name": "b"
},
"message": "Block fileA.py:a at line 1 is modified, but fileB.py:b is not",
"range": {
"end": {
"character": 39,
"line": 1
},
"start": {
"character": 3,
"line": 1
}
},
"severity": 1
}
]
}
```
`severity` follows the [LSP numbering](validators/README.md#severity): `1` error, `2` warning, `3`
info, `4` hint. For a code-scanning service, ask for the same violations as SARIF instead — see
[SARIF Output](#sarif-output).
`address` is what a `--suppress` flag points at — see [Suppressing a Violation](#suppressing-a-violation). It is absent
when the block has no `name`, which leaves a file-wide address as the only way to suppress the violation. A suppressed
violation carries `"suppressed": true` alongside it; the key is absent otherwise. **A consumer that decides on
`severity` alone has to skip the suppressed violations**, which keep the severity their author declared.
The division of labour is deliberate — the report describes what the run examined, and the violation explains what went
wrong. Once the diff touches the target as well, it appears like any other block, with an empty `checks` array because a
block that carries nothing but a `name` declares no rule of its own:
```shell
```
### Full Reports
`--verbosity full` describes every block the same way `blockwatch list` does, and adds a `checks` array naming the
validators that ran on it. A block checked by several validators lists all of them. The `summary` object carries the
same counts as the one-line report and follows the same rule: `blocks_needing_diff` is present only under `mode=all`,
and is absent — not zero — in the other two modes.
```json
{
"summary": {
"mode": "all",
"files_scanned": 240,
"files_with_blocks": 34,
"files_skipped": 179,
"blocks": 61,
"blocks_unchecked": 3,
"blocks_needing_diff": 2,
"checks": 73,
"violations": 0,
"validators": {
"affects": 41,
"check-lua": 12,
"keep-sorted": 20
}
},
"files": {
"src/validators/check_ai.rs": [
{
"attributes": {
"affects": "docs/validators/check-ai.md:check-ai-env-vars",
"name": "check-ai-env-vars",
"same-as": "docs/validators/check-ai.md:check-ai-env-vars",
"same-as-pattern": "BLOCKWATCH_AI_[A-Z_]+"
},
"checks": [
"affects",
"same-as"
],
"column": 4,
"is_content_modified": true,
"line": 31,
"name": "check-ai-env-vars"
}
]
}
}
```
Files are sorted by path, and each block's checks by validator name, so two runs over an unchanged tree print the same
bytes.
The report says which validators looked at a block, not what each one concluded. Violations are not repeated here; they
stay on stderr, under the same file paths and line numbers.
`--verbosity` cannot be combined with the `list` subcommand, because `list` already prints its own JSON to stdout.
## Exit Codes
| `0` | Success. No violations found, or every reported violation is either non-`error` [severity](validators/README.md#severity) or [suppressed](#suppressing-a-violation). |
| `1` | Failure. At least one unsuppressed `error`-severity violation was detected. |
---
[← Return to README](../README.md)