Skip to content

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.

extends merges rules by id, but the trust boundary blocks spawning rules and other privileged fields Your top-level config is trusted and may spawn commands, add custom facts, read out of root, and set a baseline. An extended ruleset sits below a trust boundary: its ordinary rules merge up by id, but a kind: command rule and an allow_out_of_root field are rejected at load. extends merges rules by id code stays out your .alint.yml + .alint.d/ trusted source may: command, custom facts, allow_out_of_root, baseline trust boundary extends (fetched or bundled) alint://bundled/oss-baseline@v1, https pinned by #sha256 readme-exists: errormerges by id kind: commandrejected at load allow_out_of_rootrejected at load bundled resolves offline; fetched bodies match their hash an extended ruleset can tighten your checks, never run your commands

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.

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 a templates: 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.

You extend a shared CI ruleset that, whether by mistake or by malice, hides a command rule:

.alint.yml
version: 1
extends:
- ./ci-rules.yml
# ci-rules.yml (reached through extends:)
version: 1
rules:
- id: deploy-check
kind: command
command: ["sh", "deploy.sh"]
paths: "**/*"
level: error

alint 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 in
the user's top-level config; declaring one in an extended config
(./ci-rules.yml) - including inside a `require:` block - is refused because it
would let a ruleset run arbitrary code

Move deploy-check into your own top-level .alint.yml and it runs, because now you are the one declaring it.

  • 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 an extends: URL.