Writing rules¶
Rules live in YAML files under rules/ (configurable). src/rulewarden/engine/schema.py is the source of
truth; this page summarises it.
A complete rule¶
version: 1
rules:
- id: R-012 # stable, never reused
title: Use jq, not Python, for JSON in shell scripts
source: CLAUDE.md:L14 # traceability: where the rule came from
severity: required # advisory | required | critical
decidability: deterministic # deterministic | heuristic | undecidable
scope: diff # diff | file | repo | command
applies_to: ["**/*.sh", "**/*.bash"]
exclude: ["vendor/**"]
check:
engine: regex
on: added_lines
pattern: 'python3?\s+-c\s+.*\bjson\b'
message: "Replace the Python one-liner with jq, e.g. jq -r '.key' file.json"
fixtures: # both lists non-empty, or the rule will not load
fail: ["python3 -c 'import json,sys; print(json.load(sys.stdin)[\"k\"])' < f.json"]
pass: ["jq -r '.k' f.json"]
deviations: allowed # allowed | forbidden
tags: [shell, tooling]
Required keys: id, title, severity, decidability, message. An undecidable rule has no check and
no fixtures; it is listed and never evaluated. A heuristic rule must be advisory.
Paths¶
Globs are matched against the repository-relative POSIX path. **/ matches zero or more directories, * does
not cross /, and a pattern without / matches the basename at any depth. applies_to defaults to
everything.
check.on¶
| Value | Evaluated on | Engines |
|---|---|---|
added_lines |
each added line of the diff | regex (default for regex) |
hunks |
each block of consecutive added lines | regex |
file |
the whole new file content | regex, ast, jq, shell, python |
path |
the path and its change status | glob (default for glob), regex |
command |
a shell command the agent is about to run (scope: command) |
regex, shell |
Engines¶
| Engine | Parameters | Notes |
|---|---|---|
regex |
pattern or patterns, flags (i m s x), mode (forbid default, or require with on: file/command), within (all, comments, code), unless, unless_changed |
ReDoS-screened at load; within: comments understands Python and shell comments and docstrings |
glob |
paths or paths_from: protected, mode (forbid or allow), change (added, modified, deleted, renamed), exempt_if_locked, unless_changed |
path policy: protected paths, allowed paths, read-only tests |
ast |
rule: bare-except, broad-except-pass, call (names), import (names), docstring-max-lines (max), function-max-lines (max) |
Python only; a file that does not parse is could not verify |
jq |
filter |
each true or string output is a finding; needs jq on PATH (refused if it resolves inside the repository) |
shell |
argv |
opt-in in .rulewarden/config.yaml; allowlisted binaries only; no shell; see Security |
python |
plugin (hygiene.comment_density, hygiene.restating_comment, answers.literal_lookup, or local:<file>:<function> under .rulewarden/plugins/), options |
local plugins are code and live under a protected path |
unless_changed: [globs] suppresses a finding when another file matching the globs changed in the same
change set. For example, "dependency manifest changed" is allowed when DECISIONS.md changed too.
Fixtures¶
A fixture is file content (for added_lines, hunks and file), a path (for path, optionally
{path: x, status: added}) or a command (for command). Fixtures in fail must produce at least one finding,
and those in pass must produce none. Run them with:
rulewarden test-rules # the configured rule set
rulewarden test-rules --path dir/ # any directory of rule files
After editing¶
Run test-rules, then a human runs rulewarden lock. Until then every check fails closed with RW-LOCK.