---
name: harn-rules
short: Structural search, lint rules, and codemods with the Harn rule engine.
description: Use for authoring and running declarative structural rules — patterns, lint diagnostics, and codemods.
when_to_use: Use when writing or running a structural rule, a `harn scan` pattern, or a `harn codemod` fix.
---

# Harn rule engine

Use this skill when matching or rewriting code structurally — a search pattern,
a lint rule, or a codemod — instead of with regex/glob. The engine is the
`harn-rules` crate; it is exposed to Harn as `std/rules` and to the CLI as
`harn scan` (read-only) and `harn codemod` (apply).

Pair it with [[harn-language]] for `.harn` syntax and [[harn-testing]] for
conformance fixtures.

## Start here

- `crates/harn-rules/README.md` is the rule-model reference (matcher forms,
  relational/composite algebra, `where`/`transform`/`fix`, safety tiers).
- `docs/src/cookbooks/rules-engine.md` is the worked-example cookbook
  (`harn scan` / `harn codemod`, authoring a rule).
- A rule is **TOML**; an agent can author and run one entirely from `.harn`
  (via `std/rules`) without recompiling the binary.

## A rule, end to end

- A rule has an identity (`id`, `language`, `severity`, `message`), a `[rule]`
  matcher block, and an optional `fix`.
- **Scalar fields come before `[rule]`** — `[rule]` opens a TOML table, so any
  later top-level scalar would bind to it.
- The rule's kind is derived: a `fix` makes it a **codemod**; a `message` with
  no `fix` is a **lint**; a bare matcher is a **search**.

## Matcher forms (the `[rule]` block)

- `pattern` — a code snippet with `$VAR` metavariable holes (ast-grep style),
  compiled to a tree-sitter query. Operators/keywords match literally
  (`??` ≠ `||`); a repeated `$VAR` unifies; a metavar-free pattern is a
  **literal** pattern (`foo()` matches calls to `foo`, not any call).
- `$VAR:kind` — a **typed placeholder**: bind only nodes of a syntactic class.
  `expr`/`stmt`/`type`/`ident` aliases resolve to the grammar's supertype
  (TypeScript/JS/Python), else use an exact tree-sitter kind. Unknown kind =
  compile error.
- `kind` — a bare tree-sitter node kind. `regex` — a regex over node text.
- **Relational** (each a sub-rule, tuned by `stopBy`/`field`): `inside`
  (ancestor), `has` (descendant), `follows`/`precedes` (siblings).
- **Composite**: `all`/`any` (lists), `not` (a sub-rule), `matches` (a
  `[utils.NAME]` utility rule by id). Every key on a node is ANDed.

## Predicates, rewrite, and safety

- A `where` predicate narrows matches: `regex`, `comparison = { op, value }`,
  a recursive `pattern`, or Harn-only semantic filters
  `resolvesTo = { name, kind, line, column, id }` and `type = "int"`.
  Harn match dictionaries keep text in `captures` and add
  `capture_metadata` with `resolved` / `type` where available.
- `[transform.NAME]` synthesizes a metavar before fixing (`convert = "snake"`,
  `replace = { regex, by }`, `substring = { start, end }`).
- `fix` interpolates `$VAR` / `${VAR}` (`$$` → `$`) and splices
  format-preserving edits.
- `safety` (least→most dangerous): `format-only` → `behavior-preserving` →
  `scope-local` (default) → `surface-changing` → `capability-changing` →
  `needs-human`. The two safest are **machine-applicable**; the rest are
  **suggestions** (opt-in). `auto_apply` refuses anything riskier;
  `apply_checked` also fails a non-idempotent fix.

## From the CLI

- `harn scan '<pattern>' <paths> --lang <lang>` — structural search; also
  `--rule <file>` / `--rule-pack <pack>`, `--report-only` (per-file counts),
  `--json`.
- `harn codemod --rule <file> <paths>` — **dry-run by default** (a unified diff
  per file with safety + idempotency); `--apply` writes (capability-gated),
  `--allow-unsafe` applies above machine-applicable safety, `--json`.
- `harn lint <paths>` — runs project lint rules. Declarative `language = "harn"`
  rules and imperative `*.lint.harn` modules in `[rules] ruleDirs` merge into
  the normal output. A script rule exports `lint(source)` and returns a
  `{message, severity?, line?, column?}` finding, a list of findings, or the
  `rules_diagnostics(...)` result. `.harn` rules run read-only and fail safe.
  Trusted native Rust rule libraries can also be loaded from
  `[rules] nativeRuleDirs`; they export `harn_native_lint_register_v1` and emit
  the same diagnostic/fix shape as built-ins.

## From `.harn` (`std/rules`)

- `rules_search({rule, source|paths, language})` → matches with capture
  bindings. `rules_report(...)` → a report-only data table (counts + rows).
- `rules_diagnostics(...)` → declarative diagnostics. `rules_apply({..., dry_run,
  allow_unsafe})` → codemod (gated; `dry_run` by default).
- `rules_visit({rule, ..., on_match: fn(node, ctx) { ... }})` — the imperative
  escape hatch: the visitor returns its report(s) (`nil`/`false` to skip,
  `true` to flag, a `{message, fix, safety, severity}` dict, or a list).

## Gotchas

- Harn source is supported: pass `--lang harn`, set `language = "harn"`, or let
  `.harn` paths resolve from their extension.
- `rules_apply` is a gated deterministic tool: call
  `hostlib_enable("tools:deterministic")` before it (even for a dry run).
- Format every rule run / fixture with both `cargo fmt` and `harn fmt`.
