The config model
alint is driven by a small declarative language. You describe the checks you want as rules; alint merges them with every other config source in play into one effective config, and then reads your repository to turn that config into a report. The root .alint.yml is the entry point, not the whole story: alint also reads the configs it extends:, any .alint.d/ drop-ins, per-directory nested configs, and of course every repository file each rule checks.
A rule is a record
Section titled “A rule is a record”The atom of the language is the rule record. Three fields are always required: id (a stable kebab-case name), kind (which built-in check to run), and level (error, warning, info, or off). The rest are optional: paths (which files, as a glob, a list, or an {include, exclude} pair), when (an expression that gates the rule on facts), fix (one of twelve repair ops), message, scope_filter, and any fields specific to the kind.
- id: readme-exists # names it (stable; used to override or disable) kind: file_exists # which built-in check when: facts.has_rust # gate: only in a Rust project paths: [README.md] # which path the rule is about level: error # severity message: "README.md is required at the repo root"alint reads those fields in a fixed order, and that order is the pipeline in miniature. when is checked first, against facts computed once per run, so a gated-out rule is dropped before its files are scanned. paths then selects the files, kind runs its check, and level and message shape what lands in the report. The when: expression is a deliberately bounded little language, with boolean logic, comparisons, in, and matches over four namespaces (facts., vars., iter., env.) and no arbitrary code; a missing fact reads as null (falsy), so a rule gated on an absent fact simply never runs.
Around the rules sit the rest of the top-level fields: extends: inherits other configs, vars: and facts: supply values the rules gate and interpolate on, ignore: and respect_gitignore: shape the walk, templates: factor out repeated rule shapes, and a few knobs (fix_size_limit, nested_configs, allow_out_of_root, baseline) tune a run. Only version: 1 is strictly required.
Many sources, one effective config
Section titled “Many sources, one effective config”Most repositories never write every rule by hand. A config is assembled from several sources, and alint merges them into one effective config before anything runs:
extends:inherits from other configs, each entry a local file, anhttps://URL pinned by a SHA-256 hash, or a bundled ruleset resolved offline from the binary (alint://bundled/...). Entries resolve left to right, each overriding the ones before it, and your own file overrides everything it extends. Fetched and bundled configs are leaf nodes: they cannot themselves declareextends:.- Drop-ins (
.alint.d/*.yml) are discovered next to the root config and merged alphabetically; the last one wins. It is the/etc/*.d/pattern for alint: ops layer50-policy.yml, a developer gitignores99-local.yml. - Nested configs (opt in with
nested_configs: true) let a subdirectory carry its own.alint.yml. Its rules are added, with their path scopes automatically prefixed with that subtree, so a rule inpackages/web/.alint.ymlonly ever looks atpackages/web/.
Overrides happen by rule id, field by field: a later layer that re-declares readme-exists with level: error changes only that field and inherits kind, paths, and message from below. (Nested structures like a whole fix: block replace wholesale rather than deep-merging, so you re-state a fix: to change part of it.) The precedence, lowest to highest, is: extended configs (left to right), then your root config, then drop-ins. Nested configs are not an override layer at all: their rules are new, subtree-scoped additions, and a duplicate id anywhere is a load error, never a silent override.
Sources are not equally trusted. Your own .alint.yml and its drop-ins are trust-equivalent: they may declare custom: facts and process-spawning rules (kind: command and its siblings). A config reached through extends:, especially a fetched or bundled one, cannot: spawning rules, custom facts, allow_out_of_root, and baseline are all rejected at load if they arrive that way. Adopting someone else’s ruleset can tighten your checks; it can never make your machine run their commands.
From config to verdicts
Section titled “From config to verdicts”Assembling the config is only the first half. Once the effective config exists, alint validates it against the schema, then evaluates it: it walks the repository a single time (honoring .gitignore and your ignore: globs) into one deterministic, sorted index; computes your facts: once, in order, against that index; drops every rule whose when: is false; and dispatches each rule. Cross-file rules scan the whole index; per-file rules run against each matched file, and every file’s bytes are read at most once no matter how many rules match it. The violations aggregate into one report. See How alint works for that evaluation pipeline in full.
Three interpolation layers thread through both halves, each resolving at a different time: {{env.X}} at config load (from the process environment, in local configs only), {{vars.X}} when a templates: body expands, and {{ctx.X}} per violation, inside a rule’s message.
In practice
Section titled “In practice”A root config sets a rule at warning; a drop-in bumps just its severity:
# .alint.yml (root)version: 1rules: - id: readme-exists kind: file_exists paths: [README.md] level: warning message: "README.md is required at the repo root"# .alint.d/99-local.yml (merged after the root; last layer wins)version: 1rules: - id: readme-exists level: error # same id, so this re-declares just one fieldIn a repo with no README, alint check reports the merged rule:
error readme-exists README.md is required at the repo rootThe drop-in supplied only level; kind, paths, and message came from the root config, and the drop-in’s error won because it is the highest-precedence layer.
Going deeper
Section titled “Going deeper”- Configuration is the field-by-field reference for all twelve top-level fields, every rule field, and the JSON Schema.
- Drop-in configs covers
.alint.d/layering and its trust posture in depth. - Variable interpolation details the three interpolation timings and
{{env.X | default(...)}}. - How alint works traces the assembly-then-evaluation pipeline end to end.