Most of what AIROM detects is data, not code. Detection lives in declarative rule packs: YAML files that pair keyword literals with a regex over classified text regions and a templated claim. The binary ships 98 rules across 47 packs, and you can add, override, or disable any of them without recompiling.

The bright line

A rule pack expresses exactly one shape: keywords + regex over code/string regions + a templated claim. Anything that needs more is a Go detector. Three component kinds are structurally reserved for Go and the assembler, and a rule can never emit them: local-model-file (binary header parsers own it), rag-pipeline (synthesized downstream), and application (the scan root).
The split is enforced, not advisory. The rule compiler rejects a pack that reaches past this envelope at startup — before any file is scanned.

The embedded ruleset

Packs live under rules/<category>/, one file per provider, and are compiled into the binary with go:embed. That makes the default detection vocabulary offline by construction and versioned with each release.

Inspecting the effective ruleset

airom rules list prints the ruleset AIROM will actually run — embedded packs plus any --rules overlays, post-merge — with each rule’s originating layer.
List the effective ruleset
The LAYER column reads embedded for a built-in rule, or the overlay’s file path for a rule an overlay added or overrode. It is the fastest way to answer “which pack is actually producing this finding?”

Overlays: --rules

--rules loads an additional pack file. It is repeatable, and files apply in flag order.
Layer two overlays on top of the embedded set
Merging is by rule ID, with three operations:
New IDs in an overlay must be namespaced by the overlay’s own pack name. A pack declaring pack: mycorp can add mycorp/internal-gateway, not openai/something.
The overlay rule replaces the existing rule wholly. There is no field-level merging: your rule must be complete and passes the same validation.
The rule leaves the effective set entirely. disable is overlay-only, and the target must exist — disabling a rule that isn’t there is an error, never a silent no-op.
Later layers win, and within the --rules list, later files win. A third layer — an OCI-distributed remote registry — is a reserved slot for v2 and pairs with the signing/trust-policy work; it is not implemented.
Verify an overlay landed before trusting a scan: airom rules list --rules mycorp.yaml shows the merged result, and the rule’s LAYER will be your file’s path.

Validating a pack: lint and test

Both commands take exactly one pack file and need no Go toolchain — which is the point. Rule authors are not expected to be Go programmers.
Validates a pack against the lint contract and its fixture coverage: every rule needs at least one positive and one negative fixture case.
Lint a pack
Fixtures are discovered by convention: a pack at rules/models/openai.yaml uses rules/models/testdata/openai/. Failures print with file, line, rule ID, and reason, and the command exits non-zero. Validation is strict by design. An unknown field is a parse error, not a shrug:
And a rule without keywords cannot ship at all:
See Writing Rules for why that rejection exists.

Selecting the rule engine: --select

All rule packs execute inside a single detector whose ID is ruleengine. --select takes a detector selection expression, so you can run only the declarative layer:
Run only the rule engine
airom detectors explain ruleengine shows the compiled selector, and the rule count it carries:
Explain the rule-engine detector
That rules:98 reflects the effective set. Add an overlay with two new rules and airom detectors list --rules mycorp.yaml reports rules:100 — a quick confirmation that your pack was actually loaded.
airom detectors list and airom detectors explain honor --select and --rules, so what they print is what a scan with those same flags will run.

How a rule runs

Compilation happens once at process startup: every layer is parsed and merged, the whole lint contract is validated (any violation aborts startup with the offending pack, rule, and reason), every regex is compiled, and one Aho–Corasick trie is built over every pack’s keywords. Then, per file:
1

Selector gate

Language and size are checked from compiled rule metadata — before any content is read.
2

Region classification

A per-language lexer splits the file into code, comment, and string regions.
3

Keyword prefilter

The trie runs over code + string regions only. No keyword hit means the file is done, at roughly memcpy speed. This eliminates the overwhelming majority of files.
4

Regex execution

Only rules whose keywords hit run their regex, and only within their declared regions.
5

Templating

Matches template into findings: claim, occurrence (with fields from named groups and captured params), and relation claims. The detection method is always source-code-analysis.
Rule findings then flow through the same assembler as every Go detector’s output — identity, dedup, and grouped noisy-OR confidence are the assembler’s job, never the rule’s.

Rules are self-invalidating

The SHA-256 of the effective compiled ruleset participates in the cache namespace:
Any change to any rule — embedded or overlay, add, override, or disable — produces a new namespace, and every previously cached finding becomes structurally invisible to the new configuration. This is why a pack’s version: field is informational: rules-as-data invalidate on content, which eliminates the forgotten-version-bump stale-cache bug.
The flip side is real: editing any rule invalidates the whole cache namespace. That is a deliberate choice of correctness over cache warmth. airom clean removes the scan cache directory outright.

Next

Writing Rules

The full YAML schema, why keywords are mandatory, and the fixture-driven test loop.

CLI Reference

Every command and global flag, including --rules and --select.