Overview
Some parts of a codebase have to change together: a function and its docs, or a value duplicated across configs.
blockwatch lets you write those rules in comments, right next to the code, or in blockwatch.toml. If they fall out of
sync, your pre-commit hook or CI run fails.
Supports 37 languages and their common file names, such as Gemfile or .mjs.
Quick Start
Section titled “Quick Start”Install blockwatch:
brew install mennanov/blockwatch/blockwatchFor Windows and other ways to install, see Installation.
Then paste this prompt into your AI coding agent:
Run `blockwatch skill` and install the skill in this project as its "Installing this skill" section says. Then use theskill to annotate the project and add `blockwatch.toml` if needed. List each block you added and the mistake itcatches.Review the suggested blocks and commit them. The skill stays in the project, so agents working on it later know
the rules too. It runs blockwatch skill each time it loads, so it always matches the installed version. To check every
future change, add blockwatch to your pre-commit hook or CI.
How it works
Section titled “How it works”A block is a pair of tags inside comments around some lines of code. The rules go in the start tag. A file that can’t
hold a comment, such as JSON, gets its rules from blockwatch.toml instead.
In this example, a web service listens on port 8080, and three files must agree on that number.
app/settings.py holds the value. It needs no tag, because the other files point at the constant by its name,
app/settings.py#/PORT:
PORT = 8080Dockerfile wraps the line in a block. The pattern \d+ picks 8080 out of EXPOSE 8080:
FROM python:3.13-slim# <block name="port" same-as="app/settings.py#/PORT" same-as-pattern="\d+">EXPOSE 8080# </block>deploy.json can’t hold a comment:
{ "name": "web", "port": 8080}So blockwatch.toml declares its block, around the port key:
[[block]]target = 'deploy.json#/port'same-as = 'app/settings.py#/PORT'If you change PORT to 9090 and forget the other two files, blockwatch fails with exit code 1 and these messages:
Block Dockerfile:port at line 2 disagrees with app/settings.py#/PORT: ["8080"] != ["9090"]Block deploy.json:(unnamed) at line 1 of "blockwatch.toml" disagrees with app/settings.py#/PORT: ["8080"] != ["9090"]The output shows the values on each side. Update both files, and the run passes.
A name after # points at a symbol: a constant in code, a key in JSON, TOML or YAML, a Markdown
section, and more. Because same-as compares values directly, it doesn’t need a diff. Running blockwatch on its own
finds mismatches anywhere in the repository. The other validators make code and docs change together,
keep lists sorted, and more.
Validators
Section titled “Validators”Two validators work across files:
affectsfails when a diff touches a block without also changing what it points to.same-asfails when a block and its target hold different values. It needs no diff.
Both can point to a block, a whole file, a specific value in a file like package.json#/version, a section of a
Markdown file, an element or attribute in an XML file such as pom.xml or a .csproj, a declaration in a CSS file,
a constant in a Go, C, C++, Python, Rust, TypeScript, JavaScript, Java, C#, Kotlin, Swift, PHP or Dart file, a
variable in a shell script or a Makefile, a field in a Protocol Buffers file, or a value in a Terraform or other HCL
file (see Symbols). The other validators check a single block on their own: the things you’d
otherwise have to nitpick in code review.
| Validator | Description | Attributes |
|---|---|---|
affects |
Forces linked blocks to be edited together (e.g. code and its docs); needs a diff | affects |
same-as |
Asserts two or more blocks hold the same value, across languages and formats; no diff needed | same-as, same-as-paths, same-as-pattern, same-as-mode, same-as-format |
keep-sorted |
Enforces alphabetical or numerical ordering on list items | keep-sorted, keep-sorted-pattern, keep-sorted-format |
keep-unique |
Prevents duplicate lines within a block | keep-unique |
line-pattern |
Enforces that every line matches a regex, or that no line matches a forbidden one | line-pattern, line-pattern-forbidden |
line-count |
Enforces lower or upper bounds on the number of lines in a block | line-count, line-count-pattern |
check-ai |
Validates content against natural language rules using an LLM | check-ai, check-ai-pattern |
check-lua |
Runs custom validation logic written in Lua | check-lua, check-lua-pattern, check-lua-timeout |
Every block can also take an optional name so other blocks can point to it, and a
severity. Only error, the default, fails the run.
See the Validators Reference for full details.
Installation
Section titled “Installation”# macOS and Linux, with Homebrewbrew install mennanov/blockwatch/blockwatch
# macOS and Linux, without Homebrewcurl --proto '=https' --tlsv1.2 -LsSf https://github.com/mennanov/blockwatch/releases/latest/download/blockwatch-installer.sh | sh
# Windowspowershell -ExecutionPolicy Bypass -c "irm https://github.com/mennanov/blockwatch/releases/latest/download/blockwatch-installer.ps1 | iex"
# With Python. On PyPI, `blockwatch` is a different project.pipx install blockwatch-cli # or: uv tool install blockwatch-cli
# From source, with Rustcargo install blockwatchPrebuilt binaries are on the Releases page.
AI Agents
Section titled “AI Agents”The skill tells an agent where blocks make sense, how to format them, and how to check its own work.
blockwatch skill prints it. The Quick Start prompt sets it up in a single project. To install it
globally for every project in Claude Code instead:
/plugin marketplace add mennanov/blockwatch/plugin install blockwatch@blockwatchSee docs/agents.md for Cursor, Copilot, Codex, and other agent setups.
Running blockwatch on its own checks every block in the repository. With --diff, blockwatch reads a unified diff
from stdin to enforce rules that need one, like affects. Adding --only-changed limits the check to only the blocks
touched by that diff:
# Check every block in the repositoryblockwatch
# Check specific files, directories or globsblockwatch src/main.rs docs "**/*.md"
# Check every block, and enforce the rules that need a diff, such as `affects`git diff --patch | blockwatch --diff
# Check only the blocks the diff changedgit diff --patch | blockwatch --diff --only-changed
# The same, for staged and unstaged changesgit diff --patch HEAD | blockwatch --diff --only-changed
# Dump all discovered blocks as JSONblockwatch list
# Show what each validator of one named block readblockwatch explain Dockerfile:portRules live in comments, next to the code they describe. Settings that apply to the whole project, such as ignored
paths, extension mappings, disabled validators, and skipped blocks, go into blockwatch.toml at the repository root:
ignore = ['**/generated/**']disable = ['check-ai']
[extensions]ino = 'cpp'The config file can also declare blocks, as in How it works:
around a symbol, around the text a regex matches, or around a whole file. That is how a file that can’t hold a tag gets
rules, such as package.json, a prompt, or a module that a build copies as it is.
To keep a known violation from failing the run without editing the source, pass its address from the output to
--suppress. The violation is still reported. For the Dockerfile failure in How it works:
blockwatch --suppress Dockerfile:port:same-as:1037fd9cA shorter address like src/lib.rs covers more violations. See
Suppressing a Violation.
To check only some blocks, or to skip some, pass FILE:BLOCK_NAME to --only-block or --skip-block. The
only-blocks and skip-blocks keys of blockwatch.toml do the same for every run. See
Selecting Blocks.
See docs/cli.md for all CLI flags, execution modes, the config file, path exclusions, and custom extension mappings.
CI Integration
Section titled “CI Integration”pre-commit (.pre-commit-config.yaml):
- repo: https://github.com/mennanov/blockwatch rev: v0.18.0 # Use latest release hooks: - id: blockwatchGitHub Actions:
- uses: mennanov/blockwatch-action@v1blockwatch exits with 1 when it finds at least one error violation, and with 2 when it can’t finish the run, such
as for a malformed tag. Otherwise it exits with 0. Warnings, info, and hints are printed, but won’t fail the run. See
Exit Codes.
For GitHub code scanning, blockwatch --format sarif writes violations as a SARIF log instead of JSON. See
SARIF Output.
For plain git hooks, local pre-commit setups, and sandboxing untrusted Lua scripts in fork pull requests, see docs/ci.md.
Supported Languages
Section titled “Supported Languages”- Bash (with
.zsh,.bashrc,.bash_profile,.zshrcand.envrcsupport) - C#
- C/C++ (
.c,.cc,.cpp,.cxx,.h,.hh,.hpp,.hxx) - CMake (
CMakeLists.txt,.cmake) - CSS
- Dart
- Dockerfile (with
Containerfileand.dockerfilesupport) - Elixir (
.ex,.exs) - Go (with
go.mod,go.sumandgo.worksupport) - GraphQL (
.graphql,.graphqls,.gql) - Groovy (with
.gradleandJenkinsfilesupport) - HCL (Terraform and OpenTofu:
.tf,.tfvars,.tofu,.hcl; Nomad:.nomad) - HTML (with
.xhtmlsupport; comments inside<script>and<style>are not read) - JSON (
.json,.jsonc,.webmanifest) - Java
- JavaScript (with
.mjsand.cjssupport) - Kotlin
- Lean
- Lua (with
.rockspecsupport) - MDX
- Makefile (with
GNUmakefileand.maksupport) - Markdown
- Nix
- OCaml (
.ml,.mli) - PHP (comments inside
<script>and<style>are not read) - Protocol Buffers (
.proto) - Python (with
.pyiand.pywsupport) - Ruby (with
Gemfile,Rakefile,Podfile,Brewfile,Fastfile,Vagrantfile,.gemspec,.podspec,.rake,.ru,.builderand.jbuildersupport) - Rust
- SQL
- Scala (with
.sbtsupport) - Starlark (Bazel:
BUILD,WORKSPACE,MODULE.bazel,.bzl,.bzlmod,.star; Buck:BUCK; Tilt:Tiltfile) - Swift
- TOML (with
Pipfilesupport) - TypeScript (with
.mtsand.ctssupport) - XML (with
.csproj,.fsproj,.vbproj,.vcxproj,.props,.targets,.xaml,.xsd,.xsland.xsltsupport) - YAML (with
.cffsupport)
blockwatch only inspects files with the extensions listed above and skips everything else, such as .ino or .cu.
To check those too, map them to a supported extension with -E or in the extensions table of blockwatch.toml:
blockwatch -E ino=cpp -E cu=cppKnown Limitations
Section titled “Known Limitations”- Deleting a block deletes its rule, quietly. If you delete a file or strip its tags, its rules are gone. The run passes without warning you that a rule disappeared. Blocks still pointing at the deleted one do fail, as a missing reference.
- A file with blocks must be UTF-8. A file in another encoding, such as Latin-1, is skipped if it has no blocks. If it has blocks, the run fails and shows the file. A UTF-16 file is skipped even when it has tags, because blockwatch can’t find them.
- Unsupported extensions are skipped silently. A run that read nothing looks identical to a run that found no
problems. Run
blockwatch --verbosity summaryto see how many files were actually checked.
Contributing
Section titled “Contributing”PRs and issues are welcome!
To run tests locally:
cargo test