Configuration
.alint.yml is alint's entry point. It declares the rules, the rule sources, the facts they're gated on, and a handful of run-time knobs. alint also reads the configs it extends:, any .alint.d/ drop-ins, per-directory nested configs, and every repository file each rule checks.
The entities of a config and how they relate:
Point your YAML language server at the JSON Schema for editor autocomplete:
# yaml-language-server: $schema=https://alint.org/_alint/configuration/schema.json version: 1extends: - alint://bundled/oss-baseline@v1 The schema is also published in the alint repo at schemas/v1/config.json.
Top-level fields
Section titled "Top-level fields"version
Section titled "version"Config-language version. Complete sources (the root, local/HTTPS/bundled extends: targets, and
nested configs) must declare a supported version. A .alint.d/ drop-in may omit it and inherit the
root version; if present, it must equal the root version.
version: 1Each source is checked before it is merged. A future v2 consumer may read v1 or v2 sources; a v1 consumer never accepts v2.
requires
Section titled "requires"Optional minimum stable alint binary version for this source:
version: 1requires: alint: "0.20.0"The map is closed and alint is a literal MAJOR.MINOR.PATCH triplet. Ranges, prerelease/build
labels, leading zeroes, YAML numbers, and {{env.*}} interpolation are rejected. The loader checks
the requirement before merging the source or following its child extends: entries. It cannot
grant trust or capabilities, and requirements from multiple sources do not override one another:
every source must be satisfied.
extends
Section titled "extends"Configs to inherit from. Resolved left-to-right; later entries override earlier ones; the current file's own definitions override everything it extends.
Each entry is either a bare string or a mapping with url: and optional only: / except: filters:
extends: # Local file (relative to the current .alint.yml): - ./shared/team-defaults.yml
# HTTPS URL with required SHA-256 SRI: - https://example.com/rules.yml#sha256-abc123...
# Bundled ruleset, resolved offline from the binary: - alint://bundled/oss-baseline@v1
# Mapping form, same source kinds, but with filters: - url: alint://bundled/ci/github-actions@v1 only: [gha-pin-actions-to-sha]
- url: alint://bundled/oss-baseline@v1 except: [oss-code-of-conduct-exists]only: and except: are mutually exclusive on a single entry. Listing an unknown rule id is a load-time error.
Bundled and HTTPS configs cannot themselves declare extends:, because relative-path resolution in a fetched body has no principled base. Nest extends locally instead.
ignore
Section titled "ignore"Extra glob patterns to exclude from the walk, on top of .gitignore. Same gitignore-style syntax. Use this for repo-specific exclusions you don't want in .gitignore itself (because they're an alint concern, not a git concern):
ignore: - "vendor/**" - "**/*.snapshot.json" - "fixtures/golden/**" ignore: patterns apply regardless of respect_gitignore. See The walker and .gitignore for what gets filtered by default and how absence-style rules interpret git state.
Because ignore: hides files from every rule, your own included, an https:// extends: entry may declare it only when you list its URL under trusted_extends:; otherwise loading fails with an error naming the URL. Local files and bundled rulesets can still contribute ignore: entries.
respect_gitignore
Section titled "respect_gitignore"Whether to honor .gitignore files (and .git/info/exclude, the global gitignore, and .ignore files) during the walk. Default true.
respect_gitignore: true # default; honor .gitignore# respect_gitignore: false # lint everything on disk regardlessSetting it to false is rarely useful during normal development because absence-style rules (dir_absent, file_absent) start firing on every locally-built artefact (target/, node_modules/, __pycache__/, and so on). It's appropriate for one-off audits or for directories that aren't git repos at all. The CLI's --no-gitignore flag overrides this for one invocation.
The full implications (including how absence-style rules interpret "tracked" vs "ignored" and where this approximation diverges from git's actual index) live in The walker and .gitignore.
Free-form string variables, referenced from when: clauses as bare vars.<name>. To substitute a variable into a rule's own fields (a pattern, a message), use a templates: block, whose {{vars.<name>}} placeholders are filled per instance; a plain, non-template rule does not expand {{vars.<name>}} in its fields.
vars: enforce_headers: "true"
rules: - id: spdx-header kind: file_header paths: "src/**/*.rs" pattern: '^// SPDX-License-Identifier:' when: vars.enforce_headers == "true" # top-level vars feed when: as bare vars.X level: errorProperties of the repo evaluated once per run. Used in when: clauses to gate rules conditionally.
facts: - id: has_rust any_file_exists: [Cargo.toml] - id: n_rs_files count_files: "**/*.rs" - id: has_src_dir all_files_exist: ["src/.keep" ] - id: has_exact_sdk file_content_matches: paths: global.json extract: { json: "$.sdk.version" } pattern: '^[0-9]+\.[0-9]+\.[0-9]+$'
rules: - id: rust-snake-case when: facts.has_rust and facts.n_rs_files > 5 kind: filename_case paths: "src/**/*.rs" case: snake level: errorAvailable fact kinds: any_file_exists, all_files_exist, count_files, file_content_matches, git_branch, custom.
file_content_matches normally applies pattern to raw file content. Its optional extract
map accepts exactly one RFC 9535 JSONPath for json, yaml, toml, xml, dotenv,
properties, ini, or hcl, and applies the pattern to each selected string instead.
Non-string selections, malformed input, and missing paths are non-matches.
Against raw content, ^ and $ anchor to the whole file, not to each line; see
Regex patterns and anchors.
custom (which spawns a subprocess) is a security boundary: it's only allowed in your own top-level config. Any extends: ancestor that declares one is rejected at load time, so a malicious or compromised ruleset can't execute arbitrary code merely by being fetched.
The rules themselves. Each has at least an id, kind, and level. Most have a paths glob; some kinds add their own option fields (e.g. min_lines:, path: + equals: for structured queries). See the Rules section for every kind.
rules: - id: readme-exists kind: file_exists paths: ["README.md", "README", "README.rst"] root_only: true level: error fix: file_create: content: "# Project\n"
- id: no-bidi-controls kind: no_bidi_controls paths: "**/*" level: error policy_url: "https://trojansource.codes/" Common per-rule fields:
id(required): kebab-case identifier. Stable; used to override or disable the rule from a child config.kind(required): which built-in implementation to invoke. Required somewhere in theextends:chain.level(required):error,warning,info, oroff.offdisables the rule entirely.paths: glob, list of globs, or{include, exclude}pair. Required for most kinds.when: bounded expression gating the rule on facts / vars. See Scoping for the gate order.expect_matches(defaultfalse): whentrue, report a violation if the active rule's effective file scope matches nothing (see below).scope_filter: extra per-file scoping by ancestor manifest presence, git diff, or membership in a manifest-declared path set (see below). Cross-file rules reject this field at build time.fix: fix-op declaration (e.g.file_trim_trailing_whitespace: {}).message: override the rule's display message.policy_url: link surfaced when the rule fires.sensitive(defaultfalse): redact this rule's message and fix detail before any report or baseline is exposed. Dotenv/properties value extractors enable the same policy automatically. See below.
sensitive (all rules, v0.19+)
Section titled "sensitive (all rules, v0.19+)"Set sensitive: true when a finding can contain a credential or other secret:
rules: - id: private-registry-token kind: cross_file source: file: private.yml extract: { yaml: "$.registry.token" } targets: - file: generated.yml extract: { yaml: "$.registry.token" } relation: equals sensitive: true level: errorBefore alint returns a public report, it replaces the complete flattened finding message and any
fix-report detail with a stable <redacted:…> marker. Repository paths and source locations remain
visible. Redaction does not change comparison, selection, severity, exit status, or whether a fix
is applicable, but proposed replacement bytes and edits for the affected file are not published.
The baseline stores the marker instead of evidence, and its fingerprint excludes the evidence
bytes; multiple redacted findings with the same structural identity are represented by the
baseline count.
Dotenv and Java properties value extractors apply this policy automatically because those formats
commonly store credentials and tokens. This includes structured-path rules and the relation rules
that share the extractor. Other JSON, YAML, TOML, XML, INI, HCL, regex, lines, and whole-file
extractions remain visible unless sensitive: true is set. Primary repository paths are
structural locations, not extracted evidence, and remain navigable.
Custom message: text is redacted too; do not use it to try to preserve part of a secret-bearing
message. A nested require: can set sensitive: true independently of its parent.
expect_matches (file-scoped rules, v0.18+)
Section titled "expect_matches (file-scoped rules, v0.18+)"Set expect_matches: true when an empty scope means the configuration has drifted rather than that the repository is compliant. Alint checks the effective scope — paths: after every scope_filter: predicate — only after when: evaluates true. If no file remains, the rule emits a normal violation at its configured level, so human, JSON, SARIF, CI annotations, baselines, and exit codes all handle it like any other finding.
rules: - id: llms-copy-has-no-stale-url kind: file_content_forbidden paths: src/data/llms.txt expect_matches: true pattern: 'https://old\.example' level: errorThis assertion always measures the full repository, even when the command uses --changed; the underlying per-file rule still evaluates only changed files. That separation prevents both failure modes: a renamed or deleted target is still caught in an incremental CI run, while a valid target outside the diff does not cause a false empty-scope finding. A false when: gate disables both the rule and its assertion.
By default, the finding identifies the configured scope and whether a scope_filter: removed its remaining candidates. A rule-level message: replaces that text, just as it does for the rule's ordinary findings.
expect_matches is supported on rules with an enumerable file paths: scope, including nested require: rules (where it is checked once per active parent iteration). It is deliberately rejected at config load for file_exists, file_absent, directory-existence rules, and selector/cross-file rules: those kinds already assign their own meaning to an empty set or select through fields other than paths:. Use the existence rule itself for presence, and use the selector kind's own completeness behavior where available.
scope_filter (per-file rules, v0.9.6+)
Section titled "scope_filter (per-file rules, v0.9.6+)"Narrows a per-file rule to files that have a specified manifest somewhere in their ancestor directory chain. The engine walks Path::parent() upward from the file (the file's own directory counts as an ancestor) and consults the file index at each step; first-match-wins on the upward walk gates the rule per-file. Combine with the rule's existing paths:. Both must match for the rule to fire.
rules: - id: rust-sources-no-bidi when: facts.has_rust kind: no_bidi_controls paths: "**/*.rs" scope_filter: has_ancestor: Cargo.toml # single string OR a list level: errorhas_ancestor: accepts a literal filename or a list of filenames; path separators and glob metacharacters are rejected at build time. The bundled ecosystem rulesets (rust@v1, node@v1, python@v1, go@v1, java@v1) use this to scope per-file content rules to their ecosystem's package subtrees in polyglot monorepos.
changed_since: <git-ref> (v0.11+) narrows a per-file rule to files in the <ref>...HEAD diff, the same merge-base diff as alint check --changed. Use it to grandfather pre-existing files in a PR (e.g. require an SPDX header only on files the PR touched):
rules: - id: spdx-on-new-files kind: file_header paths: "src/**/*.rs" pattern: "^// SPDX-License-Identifier:" scope_filter: changed_since: "{{env.ALINT_BASE_SHA | default('origin/main')}}" level: errorIt accepts the {{env.X}} interpolation and resolves the diff once per run. If Git cannot run or the lint root is not a repository, the owning rule reports a resource_read analysis failure; an unresolvable ref remains a hard error with a shallow-clone hint. The scope_filter: predicates AND-compose when more than one is set; at least one of has_ancestor:, changed_since:, include_manifest_paths:, or exclude_manifest_paths: must be present.
include_manifest_paths: / exclude_manifest_paths: (v0.15+) scope a per-file rule by membership in a path set a manifest declares, so the manifest that owns the truth and the rule that depends on it stay in one place instead of a hand-maintained paths.exclude that drifts. exclude_manifest_paths: drops files in the set; include_manifest_paths: keeps only files in it.
Exempt the package.json bin entrypoints from a no-console rule. Even though bin names the build output (dist/cli.js), derive_target: maps it back to source:
rules: - id: no-stray-console kind: file_content_forbidden paths: "src/**/*.ts" pattern: 'console\.(log|debug|info)\(' scope_filter: exclude_manifest_paths: source: package.json # the manifest (always repo-root-confined) extract: { json: "$.bin.*" } # the shared extract one-of derive_target: # optional: map declared output -> source from: '^dist/(.*)\.js$' to: 'src/$1.ts' level: errorOr scope a rule to only the source directories a workspace manifest declares:
rules: - id: rust-hygiene kind: no_trailing_whitespace paths: "**/*.rs" scope_filter: include_manifest_paths: source: Cargo.toml extract: { toml: "$.workspace.members[*]" } level: warningEach predicate takes a source: (the manifest file, always repo-root-confined; its declared paths resolve relative to its own directory), an extract: (the shared { json | toml | yaml: <JSONPath> } / { lines } / { regex } extractor registry_paths_resolve and file_graph also use), an optional derive_target: { from, to } regex mapping applied to each extracted path (a string that does not match from is dropped), and, for include_manifest_paths: only, expect_nonempty: (default true) to warn when a successfully resolved set is empty. A missing, unreadable, malformed, or over-limit source and a structured query selecting a non-string node are typed analysis failures for every active owning rule; they never become an empty set or a fallback to full scope. Non-literal string entries are dropped with an informational note. This guards the declared set; expect_matches: true separately asserts that the final effective scope contains a file.
Membership is directory-aware: a declared file matches itself; a declared directory (a workspace member) matches every file under it, respecting component boundaries (crates/a does not match crates/ab). A glob member is expanded, so members = ["crates/*"] scopes every file under each matching crates/<x> directory. A negated member (!packages/internal, the pnpm / npm workspaces exclusion form) excludes what it matches; members are applied in declared order and the last one matching a file decides. This ordered reading matches npm workspaces. pnpm applies ! entries as excludes regardless of position, so the two agree when exclusions come last, the usual style. Cargo has no ! form in members; its separate exclude list is a second extraction, for example an exclude_manifest_paths predicate over $.workspace.exclude[*]. The set is extracted once per run and cached, like changed_since:. A manifest value gates which files a rule sees, never what it decides about a file: content rules never read the manifest, extraction is pure parsing (no spawn, safe inside an extends:-provided rule), and alint explain <rule> prints the resolved set.
Cross-file rules (pair, for_each_dir, file_exists, etc.) reject scope_filter: at build time with a pointer to the for_each_dir + when_iter: pattern. Rule-major kinds (filename_case, filename_regex, file_max_size, file_min_size, executable_bit, no_empty_files, json_schema_passes, and their siblings) honor scope_filter: too as of v0.15. (Before v0.15 they applied their own paths: glob but silently ignored a changed_since: predicate, so a scope that depended on it matched nothing on them.)
fix_size_limit
Section titled "fix_size_limit"Maximum file size, in bytes, that content-editing fixes will read and rewrite. Files over this limit are reported as Skipped in the fix report and a one-line warning is printed to stderr.
fix_size_limit: 1048576 # 1 MiB; the default# fix_size_limit: null # disable the cap entirely (not recommended)Path-only fixes (file_create, file_remove, file_rename) ignore the cap, since they don't read content.
nested_configs
Section titled "nested_configs"Opt in to discovery of .alint.yml / .alint.yaml files in subdirectories. Default false.
# repo-root .alint.ymlversion: 1nested_configs: trueextends: - alint://bundled/oss-baseline@v1 When true, the loader walks the tree from the root config's directory (respecting .gitignore and ignore:) and picks up every nested config. Each nested rule's path-like scope fields (paths, select, primary) are auto-prefixed with the nested config's relative directory, so the rule scopes to that subtree.
version: 1rules: - id: components-pascal kind: filename_case paths: "components/**/*.{tsx,jsx}" # ↑ evaluates as if it read paths: "packages/frontend/components/**/*.{tsx,jsx}" case: pascal level: errorGuardrails: nested configs may only declare version: and rules:; every nested rule must have at least one scope field; absolute paths and ..-prefixed globs are rejected; rule-id collisions across configs error with a clear message.
Only the user's top-level config may set nested_configs: true. Nested configs themselves cannot spawn further nested discovery (one level of opt-in, intentionally).
allow_out_of_root
Section titled "allow_out_of_root"By default alint confines every config-declared path to the repository root: a rule can never read or resolve a path outside the tree it was pointed at. allow_out_of_root: is a deliberate, top-level-only opt-in to relax that for reads, when a trusted config needs to reference an external file (a shared JSON schema, a manifest in a sibling checkout).
allow_out_of_root: true # every rule may read out-of-root pathsOr scope it to specific rule kinds and/or ids: a rule is permitted if its kind is in kinds or its id is in rules:
allow_out_of_root: kinds: [json_schema_passes, pair_hash] # any rule of these kinds rules: [external-shared-schema] # specific rule idsAbsent or false keeps the secure default (full confinement).
Security. Like the spawning-rule trust gate, allow_out_of_root: is honored only from your own top-level config: any extends:'d ruleset that declares it is a load-time error, so adopting a published ruleset can never grant it out-of-tree reads. It currently applies to the read kinds json_schema_passes (schema_path:), pair_hash (target:), and registry_paths_resolve (source:); a permitted read emits an informational note so the escape is never silent. Resolve/index existence checks stay confined regardless.
baseline
Section titled "baseline"Path to a committed baseline file that grandfathers pre-existing violations, so alint check reports and gates on only new findings. Persisting it here means CI need not pass --baseline on every run.
baseline: .alint-baseline.jsonA --baseline <path> flag overrides this key. There is no silent auto-detect: a baseline suppresses findings only when it is explicitly opted in (via this key or the flag), never because a baseline file merely exists on disk. Write and refresh the file with alint baseline. See Baseline mode for the full workflow, the fingerprinting semantics, and which output formats are baseline-aware.
See also
Section titled "See also"- JSON Schema: authoritative source for option types.
- Rules: every rule kind, organised by family, with per-rule options.
- Regex patterns and anchors: what each kind matches a pattern against, and what
^and$mean there. - Concepts: the rule model, the walker, and how alint runs, explained.
- Scoping: the
when:,paths:, andscope_filter:gates a rule narrows its files through.