pi-lens picks up project-local and user-level rules automatically alongside its built-ins. Drop YAML files in the right directory and they are active on the next file dispatch — no config required.
For the full list of bundled rules per language, see the generated catalogs:
ast-grep_rules_catalog.mdandtree-sitter_rules_catalog.md.
your-project/
rules/
tree-sitter-queries/
typescript/
my-rule.yml ← tree-sitter rule, loaded alongside built-ins
ast-grep-rules/
rules/
my-rule.yml ← ast-grep rule, overrides built-in with same id
Both rule families are fingerprinted by relative path and contents, so in-place edits, renames, additions, and removals are picked up without a restart, even when mtimes are preserved.
A per-file dispatch reads the rule corpus fresh every time: its cache key is a content fingerprint of every rule file the language's rule set is built from, so an edit, addition, or removal is live on the next dispatched file.
Whole-project surfaces — a project scan and a structural search — reuse a corpus fingerprint computed once per turn, so a rule edit lands on the next turn, or immediately if any file is dispatched in between (a dispatch republishes the fresh fingerprint for the rest of that turn). This is what keeps a warm scan at the cost of a string comparison instead of re-reading every rule file on every call.
Rules shared across projects live under the relocatable machine-data directory:
<PI_LENS_HOME>/rules/ (or the platform default pi-lens directory when
PI_LENS_HOME is unset). Use the same tree-sitter-queries/ and
ast-grep-rules/ layout shown above. Precedence is project > user > bundled;
when rule IDs collide, the higher tier wins and the lower definition is
shadowed from execution. User rules are part of the same fingerprints as
project rules, so they follow the same timing above.
<project-root>/rules/tree-sitter-queries/<language>/<rule-id>.yml
Valid <language> directory names: typescript javascript tsx python go rust java csharp kotlin ruby cpp c css php plsql abap cobol
Project rules and built-in rules are merged — both run on every matching file.
Rename the language directory with a -disabled suffix to exclude all rules in it from dispatch (they still load for tests):
rules/tree-sitter-queries/typescript-disabled/
There is currently no per-rule disable mechanism; if you need to suppress one built-in rule, copy the directory, remove the file, and rename appropriately.
See rules/tree-sitter-queries/rule-schema.json for a machine-readable schema (works with the VS Code YAML extension).
| Field | Required | Type | Notes |
|---|---|---|---|
id |
✅ | string | Unique across all rules for this language |
query |
✅ | string (block |) |
Tree-sitter S-expression; capture names use @UPPER_SNAKE |
name |
— | string | Human-readable; defaults to id |
severity |
— | error | warning | info |
Defaults to warning |
category |
— | string | Defaults to "general" |
language |
— | string | Inferred from directory name; override only if the file lives in a shared dir |
message |
— | string | Shown inline; defaults to "Pattern: <id>" |
description |
— | string (block |) |
Extended explanation shown in the detail view |
metavars |
— | string[] | Capture names to surface as evidence; auto-extracted from @VAR patterns if omitted |
predicates |
— | Predicate[] | Native tree-sitter predicates (run in WASM, faster than post-filters) |
inline_tier |
— | blocking | warning | review |
Override dispatch tier for this rule |
defect_class |
— | string | e.g. injection, xss, logic |
confidence |
— | low | medium | high |
|
tags |
— | string[] | e.g. [security, owasp-top-10] |
cwe |
— | string[] | e.g. [CWE-89] |
owasp |
— | string[] | |
has_fix |
— | boolean | Defaults to false |
fix_action |
— | string | Short label for the fix suggestion |
examples |
— | {bad?, good?} |
Code strings shown in docs |
Predicate shape:
predicates:
- type: eq # or: match, any-of
var: "@FUNC_NAME"
value: "dangerousMethod"id: no-sync-fs-in-request
name: Synchronous fs call inside request handler
severity: warning
category: performance
language: typescript
message: "Synchronous fs call blocks the event loop — use the async variant"
query: |
(call_expression
function: (member_expression
object: (identifier) @FS
property: (property_identifier) @METHOD)
(#eq? @FS "fs")
(#match? @METHOD "^(readFileSync|writeFileSync|existsSync|statSync)$"))
metavars:
- FS
- METHOD
has_fix: false
tags:
- performance
- nodejs
examples:
bad: |
const data = fs.readFileSync(path, "utf-8");
good: |
const data = await fs.promises.readFile(path, "utf-8");<project-root>/rules/ast-grep-rules/rules/<language-or-group>/<rule-id>.yml
Rule discovery is recursive and accepts .yml or .yaml. The shared raw-LSP/NAPI precedence is project primary rules, project secondary CodeRabbit rules, bundled native rules, then bundled CodeRabbit rules. A higher-precedence rule shadows a lower-precedence rule with the same id; duplicate IDs within one source layer are blocking configuration errors rather than silently selecting one definition.
See rules/ast-grep-rules/rule-schema.json for a machine-readable schema.
| Field | Required | Type | Notes |
|---|---|---|---|
id |
✅ | string | |
rule |
✅ | RuleCondition | At least one of pattern, kind, regex, has, any, all, not |
language |
— | string | See valid values below |
severity |
— | error | warning | info |
|
message |
— | string | |
note |
— | string (block |) |
Extended guidance shown in the detail view |
fix |
— | string | Suggested replacement |
metadata.weight |
— | number | Priority weight |
metadata.category |
— | string | |
constraints |
— | Record<string, {regex}> | Metavariable regex — KEY: { regex: "..." } narrows what $KEY will match. Supported by the napi engine + the ast-grep CLI/LSP. |
Valid language values: TypeScript JavaScript Python Go Rust Java C Cpp CSharp Kotlin Ruby Php
(Note: PascalCase, unlike tree-sitter directory names which are lowercase.)
RuleCondition fields:
| Field | Notes |
|---|---|
pattern |
Ast-grep pattern syntax. Accepts BOTH a string shorthand (foo($A)) AND the rich object form ({context, selector}) — the rich form matches a specific AST kind inside a syntactic context snippet. Avoid single-metavariable string patterns like $VAR (too broad). |
kind |
AST node kind name |
regex |
Regex match against node text |
has |
Nested condition — node must have a descendant matching (default = direct child) |
any |
Array — node matches if any item matches (OR) |
all |
Array — node matches only if all items match (AND) |
not |
Negation condition |
inside |
Ancestor must match (default = direct parent; pair with stopBy: end to walk all ancestors) |
follows |
Immediately-preceding sibling must match |
precedes |
Immediately-following sibling must match |
stopBy |
neighbor (default — direct parent/child/sibling) or end (walk to root/leaves) |
field |
Field name constraint (e.g. field: name on an import specifier) |
nthChild |
Match only the Nth child of its parent |
All of these are supported by the in-process napi runner (#206) and the ast-grep CLI / LSP — the runner delegates matching to napi's native engine (root.findAll({rule})), the same Rust core the CLI and LSP use.
id: no-process-exit-in-library
language: TypeScript
severity: warning
message: "process.exit() in library code terminates the host process"
note: |
Library code should throw an error or return a result code instead of
calling process.exit(). The caller decides whether to exit.
BAD: process.exit(1)
GOOD: throw new Error("fatal condition")
rule:
pattern: process.exit($CODE)Ast-grep rule files support multiple YAML documents separated by ---:
id: rule-one
language: TypeScript
severity: warning
message: "First rule"
rule:
pattern: somePattern($A)
---
id: rule-two
language: TypeScript
severity: error
message: "Second rule"
rule:
pattern: otherPattern($B)Tree-sitter query files are one rule per file.
Run the built-in type-check against your rules to surface YAML parse errors before committing:
# type-check only (no emit) — errors in rule files show up via the loader path
npx tsc --noEmitFor richer editor feedback (autocomplete, hover docs), point the VS Code YAML extension at the bundled schemas:
// .vscode/settings.json
{
"yaml.schemas": {
"./rules/tree-sitter-queries/rule-schema.json": "rules/tree-sitter-queries/**/*.yml",
"./rules/ast-grep-rules/rule-schema.json": "rules/ast-grep-rules/rules/*.yml"
}
}