Review Rules
Rules tell kaid what to focus on when reviewing each file. They live in JSON files at three layers, plus an embedded system default that ships with the binary.
Priority chain¶
kaid resolves rules using a four-layer priority chain. For each file path, the layers are tried in order; the first matching pattern wins.
| Priority | Source | Path | Notes |
|---|---|---|---|
| 1 (highest) | --rule flag |
user-specified | CLI override; always wins when supplied. |
| 2 | Project config | <repoDir>/.kaid/rule.json |
Per-project rules — safe to commit. |
| 3 | Global config | ~/.kaid/rule.json |
User-wide preferences. |
| 4 (lowest) | System default | embedded system_rules.json |
Built-in rules covering common languages. |
If a higher-priority layer's file doesn't exist, it's silently skipped —
not an error. So a project that never adds .kaid/rule.json
just falls through to the global / system layers.
The system layer is always present (it ships in the binary), so there is always some rule resolved.
Rule file format (layers 1–3)¶
{
"include": ["src/**/*.{ts,tsx}", "src/**/*.go"],
"exclude": ["**/*.test.ts", "**/generated/**"],
"rules": [
{
"path": "src/api/**/*.go",
"rule": "All exported handlers must validate request bodies before use."
},
{
"path": "**/*mapper*.xml",
"rule": "Check SQL for injection risks, parameter errors, and missing closing tags."
}
]
}
Three independent fields:
include— optional. Glob patterns that bypass built-in default exclude patterns (test-file exclusions — see below). It is not a whitelist: files not matching anyincludepattern still proceed through theunsupported_extanddefault_pathchecks and may still be reviewed.exclude— optional. Glob patterns for files kaid must not review. Highest precedence among user-configured filters.rules— array of{path, rule}entries, evaluated in declaration order. The firstpathwhose glob matches the file determines the prompt kaid sends to the model for that file.
Each rules entry also accepts an optional third field:
merge_system_rule— optional, defaults tofalse. Whenfalse(the default), a matching entry replaces the built-in system rule for that file. Whentrue, the matched system rule is kept and the user rule is combined with it.
{
"rules": [
{
"path": "**/*",
"rule": "Security review: flag hardcoded secrets, unvalidated redirects, and missing authz checks.",
"merge_system_rule": true
}
]
}
With that entry in place, kaid rules check src/main/java/com/example/UserService.java
reports both halves:
$ kaid rules check src/main/java/com/example/UserService.java
Source: Project (.kaid/rule.json)
Pattern: **/*
Rule:
────────────────────────────────────────
## System-Specific Rules (Mandatory)
…contents of java.md…
---
## User-Specific Rules (Mandatory)
Security review: flag hardcoded secrets, unvalidated redirects, and missing authz checks.
────────────────────────────────────────
Two things are worth knowing about how the merge is assembled:
- The system half is resolved per file, not once for the run. The
entry above is a catch-all, yet a
.javafile getsjava.md, a.pyfile getspython.md, an unrecognized extension usesdefault.md. One entry therefore adds your rule on top of the right language rules everywhere, without repeating it per extension. - Either half may be empty. If the system layer resolves to nothing for a file, you get your rule alone; if your rule text is empty, you get the system rule alone. In neither case is the other half replaced by a placeholder.
The field is read from all three user layers — --rule, the project's
.kaid/rule.json, and ~/.kaid/rule.json. Layer
priority is unchanged: the first matching entry still wins, and a
matching layer still shadows the layers below it.
The limitation to keep in mind: merging reaches the system layer
only. If two of your own entries match the same file, the first one
still wins outright — merge_system_rule does not combine them with
each other.
Glob features¶
kaid uses bmatcuk/doublestar/v4
for matching:
*— match any characters except/.**— match across directory boundaries (src/**/*.gocovers any depth).{a,b,c}— brace expansion.*.{ts,tsx,js,jsx}is expanded to four patterns and matched in turn.?— match a single character.[abc]— character class.
Patterns are matched case-insensitively (file path is lowercased before matching). When in doubt, use
kaid rules check <path>to confirm.
How files are filtered¶
The filter is a six-gate algorithm in
internal/agent/selection.go.
For each diff, kaid asks:
binary— Is the file binary? Excluded.-
secret_exclude— Does either path match a built-in secret-path protection? Excluded. This protection runs before user rules and cannot be overridden by anincludepattern; the patterns are listed under Built-in secret paths below. -
user_exclude— Does the path match any userexcludepattern? Excluded. user_include— If the user definedinclude, does the path match? If yes, kept immediately (bypasses theunsupported_extanddefault_pathgates below).unsupported_ext— Is the file extension in the allowlist? Excluded if not.default_path— Does the path match a built-in exclude pattern? Excluded. These cover test files (**/*_test.go,**/*.test.{js,jsx,ts,tsx},**/*_spec.rb, …) and dependency or build-output directories (**/node_modules/**,**/vendor/**,**/target/**, …).
Files that survive all six gates are sent to the LLM, unless the diff
alone exceeds 80% of max_tokens: selectFiles applies that ceiling
after the gates and excludes the file as too_large. It also marks a
file whose new path is /dev/null as deleted; there's no new content
to review. Use kaid review --preview to print the result of this filter
without spending a token.
Built-in secret paths¶
The built-in secret paths are not reviewed (see
internal/config/allowlist/default_secret_patterns.json):
**/.ssh/****/id_rsa**/id_dsa**/id_ecdsa**/id_ed25519**/.netrc**/_netrc**/.npmrc**/.pypirc**/.dockercfg
.env and any .env.* variant is likewise treated as a secret path, except the templates .env.example, .env.sample, and .env.template.
Default path exclusions¶
The built-in exclude list (see
internal/config/allowlist/default_exclude_patterns.json)
covers two groups. Test files across languages, plus test fixtures,
snapshots, and generated code:
**/*_test.go**/src/test/java/**/*.java**/src/test/**/*.{kt,kts}**/*Test.fs**/*.test.{js,jsx,ts,tsx}**/*.spec.{js,jsx,ts,tsx}**/__tests__/****/test/**/*_test.py**/tests/**/*_test.py**/*_test.py**/test_*.py**/*_spec.rb**/spec/**/*_spec.rb**/*Test.java**/*Tests.java**/*_test.rs**/oh_modules/****/*.test.ets**/test/**/*.jl**/test/**/*.hs**/*Spec.hs**/test/**/*.lhs**/*Spec.lhs**/tests/**/*.nim**/tests/**/*.nu**/test/**/*.nu**/*_test.nu**/test_*.nu**/test-*.nu**/tests/**/*.R**/__snapshots__/****/*.snap**/testdata/****/fixtures/****/.ipynb_checkpoints/****/*.generated.***/*.gen.go**/*.pb.go**/*.pb.cc**/*.pb.h**/*Test.swift**/*Tests.swift**/Tests/**/*.swift**/tests/**/*.elm**/vendor/**/*.{jsonnet,libsonnet}**/test/**/*.zig**/*_test.zig**/kitex_gen/**/*.go**/*.capnp.h**/*.capnp.go**/*.capnp.ts**/*_capnp.rs**/*_capnp.py**/test/**/*.ml**/tb_*.{v,sv,vhd,vhdl}**/*_tb.{v,sv,vhd,vhdl}lib/**/*.sol**/*.t.sol**/test/**/*.sol**/tests/**/*.sol**/test/**/*.vy**/tests/**/*.vy
…and dependency or build-output directories:
**/node_modules/****/bower_components/****/vendor/****/target/****/dist/****/__pycache__/**,**/.venv/**,**/site-packages/****/Pods/**,**/Carthage/****/.next/**,**/.nuxt/**,**/.gradle/**,**/.terraform/**, …
**/build/** and **/bin/** are deliberately absent: projects keep
hand-written sources in both.
The same noisy directories are also filtered earlier, at the diff level in
internal/diff/git.go.
That list matches by path prefix, so it catches only a directory at the
repository root. vendor/pkg/x.go never reaches the per-file filter and
is reported as provider_directory; api/vendor/pkg/x.go does reach it and
is reported as default_path. Only the second can be brought back by an
include rule.
To review a file that matches one of these patterns, add
it to the user include list — that overrides the default-path gate.
Rule resolution per file¶
After filtering decides a file will be reviewed, kaid picks the rule text the agent should follow:
- Try
--rule(custom) layer in declaration order. - Try
<repo>/.kaid/rule.jsonin declaration order. - Try
~/.kaid/rule.jsonin declaration order. - Fall back to the embedded system rule layer.
Selected embedded system_rules.json patterns are shown below in relative
matching order:
| Pattern | Rule doc |
|---|---|
**/*.properties |
properties.md — i18n / configuration files. |
**/*{mapper,dao}*.xml |
mapper_dao_xml.md — MyBatis-style mapper SQL. |
**/pom.xml |
pom_xml.md — Maven dependencies. |
**/build.gradle |
build_gradle.md — Gradle dependencies. |
**/package.json |
package_json.md — NPM dependencies / scripts. |
**/Cargo.toml |
cargo_toml.md — Rust manifest. |
**/composer.json |
composer_json.md — Composer dependencies, autoloading, scripts, plugins, and package configuration. |
**/*.{json,json5} |
json.md — generic JSON (also matches .json5). |
.github/workflows/**/*.{yaml,yml} |
github_workflows.md — GitHub Actions workflow YAML. |
.github/**/*.{yaml,yml} |
github_config.md — other .github config YAML. |
**/*.{yaml,yml} |
yaml.md |
**/*.java |
java.md |
**/*.go |
go.md — Go source. |
**/*.{ftl,ftlh,ftlx} |
freemarker.md — FreeMarker templates (SSTI / XSS / null handling). |
**/*.{hbs,mustache} |
handlebars_mustache.md — Handlebars and Mustache templates. |
**/*.{jinja2,j2} |
jinja.md — Jinja templates |
**/*.ets |
arkts.md — ArkTS / HarmonyOS. |
**/*.astro |
astro.md — Astro components and islands. |
**/*.{ts,js,tsx,jsx,mjs,cjs} |
ts_js_tsx_jsx.md |
**/*.{kt,kts} |
kotlin.md |
**/*.{fs,fsi,fsx} |
fsharp.md — F# implementation, signature, and script files. |
**/*.rs |
rust.md |
**/*.R |
r.md |
**/*.{cpp,cc,cxx,hpp,hxx} |
cpp.md |
**/*.c |
c.md |
**/*.{py,pyi,ipynb} |
python.md — Python source. |
**/*.{php,phtml} |
php.md — PHP source and PHP templates. |
**/*.proto |
protobuf.md — Protocol Buffers wire compatibility. |
**/*.po |
po.md — gettext translation source catalogs. |
**/*.pot |
pot.md — gettext template files. |
**/*.{graphql,gql} |
graphql.md — GraphQL schema and operations. |
**/*.prisma |
prisma.md — Prisma schema. |
**/*.jl |
julia.md — Julia source. |
**/*.{tf,tofu,hcl,tfvars} |
terraform.md — Terraform / HCL. |
**/*.bicep |
bicep.md — Bicep (Azure) templates. |
**/*.nu |
nushell.md — Nushell scripts. |
**/*.elm |
elm.md - Elm source. |
**/*.{jsonnet,libsonnet} |
jsonnet.md — Jsonnet configuration templates and libraries. |
**/*.thrift |
thrift.md — Apache Thrift IDL wire compatibility. |
**/*.capnp |
capnp.md — Cap'n Proto schema wire compatibility. |
**/*.{glsl,hlsl,wgsl,metal} |
shader.md — GLSL, HLSL, WGSL, and Metal shaders. |
**/*.{v,sv,vh} |
verilog.md — Verilog and SystemVerilog RTL. |
**/*.{vhd,vhdl} |
vhdl.md — VHDL RTL. |
**/*.m |
matlab.md (or objc.md via content sniffing) |
**/*.mm |
objc.md — Objective-C++ source. |
**/*.sol |
solidity.md — Solidity smart contracts. |
**/*.vy |
vyper.md — Vyper smart contracts. |
**/*.rego |
rego.md — Rego policy (OPA). |
**/*.{ps1,psm1,psd1} |
powershell.md — PowerShell scripts, modules, and data/manifest files. |
| (fallback) | default.md |
The resolved rule body becomes the {{system_rule}} placeholder in the
plan and main task prompts.
Content sniffing for .m files¶
.m is shared by MATLAB and Objective-C. kaid peeks at the file's first
non-blank line to disambiguate: if it looks like Objective-C (e.g. #import,
@implementation, a C-style comment), objc.md is used instead of
matlab.md. When the content cannot be read, resolution falls back to
matlab.md.
Stability note. The sniff heuristic may change between kaid versions. If you need deterministic
.mrouting, set an explicit project-level rule for your.mpaths — project rules always outrank the system layer.
Inspecting which rule wins: kaid rules check¶
$ kaid rules check src/main/java/com/example/UserService.java
File: src/main/java/com/example/UserService.java
Source: System built-in
Pattern: **/*.java
Rule:
────────────────────────────────────────
…contents of java.md…
────────────────────────────────────────
$ kaid rules check --rule custom.json src/main/resources/mapper/UserMapper.xml
File: src/main/resources/mapper/UserMapper.xml
Source: Custom (--rule)
Pattern: **/*mapper*.xml
Rule:
────────────────────────────────────────
…contents of your custom rule…
────────────────────────────────────────
Use this whenever a rule isn't behaving the way you expected — it tells you the layer and the pattern that won.
Recipes¶
Project-level: enforce a coding standard¶
Save as <repo>/.kaid/rule.json and commit:
{
"rules": [
{
"path": "src/api/**/*.go",
"rule": "Every public handler must `defer tx.Rollback()` immediately after starting a transaction."
},
{
"path": "**/*mapper*.xml",
"rule": "Check SQL for injection risks, missing parameter binding, and unclosed XML tags."
}
]
}
Project-level: skip generated code, focus on src¶
With include set, files inside src/ are kept even if they'd otherwise
be dropped by a built-in default exclude pattern (e.g., a test file).
Files outside src/ still go through the normal ext / default checks —
include is a bypass, not a whitelist.
Per-PR override¶
Bypasses both the project and global layers — handy when a single PR needs a totally different review checklist (e.g., security-only review).
Global personal preferences¶
Put them at ~/.kaid/rule.json so every repo on your machine
inherits them:
{
"rules": [
{
"path": "**/*.{ts,tsx,js,jsx}",
"rule": "Always check for unhandled promise rejections; warn on `// eslint-disable` without a reason comment."
}
]
}
Global security rules on top of the built-in language rules¶
A catch-all user rule replaces the built-in per-language rules by
default, so adding one entry for **/* would quietly drop java.md,
python.md and the rest everywhere. Set merge_system_rule to keep
both:
{
"rules": [
{
"path": "**/*",
"rule": "Security review: flag hardcoded secrets, unvalidated redirects, and missing authz checks.",
"merge_system_rule": true
}
]
}
Save it at ~/.kaid/rule.json to apply it to every repo, or
in <repo>/.kaid/rule.json to apply it to one. Because the
system half is resolved per file, each language still gets its own
built-in rules alongside yours — no need to enumerate extensions.
The global file is the lowest of the three user layers. If --rule
or the project's .kaid/rule.json has an entry that matches
the same file, that entry wins and the global one is never reached, so
put the catch-all rule in only one place.
See Also¶
- CLI Reference —
kaid review --rule,--preview, andkaid rules check. - Configuration — config file locations and the layered resolution chain.
- Architecture — how the resolved rule feeds the agent prompt.