Skip to content

Cookbook

Each pattern below is meant to be copied into a .alint.yml and customized. If you're starting from scratch, Getting Started is a better entry point.

1. One-line baseline from a bundled ruleset

Section titled "1. One-line baseline from a bundled ruleset"

The shortest useful .alint.yml: adopt the OSS-hygiene baseline and nothing else. Good for enforcing README, LICENSE, and no-merge-marker basics on a fresh repository.

version: 1
extends:
- alint://bundled/oss-baseline@v1

2. Compose several bundled rulesets for a specific stack

Section titled "2. Compose several bundled rulesets for a specific stack"

A Rust monorepo wants OSS docs + Rust-idiomatic structure + layout checks + no tracked build artefacts:

version: 1
extends:
- alint://bundled/oss-baseline@v1
- alint://bundled/rust@v1 # Cargo.toml, target/ ban, snake_case
- alint://bundled/monorepo@v1 # every crate has README
- alint://bundled/hygiene/no-tracked-artifacts@v1 # node_modules, target/, .DS_Store…
- alint://bundled/hygiene/lockfiles@v1 # Cargo.lock only at root

Language-specific rulesets (rust, node, python, go) are gated by facts (when: facts.is_<lang>) and silently no-op in projects where they don't apply, so layering them is cheap.

3. Override a bundled rule without restating its body

Section titled "3. Override a bundled rule without restating its body"

Children in an extends: chain only need to declare the fields that change. The inherited kind, paths, pattern, etc. carry over:

version: 1
extends:
- alint://bundled/oss-baseline@v1
rules:
# Turn a warning into a blocking error for this repository:
- id: oss-license-exists
level: error
# Silence a rule that this repository deliberately opts out of:
- id: oss-code-of-conduct-exists
level: off

Unknown-id overrides are flagged at config load, so typos don't silently pass.

4. Adopt only part of a bundled ruleset

Section titled "4. Adopt only part of a bundled ruleset"

When you want most of a bundled ruleset but not all of it, filter at the extends: entry with only: or except: (mutually exclusive). Unknown rule ids in either list are flagged at load time.

version: 1
extends:
# Most of oss-baseline, minus the CoC nag:
- url: alint://bundled/oss-baseline@v1
except: [oss-code-of-conduct-exists]
# Just the pinning check from the CI ruleset, nothing else:
- url: alint://bundled/ci/github-actions@v1
only: [gha-pin-actions-to-sha]

5. Enforce values inside package.json with structured queries

Section titled "5. Enforce values inside package.json with structured queries"

json_path_equals applies a JSONPath query and checks the value. Missing fields are treated as violations (conservative default; scope narrowly if a field is truly optional).

version: 1
rules:
- id: require-mit-license
kind: json_path_equals
paths: "packages/*/package.json"
path: "$.license"
equals: "MIT"
level: error
- id: semver-package-version
kind: json_path_matches
paths: "packages/*/package.json"
path: "$.version"
matches: '^\d+\.\d+\.\d+$'
level: error

6. Lock down GitHub Actions workflows

Section titled "6. Lock down GitHub Actions workflows"

