BlockWatch
June 14, 2026 · View on GitHub
BlockWatch is a linter that keeps your code, documentation, and configuration in sync and enforces strict formatting and validation rules.
It helps you avoid broken docs and messy config files by enforcing rules directly in your comments. You can link code to documentation, enforce sorted lists, ensure uniqueness, and even validate content with Regex, AI, or custom Lua scripts.
It works with almost any language (Rust, Python, JS, Go, Markdown, YAML, etc.) and can run on your entire repo or just your VCS diffs.
Annotate your project with an AI agent (recommended)
Adding the first <block> tags by hand is the tedious part of picking up BlockWatch.
AI coding agents are good at this. An agent can read through the repo, pick reasonable spots, add the tags in the
correct comment syntax for each language, and run blockwatch to check its own work.
There's a skill in this repo for it: .agents/skills/blockwatch/SKILL.md. It
tells
the agent where blocks are worth adding, documents the tag syntax, and explains how to verify the result.
1. Install the binary so the agent can run it:
cargo install blockwatch # or: brew install mennanov/blockwatch/blockwatch
2. Give the skill to your agent.
Claude Code users: install the plugin once and the skill is available in every project — no per-project setup:
/plugin marketplace add mennanov/blockwatch
/plugin install blockwatch@blockwatch
For other agents (or if you prefer a project-local copy), place SKILL.md where your tool looks
for instructions:
| Agent | Where to put the skill |
|---|---|
| Claude Code | Use the plugin above (recommended), or .claude/skills/blockwatch/SKILL.md (project) / ~/.claude/skills/... (global) |
| Cursor | .cursor/rules/blockwatch.mdc |
| GitHub Copilot | append to .github/copilot-instructions.md |
| Codex / others | append to AGENTS.md |
You can pull the file straight from this repo:
mkdir -p .claude/skills/blockwatch
curl -sL https://raw.githubusercontent.com/mennanov/blockwatch/main/.agents/skills/blockwatch/SKILL.md \
-o .claude/skills/blockwatch/SKILL.md
3. Ask the agent to annotate the project, for example:
Using the BlockWatch skill, annotate this repository with
<block>tags. Focus on lists that should stay sorted/unique and on code that must stay in sync with docs or config. Add only high-value blocks, then runblockwatchto confirm they all pass.
Review the diff before you commit it: the agent's choices are a starting point.
4. Turn on enforcement so the rules stay in place: wire up the pre-commit hook and GitHub Action from CI Integration.
Features
- Drift Detection: Link a block of code to its documentation. If you change the code but forget the docs, BlockWatch alerts you.
- Strict Formatting: Enforce sorted lists (
keep-sorted) and unique entries (keep-unique) so you don't have to nitpick in code reviews. - Content Validation: Check lines against Regex patterns (
line-pattern) or enforce block size limits (line-count). - AI Rules: Use natural language to validate code or text (e.g., "Must mention 'banana'").
- Lua Scripting: Write custom validation logic in Lua scripts (
check-lua). - Flexible: Run it on specific files, glob patterns, or just your unstaged changes.
Installation
Homebrew (macOS/Linux)
brew install mennanov/blockwatch/blockwatch
The fully-qualified name keeps working once Homebrew starts requiring
explicit trust for third-party taps: it trusts only the
blockwatch formula, not the whole tap. If you install via a Brewfile, use:
brew "mennanov/blockwatch/blockwatch", trusted: true
From Source (Rust)
cargo install blockwatch
Prebuilt Binaries
Check the Releases page for prebuilt binaries.
Quick start example
-
Add a special
blocktag in the comments in any supported file (See Supported Languages) like this:user_ids = [ # <block keep-sorted keep-unique> "cherry", "apple", "apple", "banana", # </block> ] -
Run
blockwatch:blockwatchBlockWatch will fail and tell you that the list is not sorted and has duplicate entries.
-
Fix the order and uniqueness:
user_ids = [ # <block keep-sorted keep-unique> "apple", "banana", "cherry", # </block> ] -
Run
blockwatchagain:blockwatchNow it passes!
How It Works
You define rules using HTML-like tags inside your comments.
Linking Code Blocks (affects)
This ensures that if you change some block of code, you're forced to look at the other blocks too.
src/lib.rs:
// <block affects="README.html:supported-langs">
pub enum Language {
Rust,
Python,
}
// </block>
README.html:
<!-- <block name="supported-langs"> -->
<ul>
<li>Rust</li>
<li>Python</li>
</ul>
<!-- </block> -->
If you modify the enum in src/lib.rs, BlockWatch will fail until you touch the corresponding block supported-langs
in README.html as well.
Enforce Sort Order (keep-sorted)
Keep lists alphabetized. Default is asc (ascending).
# <block keep-sorted>
"apple",
"banana",
"cherry",
# </block>
If the list is not sorted alphabetically, BlockWatch will fail until you fix the order.
Sort by Regex
You can sort by a specific part of the line using a regex capture group named value.
items = [
# <block keep-sorted="asc" keep-sorted-pattern="id: (?P<value>\d+)">
"id: 1 apple",
"id: 2 banana",
"id: 10 orange",
# </block>
]
Numeric Sort (keep-sorted-format)
By default, values are compared lexicographically (as strings). This means "10" sorts before "2" because "1" < "2"
character-by-character. Use keep-sorted-format="numeric" to compare values as numbers instead.
numbers = [
# <block keep-sorted keep-sorted-format="numeric">
2
10
20
# </block>
]
This works with keep-sorted-pattern to extract numeric values from lines with mixed content:
items = [
# <block keep-sorted keep-sorted-format="numeric" keep-sorted-pattern="id: (?P<value>\d+)">
"id: 2 banana",
"id: 10 orange",
"id: 20 apple",
# </block>
]
Without keep-sorted-format="numeric", the example above would fail because "10" is lexicographically less than
"2".
Enforce Unique Lines (keep-unique)
Prevent duplicates in a list.
# <block keep-unique>
"user_1",
"user_2",
"user_3",
# </block>
Uniqueness by Regex
Just like sorting, you can check uniqueness based on a specific regex match.
ids = [
# <block keep-unique="^ID:(?P<value>\d+)">
"ID:1 Alice",
"ID:2 Bob",
"ID:1 Carol", # Violation: ID:1 is already used
# </block>
]
Regex Validation (line-pattern)
Ensure every line matches a specific regex pattern.
slugs = [
# <block line-pattern="^[a-z0-9-]+$">
"valid-slug",
"another-one",
# </block>
]
Enforce Line Count (line-count)
Enforce the number of lines in a block.
Supported operators: <, >, <=, >=, ==.
# <block line-count="<=5">
"a",
"b",
"c"
# </block>
Validate with AI (check-ai)
Use an LLM to validate logic or style.
<!-- <block check-ai="Must mention the company name 'Acme Corp'"> -->
<p>Welcome to Acme Corp!</p>
<!-- </block> -->
Targeted AI Checks
Use check-ai-pattern to send only specific parts of the text to the LLM.
prices = [
# <block check-ai="Prices must be under \$100" check-ai-pattern="\$(?P<value>\d+)">
"Item A: \$50",
"Item B: \$150", # Violation
# </block>
]
Supported environment variables
BLOCKWATCH_AI_API_KEY: API Key.BLOCKWATCH_AI_MODEL: Model name (default:gpt-5-nano).BLOCKWATCH_AI_API_URL: Custom OpenAI compatible API URL (optional).
Validate with Lua Scripts (check-lua)
Run custom validation logic using a Lua script. The script must define a global validate(ctx, content) function that
returns nil if validation passes or a string error message if it fails.
colors = [
# <block check-lua="scripts/validate_colors.lua">
'red',
'green',
'blue',
# </block>
]
scripts/validate_colors.lua:
function validate(ctx, content)
if content:find("purple") then
return "purple is not an allowed color"
end
return nil
end
The validate function receives two arguments:
ctx— a table with the following fields:ctx.file— the source file path.ctx.line— the line number of the block's start tag.ctx.attrs— a table of all block attributes.ctx.affects— only present when the block also has anaffectsattribute. A list (1-based array) of the blocks this block affects, each a table withfile,name, and (trimmed)contentfields. References to blocks that don't exist are skipped.
content— the trimmed text content of the block.
Checking affected blocks (affects + check-lua)
Combining affects with check-lua lets a script inspect the blocks it affects through ctx.affects
— without any file IO, so it works in the default sandboxed mode. This is handy for keeping two blocks
in sync deterministically:
allowed_colors = [
# <block check-lua="scripts/in_sync.lua" affects=":allowed-colors-docs">
'blue',
'green',
'red',
# </block>
]
docs = [
# <block name="allowed-colors-docs">
'blue',
'green',
'red',
# </block>
]
scripts/in_sync.lua:
function validate(ctx, content)
for _, affected in ipairs(ctx.affects) do
if affected.content ~= content then
return "block '" .. affected.name .. "' in " .. affected.file .. " is out of sync"
end
end
return nil
end
Lua safety mode
By default, Lua scripts run in a sandboxed mode with only the coroutine, table, string, utf8, and math
standard libraries available. The io, os, and package libraries are not loaded, preventing file system access,
command execution, and loading of external modules.
You can change the security level by setting the BLOCKWATCH_LUA_MODE environment variable:
# Allow IO and OS libraries (memory-safe, but with file/system access)
BLOCKWATCH_LUA_MODE=safe blockwatch
# Allow all libraries including C module loading (unsafe)
BLOCKWATCH_LUA_MODE=unsafe blockwatch
BLOCKWATCH_LUA_MODE | Libraries available | Security Level |
|---|---|---|
sandboxed (default) | coroutine, table, string, utf8, math | Most secure - No file/OS access |
safe | All memory-safe libraries (including io, os, package) | Memory-safe - Allows file/OS access |
unsafe | All Lua standard libraries with no restrictions (including C modules) | Unsafe - Full system access |
Usage
Run Locally
Validate all blocks in your project:
# Check everything
blockwatch
# Check specific files
blockwatch "src/**/*.rs" "**/*.md"
# Ignore stuff
blockwatch "**/*.rs" --ignore "**/generated/**"
Tip: Glob patterns should be quoted to avoid shell expanding them.
Check Only What Changed
Pipe a git diff to BlockWatch to validate only the blocks you touched. This is perfect for pre-commit hooks.
# Check unstaged changes
git diff --patch | blockwatch
# Check staged changes
git diff --cached --patch | blockwatch
# Check changes in a specific file only
git diff --patch path/to/file | blockwatch
# Check changes and some other (possibly unchanged) files
git diff --patch | blockwatch "src/always_checked.rs" "**/*.md"
Listing Blocks
You can list all blocks that BlockWatch finds without running any validation. This is useful for auditing your blocks or debugging your configuration.
# List all blocks in the current directory
blockwatch list
# List blocks in specific files
blockwatch list "src/**/*.rs" "**/*.md"
# List only blocks affected by current changes (reads the diff from stdin)
git diff | blockwatch list --diff
blockwatch list does not read stdin by default, so it never blocks waiting for
input. This makes it safe to run non-interactively — in CI, or when invoked by
another program such as an AI agent — and to feed its JSON output into a pipe,
e.g. blockwatch list "src/**/*.ts" | jq. Pass --diff to opt in to reading a
unified diff from stdin; list then reports which blocks the diff touched via
the is_content_modified field.
The output is a JSON object.
Example Output
{
"README.md": [
{
"name": "available-validators",
"line": 18,
"column": 10,
"is_content_modified": false,
"attributes": {
"name": "available-validators"
}
}
]
}
CI Integration
Pre-commit Hook
Add this to .pre-commit-config.yaml (pre-commit builds blockwatch from source with cargo on first run):
- repo: https://github.com/mennanov/blockwatch
rev: v0.2.27 # use the latest release tag
hooks:
- id: blockwatch
If you already have the blockwatch binary installed (e.g. via Homebrew), you can use the local form instead:
- repo: local
hooks:
- id: blockwatch
name: blockwatch
entry: bash -c 'git diff --cached --patch --unified=0 | blockwatch'
language: system
stages: [ pre-commit ]
pass_filenames: false
GitHub Action
Add this to .github/workflows/your_workflow.yml:
- uses: mennanov/blockwatch-action@v1
Supported Languages
BlockWatch supports comments in:
- Bash
- C#
- C/C++
- CSS
- Go (with
go.mod,go.sumandgo.worksupport) - HTML
- Java
- JavaScript
- Kotlin
- Makefile
- Markdown
- PHP
- Python
- Ruby
- Rust
- SQL
- Swift
- TOML
- TypeScript
- XML
- YAML
CLI Options
- List Blocks:
blockwatch listoutputs a JSON report of all found blocks. - Extensions: Map custom extensions:
blockwatch -E cxx=cpp - Disable Validators:
blockwatch -d check-ai - Enable Validators:
blockwatch -e keep-sorted - Ignore Files:
blockwatch --ignore "**/generated/**"
Known Limitations
- Deleted blocks are ignored.
- Files with unsupported grammar are ignored.
Contributing
Contributions are welcome! A good place to start is by adding support for a new grammar.
Run Tests
cargo test