Composition and trust
A config rarely stands alone. extends: pulls in other configs and merges their rules into yours, field by field, keyed on rule id. But the further a config sits from you, the less it is trusted: your own .alint.yml may run commands and reach outside the repo, while anything reached through extends: cannot. Adopting someone else’s ruleset can only ever tighten your checks; it can never make your machine run their code.
How extends merges
Section titled “How extends merges”Each extends: entry names another config, and they resolve left to right: a later entry overrides an earlier one, and your own file overrides everything it extends. Merging is by rule id, field by field, so an entry that re-declares readme-exists with only level: error changes just that field and inherits kind, paths, and message from below. An entry can be a local file, an https:// URL, or a bundled ruleset resolved offline from the binary (alint://bundled/...); only: / except: filters (mutually exclusive on one entry) narrow which rules an entry contributes.
Fetched and bundled configs are leaf nodes: they cannot declare extends: of their own. A fetched body has no principled base for resolving relative paths, and bundled rulesets are deliberately flat catalogs; you nest extends: locally instead.
The trust boundary
Section titled “The trust boundary”Sources are not equally trusted, and the boundary is drawn at extends:. Your own .alint.yml and its .alint.d/ drop-ins are trusted source: they may declare process-spawning rules (kind: command and its siblings), custom: facts (which also spawn), allow_out_of_root:, and baseline:. A config reached through extends: may declare none of these. Each is rejected at load, by name, so adopting a published ruleset can never:
- run code on your machine (a spawning rule, including one smuggled through a
require:block or atemplates:entry, is refused), - read outside the repo (
allow_out_of_root:is a top-level-only grant), or - choose which findings are suppressed (
baseline:is a top-level-only input).
For https:// entries, a SHA-256 subresource-integrity hash (#sha256-...) pins exactly which bytes are trusted; an https:// entry that omits the pin is refused outright, a plain http:// URL is rejected, and a fetched body that does not match its hash is refused. Bundled rulesets ship inside the binary and are resolved offline, so there is nothing to fetch or pin. The hash pins which bytes load, and the trust boundary governs what those bytes may do.
In practice
Section titled “In practice”You extend a shared CI ruleset that, whether by mistake or by malice, hides a command rule:
version: 1extends: - ./ci-rules.yml# ci-rules.yml (reached through extends:)version: 1rules: - id: deploy-check kind: command command: ["sh", "deploy.sh"] paths: "**/*" level: erroralint refuses to load, naming the rule and the offending config rather than running anything:
rule "deploy-check": `kind: command` spawns a process and is only allowed inthe user's top-level config; declaring one in an extended config(./ci-rules.yml) - including inside a `require:` block - is refused because itwould let a ruleset run arbitrary codeMove deploy-check into your own top-level .alint.yml and it runs, because now you are the one declaring it.
Going deeper
Section titled “Going deeper”- Configuration is the field reference for
extends:,allow_out_of_root:, and the rule filters. - Config layering covers drop-ins and nested configs, the other sources that merge into one effective config.
- Variable interpolation details the
#sha256-...pin and how{{env.X}}interacts with anextends:URL.