Use yaml_path_equals for the workflow-wide permission value and the consumer-aware pinned_references kind for immutable action, reusable-workflow, composite-action, and container references. If you want the full set without typing them, extends: [alint://bundled/ci/github-actions@v1] ships these rules plus a name: presence check.

version: 1
rules:
# OpenSSF: workflows should declare `permissions.contents: read` explicitly.
- id: workflow-contents-read
kind: yaml_path_equals
paths: ".github/workflows/*.yml"
path: "$.permissions.contents"
equals: "read"
level: error
# OpenSSF: pin external actions and reusable workflows to full commit
# SHAs, and workflow container images to full sha256 digests. Composite
# action manifests are checked too; local `./` references remain exempt.
- id: pin-actions-to-sha
kind: pinned_references
paths:
- ".github/workflows/*.yml"
- ".github/workflows/*.yaml"
- "action.yml"
- "action.yaml"
- "**/action.yml"
- "**/action.yaml"
format: github_actions
level: warning

7. Enforce Cargo manifest shape across a workspace

Section titled "7. Enforce Cargo manifest shape across a workspace"

toml_path_equals / toml_path_matches round out the structured-query family for Rust and Python (pyproject.toml) projects.

version: 1
rules:
- id: rust-edition-2024
kind: toml_path_equals
paths: "crates/*/Cargo.toml"
path: "$.package.edition"
equals: "2024"
level: error
- id: crate-version-follows-semver
kind: toml_path_matches
paths: "crates/*/Cargo.toml"
path: "$.package.version"
matches: '^\d+\.\d+\.\d+(-[\w.-]+)?$'
level: error

8. Monorepo: every package has README + license + non-stub docs

Section titled "8. Monorepo: every package has README + license + non-stub docs"

for_each_dir iterates every directory matching select: and evaluates the nested require: block against each, substituting {path} with the iterated directory. file_min_lines catches the "README is a title plus TODO" case without being pedantic about word count.

version: 1
rules:
- id: every-package-is-documented
kind: for_each_dir
select: "packages/*"
level: error
require:
- kind: file_exists
paths: "{path}/README.md"
- kind: file_min_lines
paths: "{path}/README.md"
min_lines: 5
level: warning
- kind: file_exists
paths: ["{path}/LICENSE", "{path}/LICENSE.md"]
level: warning

9. Nested .alint.yml for subtree-specific rules

Section titled "9. Nested .alint.yml for subtree-specific rules"

Large repos rarely have a single policy. nested_configs: true auto-discovers .alint.yml files in subdirectories and scopes each nested rule's paths / select / primary to the subtree it lives in. The frontend team can own packages/frontend/.alint.yml without waiting on root-config review:

# .alint.yml (repo root)
version: 1
nested_configs: true
extends:
- alint://bundled/oss-baseline@v1
packages/frontend/.alint.yml
version: 1
rules:
- id: components-are-pascal-case
kind: filename_case
paths: "components/**/*.{tsx,jsx}" # auto-scoped to packages/frontend/**
case: pascal
level: error

MVP guardrails: nested rules must declare at least one scope field; absolute paths and ..-prefixed globs are rejected; duplicate rule ids across configs surface with a clear message.

Pair a low-severity rule with a fixer and let alint fix do the boring part. Ideal for pre-commit or editor-save hooks.

version: 1
rules:
- id: trim-trailing-whitespace
kind: no_trailing_whitespace
paths: ["**/*.md", "**/*.rs", "**/*.yml"]
level: info
fix:
file_trim_trailing_whitespace: {}
- id: final-newline
kind: final_newline
paths: ["**/*.md", "**/*.rs", "**/*.yml"]
level: info
fix:
file_append_final_newline: {}
- id: no-bak-files
kind: file_absent
paths: "**/*.{bak,swp,orig}"
level: warning
fix:
file_remove: {}

Preview with alint fix --dry-run; apply with alint fix. Content-editing fixers honour fix_size_limit (default 1 MiB) and skip oversize files rather than rewriting them.

11. Conditional rules gated on repo facts

Section titled "11. Conditional rules gated on repo facts"

Facts are evaluated once per run and referenced in when:. Here: only enforce snake_case Rust filenames when the repo actually is a Rust project.

version: 1
facts:
- id: has_rust
any_file_exists: [Cargo.toml]
- id: has_typescript
any_file_exists: ["tsconfig.json", "packages/*/tsconfig.json"]
rules:
- id: rust-snake-case
when: facts.has_rust
kind: filename_case
paths: "src/**/*.rs"
case: snake
level: error
- id: ts-kebab-case
when: facts.has_typescript and not (facts.has_rust)
kind: filename_case
paths: "src/**/*.ts"
case: kebab
level: warning

11.5. Polyglot monorepo: per-ecosystem rules with closest-ancestor scoping

Section titled "11.5. Polyglot monorepo: per-ecosystem rules with closest-ancestor scoping"

In a monorepo where Rust packages sit under crates/, Node packages under packages/, and Python packages under apps/, you want each ecosystem's hygiene rules to fire only on the files inside that ecosystem's package directories, not on stray .py helpers checked into a Rust crate or vice versa. The scope_filter: primitive (v0.9.6+) handles this declaratively: extend every ecosystem's bundled ruleset and they auto-scope by ancestor manifest.

version: 1
extends:
- alint://bundled/oss-baseline@v1 # tree-wide: README, LICENSE, hygiene
- alint://bundled/rust@v1 # auto-scopes to ancestor-Cargo.toml
- alint://bundled/node@v1 # auto-scopes to ancestor-package.json
- alint://bundled/python@v1 # auto-scopes to ancestor-pyproject.toml/setup.py/requirements.txt

Each bundled ecosystem ruleset since v0.9.6 ships with a scope_filter: on its per-file content rules. rust@v1's rust-sources-no-bidi only fires on .rs files inside an ancestor-Cargo.toml directory, node@v1's node-sources-no-trailing-whitespace only on JS/TS files inside an ancestor-package.json, etc. Tree-wide rules (the existence checks, oss-baseline@v1's LICENSE/README rules) keep their global scope.

To layer a custom rule with the same scoping pattern, declare scope_filter: directly on the rule:

rules:
# Custom: forbid `unwrap()` only inside Rust packages — won't fire on
# a stray `.rs` file checked into a Node package's docs/.
- id: rust-no-unwrap-in-libs
kind: file_content_forbidden
paths: "**/src/**/*.rs"
scope_filter:
has_ancestor: Cargo.toml
pattern: '\.unwrap\(\)'
level: warning

scope_filter: works on per-file rules and, since v0.15, on the rule-major kinds (filename_case, file_max_size, and similar); cross-file rules (pair, for_each_dir, file_exists, and so on) reject it at build time and direct you to the for_each_dir + when_iter: pattern instead.

The has_ancestor: predicate scopes by an ancestor manifest's presence. When the manifest also declares which paths belong — a workspace's members, a package's bin entrypoints — include_manifest_paths: / exclude_manifest_paths: scope by that declaration instead of a hand-maintained glob (v0.15+). See alint and monorepos for worked include / exclude + derive_target examples.

pair and unique_by cover the "every X has a matching Y" and "no two files share a derived key" cases, the ones that ad-hoc shell pipelines usually get wrong on the edges. Template tokens are {path}, {dir}, {basename}, {stem}, {ext}, {parent_name}.

version: 1
rules:
# Every `*.c` source file has a same-directory `*.h` header:
- id: every-c-has-a-header
kind: pair
primary: "src/**/*.c"
partner: "{dir}/{stem}.h"
level: error
# No two Rust source files share a stem anywhere in the repo — a
# frequent mod-path surprise in larger workspaces:
- id: unique-rs-stems
kind: unique_by
select: "**/*.rs"
key: "{stem}"
level: warning

Keep .env keys aligned with .env.example

Section titled "Keep .env keys aligned with .env.example"

Use key_parity when files must declare the same keys as a reference. This rule makes .env.example authoritative and compares key names only, never values:

version: 1
rules:
- id: env-key-parity
kind: key_parity
reference: .env.example
peers: .env
allow_missing_peers: true
level: error

.env is usually gitignored, and a peers path without glob characters is read from disk anyway. allow_missing_peers: true turns an absent .env (in CI, say) into an informational note; drop it where .env must exist. direction: missing reports only keys .env lacks, for setups that allow local-only keys. Findings quote key names, never values. The same shape keeps locale files in step: reference: locales/en.json with peers: "locales/*.json", plus strip_root: true for Rails-style files and plurals: cldr for plural groups.

13. Ban risky characters / files outright

Section titled "13. Ban risky characters / files outright"

The security-family rules catch categories that are almost never intentional. Trojan-Source (CVE-2021-42574), zero-width tricks, and stray merge markers all create changes that do not match an author's visible intent.

version: 1
rules:
- id: no-merge-markers
kind: no_merge_conflict_markers
paths: ["**/*"]
level: error
- id: no-bidi
kind: no_bidi_controls
paths: ["**/*"]
level: error
fix:
file_strip_bidi: {}
- id: no-zero-width
kind: no_zero_width_chars
paths: ["**/*"]
level: error
fix:
file_strip_zero_width: {}
- id: no-committed-env
kind: file_absent
paths: [".env", ".env.*.local"]
level: error

Coding agents (Claude Code, Cursor agent, Copilot agent, Aider, Codex) leave characteristic structural debris: backup-suffix files, scratch / planning docs, debug-print residue, stale TODO(claude:) markers, AI-style affirmation prose. The bundled agent-hygiene@v1 ruleset (shipped in v0.6) catches all of those without overlapping the existing hygiene/* set. Pair it with agent-context@v1 for AGENTS.md / CLAUDE.md / .cursorrules hygiene:

version: 1
extends:
# OS / editor / build / .env junk — covers what agents AND humans leave behind.
- alint://bundled/hygiene/no-tracked-artifacts@v1
- alint://bundled/hygiene/lockfiles@v1
# Agent-specific patterns — versioned duplicates, scratch docs,
# debug residue, AI-affirmation prose, model-attributed TODOs.
- alint://bundled/agent-hygiene@v1
# AGENTS.md / CLAUDE.md / .cursorrules hygiene (existence, stub
# guard, bloat guard, stale-path heuristic). Fact-gated, safe
# no-op when no agent-context file is present.
- alint://bundled/agent-context@v1

Layer with the language-ecosystem rulesets if your stack matches one. They're all when: facts.is_<lang> gated, so extending them costs nothing in projects where they don't apply:

version: 1
extends:
- alint://bundled/oss-baseline@v1
- alint://bundled/hygiene/no-tracked-artifacts@v1
- alint://bundled/agent-hygiene@v1
- alint://bundled/agent-context@v1
- alint://bundled/rust@v1 # or node / python / go / java
- alint://bundled/monorepo@v1 # if multi-package

Feeding violations back to an agent

Section titled "Feeding violations back to an agent"

--format agent (also accepted as --format agentic or --format ai) emits a flat JSON shape optimised for an LLM to act on. Each violation carries an agent_instruction field templated from the rule's message + location + fix availability + policy URL, so an agent loop can read the violation and apply the suggested remediation directly:

Terminal window
alint check --format agent
{
"schema_version": 1,
"format": "agent",
"summary": {
"total_violations": 1,
"by_severity": {"error": 0, "warning": 1, "info": 0},
"fixable_violations": 0,
"passing_rules": 5,
"failing_rules": 1
},
"violations": [
{
"rule_id": "agent-no-console-log",
"severity": "warning",
"file": "src/api.ts",
"line": 42,
"column": 1,
"human_message": "`console.log` / `.debug` / `.trace` left in non-test source. Route through the project logger or remove before merge.",
"agent_instruction": "warning: `console.log` / `.debug` / `.trace` left in non-test source. Route through the project logger or remove before merge. To resolve: edit src/api.ts:42:1.",
"fix_available": false
}
]
}

A typical agent-harness pattern: after each edit, run alint check --format agent, parse the JSON, address the first violation, repeat until empty. The agent_instruction field is intentionally verbose. It's optimised for an LLM to act on without having to re-derive the action from rule_id and human_message separately. See the agent format reference for the full field-by-field shape.

The bundled defaults are deliberately non-blocking on the heuristic checks (info for AI-prose patterns, warning for clean-up debt, error for unambiguous bugs like debugger; in production source). Override per-rule once your team is ready to enforce; field-level override means you only have to declare the field you change:

version: 1
extends:
- alint://bundled/agent-hygiene@v1
rules:
# Promote scratch-doc bans to error before merge, not just warn.
- id: agent-no-scratch-docs-at-root
level: error
# Tighten the affirmation-prose check from info to warning.
- id: agent-no-affirmation-prose
level: warning

When you're writing about agent patterns

Section titled "When you're writing about agent patterns"

Projects that document these patterns (a how-to guide about AI hygiene, an internal style guide that quotes agent stock phrases, etc.) will trip the prose / TODO rules on their own examples. The agent-hygiene@v1 defaults already exclude **/CHANGELOG*, **/ROADMAP*, **/cookbook/**, **/*test*/**, and **/fixtures/** for that reason. If your docs live somewhere else, extend the exclude list. paths.exclude field-overrides the bundled list, so list everything you want excluded:

version: 1
extends:
- alint://bundled/agent-hygiene@v1
rules:
- id: agent-no-affirmation-prose
paths:
include: ["**/*.{rs,ts,tsx,js,jsx,py,go,java,kt,rb,md}"]
exclude:
- "**/*test*/**"
- "**/__tests__/**"
- "**/fixtures/**"
- "**/CHANGELOG*"
- "**/ROADMAP*"
- "**/*.snap"
- "docs/agent-style.md" # your custom doc that quotes the patterns
- "docs/style/**" # or a whole directory