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 “we just want README / LICENSE / no merge markers” rigour on a fresh repo.
version: 1extends: - alint://bundled/oss-baseline@v12. 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: 1extends: - 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 rootLanguage-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: 1extends: - alint://bundled/oss-baseline@v1
rules: # Turn a warning into a blocking error for our repo: - id: oss-license-exists level: error
# Silence a rule we've deliberately opted out of: - id: oss-code-of-conduct-exists level: offUnknown-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: 1extends: # 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: 1rules: - 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: error6. Lock down GitHub Actions workflows
Section titled “6. Lock down GitHub Actions workflows”yaml_path_equals for workflow-wide permissions; yaml_path_matches for action-SHA pinning. Both use the same JSONPath engine. YAML is coerced through serde into a JSON value first, so array and wildcard expressions work the same way. If you want the full set without typing them, extends: [alint://bundled/ci/github-actions@v1] ships these rules plus a name: presence check.
if_present: true on the pinning rule means workflows with only run: steps (no uses: at all) are silently OK; the rule only fires on actual matches that fail the regex.
version: 1rules: # 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
# Security practice: pin third-party actions to a full commit SHA, # not a mutable @v4-style tag. `$.jobs.*.steps[*].uses` iterates # every step across every job. `if_present: true` skips workflows # that have no `uses:` at all. - id: pin-actions-to-sha kind: yaml_path_matches paths: ".github/workflows/*.yml" path: "$.jobs.*.steps[*].uses" matches: '^[a-zA-Z0-9._/-]+@[a-f0-9]{40}$' if_present: true level: warning7. 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: 1rules: - 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: error8. 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: 1rules: - 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: warning9. 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: 1nested_configs: trueextends: - alint://bundled/oss-baseline@v1version: 1rules: - id: components-are-pascal-case kind: filename_case paths: "components/**/*.{tsx,jsx}" # auto-scoped to packages/frontend/** case: pascal level: errorMVP 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.
10. Auto-fix hygiene on commit
Section titled “10. Auto-fix hygiene on commit”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: 1rules: - 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: warning11.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.txtEach 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: warningscope_filter: is supported on per-file rules only. 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.
12. Cross-file relationships
Section titled “12. Cross-file relationships”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: 1rules: # 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: warning13. 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 lead to “I didn’t write that” incidents.
version: 1rules: - 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_controls: {}
- 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: error14. Guard an agent-heavy repo
Section titled “14. Guard an agent-heavy repo”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: 1extends: # 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@v1Layer 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: 1extends: - 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-packageFeeding 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:
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.
Severity escalation
Section titled “Severity escalation”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: 1extends: - 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: warningWhen 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: 1extends: - 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