Open Code Review custom rules: how to write your rule.json, with real examples
Rules are what turn Open Code Review from a generic reviewer into one that knows your project. They tell the model what to look for in each file: "amounts are in cents", "every route validates its input", "no test only covers the happy path". In this third part of the guide we write a real rule.json for the sample API from part 2, debug it with ocr rules check and show you the ordering mistake that caught us out.
How ocr picks the rule for each file
For every file that passes the filter, ocr looks for a rule in four layers, in this order. The first match wins and the other layers are never consulted:
| Priority | Layer | Where it lives | Typical use |
|---|---|---|---|
| 1 | `--rule` | The file you pass on the CLI | A special review (security only, an audit) |
| 2 | Project | `<repo>/.opencodereview/rule.json` | Team standards. Committed to the repo. |
| 3 | Global | `~/.opencodereview/rule.json` | Your personal preferences across repos |
| 4 | System | Embedded in the binary | Per-language rules: Java, Go, TS/JS, Python, PHP, SQL, YAML… |
A missing layer file is silently skipped. The system layer always exists, so there is always some rule.
The chosen rule is injected into the sub-agent prompt as {{system_rule}}. That is why a good rule makes such a difference: it is the checklist the model reviews that file against.
The rule.json format
It has three fields, all optional:
{
"include": ["src/**/*.test.js"],
"exclude": ["scripts/**"],
"rules": [
{ "path": "src/routes/**/*.js", "rule": "Rule text..." }
]
}include: patterns that bypass the default exclusions (tests, unsupported extensions,dist/…). Careful: it is not an allowlist; files that don't match are still reviewed normally.exclude: patterns that are never reviewed. The highest-priority user filter. Merged with whatever you pass to--exclude.rules: a list of{path, rule}evaluated in order. The first entry whosepathmatches decides the file's rule.
Each rules entry accepts a third field, merge_system_rule. With false (default) your rule replaces the system rule for that file. With true they are combined: the language rule first, then yours.
Patterns use doublestar glob syntax: * (does not cross /), ** (any depth), {ts,tsx} (alternatives), ? and [abc]. Matching is case-insensitive.
A real rule.json, step by step
Our feat/cupones branch adds src/lib/cupones.js (applies a coupon and splits the total into installments), changes an Express route and adds a test. We want three things:
- Code in
src/lib/reviewed with the JavaScript rules plus our money rule. - Routes reviewed with our validation rule (without the generic React rules, which don't apply).
- Tests, which
ocrexcludes by default, reviewed too, with their own rule.

{
"include": ["src/**/*.test.js"],
"exclude": ["scripts/**"],
"rules": [
{
"path": "**/*.test.js",
"rule": "Check that every test covers edge cases (0, 1, negative values), not just the happy path."
},
{
"path": "src/lib/**/*.js",
"rule": "Money: amounts are stored in cents (integers). Flag any decimal arithmetic, any percentage not divided by 100 and any split whose parts don't add up to the total.",
"merge_system_rule": true
},
{
"path": "src/routes/**/*.js",
"rule": "Every Express route must validate req.params and req.body before use, await every async call and return 404 when the resource does not exist."
}
]
}(Our test used the same rules written in Spanish, as in the screenshot; the rule text goes into the prompt verbatim, so any language works.)
Once committed, ocr review --preview lists src/lib/cupones.test.js under "Will review": the include overrode the default test exclusion. .opencodereview/rule.json shows up too, reviewed with the system JSON rule.
Check which rule wins: ocr rules check
ocr rules check <path> tells you which layer and pattern win for a file, and prints the full rule text. It is debugging tool number one:

ocr rules check src/routes/pedidos.js
ocr rules check src/lib/cupones.test.js
ocr rules check src/lib/cupones.jsFor cupones.js, thanks to merge_system_rule: true, the output has two blocks: ## System-Specific Rules (Mandatory) with the JS/TS checklist (typos, dead code, ===, null checks, async handling, XSS, eval, secrets…) and ## User-Specific Rules (Mandatory) with our money rule.
To see what a one-off rule would do without touching the project, pass it with --rule:
echo '{"rules":[{"path":"**/*","rule":"Security only: SQL injection, secrets in code and missing authorization. Ignore style."}]}' > security-only.json
ocr rules check --rule security-only.json src/lib/cupones.js
# Source: Custom (--rule)
# Pattern: **/*
ocr review --from main --to feat/cupones --rule security-only.jsonThe mistake we made: rule order
Our first version had the test rule last, after src/lib/**/*.js. Going from general to specific felt natural. ocr rules check set us straight:

src/lib/cupones.test.js matches src/lib/**/*.js, and since that entry came first, it won. The test rule never applied, with no warning at all. The fix is the rule.json above: most specific first, most general last. And if two of your rules must apply to the same file, write them together as one: merge_system_rule only merges with the system layer, not your rules with each other.
Useful recipes
Global security on top of each language's rules
In ~/.opencodereview/rule.json, for all your repos:
{
"rules": [
{
"path": "**/*",
"rule": "Security review: flag hardcoded secrets, unvalidated redirects, and missing authz checks.",
"merge_system_rule": true
}
]
}Without merge_system_rule, that **/* would replace the Java, Python, Go… rules in every file. With true, each file gets its language rule plus yours. Remember the global layer has the lowest priority: if the project has a matching rule, the global one is skipped.
Ignore generated code and focus on src
{
"include": ["src/**/*.{ts,tsx,js,jsx}"],
"exclude": ["**/*.gen.ts", "**/generated/**", "migrations/**"]
}Backend standards for Go or SQL
{
"rules": [
{
"path": "internal/api/**/*.go",
"rule": "Any handler that opens a transaction must defer tx.Rollback() right after. Wrap errors with fmt.Errorf(\"...: %w\", err)."
},
{
"path": "**/*mapper*.xml",
"rule": "Check for SQL injection (${} instead of #{}), unbound parameters and unclosed XML tags."
}
]
}Docs-in-sync rule
The Open Code Review repo itself uses this technique in its .opencodereview/rule.json: when someone touches internal/llm/providers.go, the rule requires updating the provider table in all four documentation languages and adding a test per new provider. It is a great model for "if you change A, change B too" rules that no linter catches.
Rules and context: --background
Rules say how to review; --background says what the change was meant to do. They complement each other:
ocr review --from main --to feat/cupones \
-b "Discount coupons (percentage 0-100) and installment payments. Amounts in cents."
# or from a requirements file
ocr review --from main --to feat/cupones -B docs/coupons.mdWith context, the model can say "this does not meet the requirement", not just "this has bad style".
Common errors
My rule isn't applied. Run ocr rules check <file>. It is almost always one of these: an earlier entry matches first (the case above), the pattern doesn't cover the depth (src/*.js doesn't enter subfolders; use src/**/*.js), or a --rule overrides the whole project.
A file isn't reviewed even though it has a rule. Rules don't decide whether a file is reviewed; the filter does. Check the reason with ocr review --preview and, if it is default_path or unsupported_ext, add the pattern to include.
I added include and now "it only reviews that". It shouldn't: include is not an allowlist. If you really want to narrow it, use exclude.
include doesn't bring back vendor/. If the folder is at the repo root, it is filtered while reading the diff (provider_directory) and no include recovers it. A nested vendor/ (api/vendor/) can be included.
.env is never reviewed. By design: secret paths are excluded before your rules and cannot be forced.
FAQ
Can I write rules in another language? Yes. The rule text goes into the prompt as is; we wrote ours in Spanish and the model applied them. To get comments in a given language, set ocr config set language <Language>.
Where can I see the system rules? Run ocr rules check on a file of that language (with no matching project rule), or browse internal/config/rules/rule_docs/ in the repo (one Markdown file per language).
Do rules apply in delegation mode too? Yes. ocr delegate rule <files> resolves the same layers and groups files that share a rule. See part 4.
Should I commit .opencodereview/rule.json? Yes, that is the point: the whole team and CI review with the same standards.
Conclusion
A short, well-ordered rule.json is what gets the most out of Open Code Review: specific rules first, merge_system_rule: true when you want to add rather than replace, and include to review what is ignored by default. Before trusting a rule, verify it with ocr rules check.
In part 4 we use these same rules from Claude Code, with no API key, through delegation mode.
// source code
// implementation
More on AI agents

- Skills for your agents
- Claude Code

- Skills for your agents
- Claude Code

- Skills for your agents
- Claude Code