Skip to content

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.

Install blockwatch:

Terminal window
brew install mennanov/blockwatch/blockwatch

For 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 the
skill to annotate the project and add `blockwatch.toml` if needed. List each block you added and the mistake it
catches.

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.

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 = 8080

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

Two validators work across files:

  • affects fails when a diff touches a block without also changing what it points to.
  • same-as fails 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.

Terminal window
# macOS and Linux, with Homebrew
brew install mennanov/blockwatch/blockwatch
# macOS and Linux, without Homebrew
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/mennanov/blockwatch/releases/latest/download/blockwatch-installer.sh | sh
# Windows
powershell -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 Rust
cargo install blockwatch

Prebuilt binaries are on the Releases page.

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@blockwatch

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

Terminal window
# Check every block in the repository
blockwatch
# Check specific files, directories or globs
blockwatch 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 changed
git diff --patch | blockwatch --diff --only-changed
# The same, for staged and unstaged changes
git diff --patch HEAD | blockwatch --diff --only-changed
# Dump all discovered blocks as JSON
blockwatch list
# Show what each validator of one named block read
blockwatch explain Dockerfile:port

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

Terminal window
blockwatch --suppress Dockerfile:port:same-as:1037fd9c

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

pre-commit (.pre-commit-config.yaml):

- repo: https://github.com/mennanov/blockwatch
rev: v0.18.0 # Use latest release
hooks:
- id: blockwatch

GitHub Actions:

- uses: mennanov/blockwatch-action@v1

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

  • Bash (with .zsh, .bashrc, .bash_profile, .zshrc and .envrc support)
  • C#
  • C/C++ (.c, .cc, .cpp, .cxx, .h, .hh, .hpp, .hxx)
  • CMake (CMakeLists.txt, .cmake)
  • CSS
  • Dart
  • Dockerfile (with Containerfile and .dockerfile support)
  • Elixir (.ex, .exs)
  • Go (with go.mod, go.sum and go.work support)
  • GraphQL (.graphql, .graphqls, .gql)
  • Groovy (with .gradle and Jenkinsfile support)
  • HCL (Terraform and OpenTofu: .tf, .tfvars, .tofu, .hcl; Nomad: .nomad)
  • HTML (with .xhtml support; comments inside <script> and <style> are not read)
  • JSON (.json, .jsonc, .webmanifest)
  • Java
  • JavaScript (with .mjs and .cjs support)
  • Kotlin
  • Lean
  • Lua (with .rockspec support)
  • MDX
  • Makefile (with GNUmakefile and .mak support)
  • Markdown
  • Nix
  • OCaml (.ml, .mli)
  • PHP (comments inside <script> and <style> are not read)
  • Protocol Buffers (.proto)
  • Python (with .pyi and .pyw support)
  • Ruby (with Gemfile, Rakefile, Podfile, Brewfile, Fastfile, Vagrantfile, .gemspec, .podspec, .rake, .ru, .builder and .jbuilder support)
  • Rust
  • SQL
  • Scala (with .sbt support)
  • Starlark (Bazel: BUILD, WORKSPACE, MODULE.bazel, .bzl, .bzlmod, .star; Buck: BUCK; Tilt: Tiltfile)
  • Swift
  • TOML (with Pipfile support)
  • TypeScript (with .mts and .cts support)
  • XML (with .csproj, .fsproj, .vbproj, .vcxproj, .props, .targets, .xaml, .xsd, .xsl and .xslt support)
  • YAML (with .cff support)

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:

Terminal window
blockwatch -E ino=cpp -E cu=cpp
  • 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 summary to see how many files were actually checked.

PRs and issues are welcome!

To run tests locally:

Terminal window
cargo test