Examples
Problems repositories run into, grouped. Open one to see the rule that catches it, what alint prints and the fix.
Files that should never be committed
Secrets, build output and large binaries. Most of these rules come from the hygiene/no-tracked-artifacts ruleset.
A .env file or a private key gets committed Environment files containing real values must not be committed. Use .env.example (or similar) for shared non-secret defaults.Rules: hygiene/no-tracked-artifacts, git_no_denied_paths2 findings
version: 1
extends:
- url: alint://bundled/hygiene/no-tracked-artifacts@v1
only: [hygiene-no-env-files]
rules:
- id: no-private-keys
kind: git_no_denied_paths
denied: ["*.pem", "*.p12", id_rsa, id_ed25519]
level: error
message: "Private keys must not be committed"alint check.env ✗ error hygiene-no-env-files Environment files containing real values must not be committed. Use `.env.example` (or similar) for shared non-secret defaults. deploy/staging.pem ✗ error no-private-keys Private keys must not be committed.env ✗ error hygiene-no-env-files Environment files containing real values must not be committed. Use `.env.example` (or similar) for shared non-secret defaults. deploy/staging.pem ✗ error no-private-keys Private keys must not be committedThe rule leaves .env.example alone whatever it holds; the .env.example example further down checks its values.
Remove both files and rotate what they held: a pushed secret stays in the history.
Build output, node_modules or .DS_Store gets committed node_modules/ must not be committed. Add it to .gitignore.Rules: hygiene/no-tracked-artifacts3 findings
version: 1
extends:
- alint://bundled/hygiene/no-tracked-artifacts@v1 alint check.DS_Store ✗ error hygiene-no-macos-junk macOS Finder metadata must not be committed. dist ⚠︎ warning hygiene-no-js-build-outputs directory is forbidden (matches [**/dist, **/build, **/out, **/.next, **/.nuxt, **/.svelte-kit, **/.turbo, **/coverage]): dist node_modules ✗ error hygiene-no-node-modules `node_modules/` must not be committed. Add it to .gitignore..DS_Store ✗ error hygiene-no-macos-junk macOS Finder metadata must not be committed. dist ⚠︎ warning hygiene-no-js-build-outputs directory is forbidden (matches [**/dist, **/build, **/out, **/.next, **/.nuxt, **/.svelte-kit, **/.turbo, **/coverage]): dist node_modules ✗ error hygiene-no-node-modules `node_modules/` must not be committed. Add it to .gitignore.alint fix deletes .DS_Store. For dist/ and node_modules/, run git rm -r --cached and add them to .gitignore.
alint fix 2 more findings error [hygiene-no-macos-junk]: · .DS_Store - macOS Finder metadata must not be committed. (suggested: remove the violating file (requires --unsafe-fixes)) 0 applied, 0 skipped, 2 unfixable, 1 suggested. 2 more findings error [hygiene-no-macos-junk]: · .DS_Store - macOS Finder metadata must not be committed. (suggested: remove the violating file (requires --unsafe-fixes)) 0 applied, 0 skipped, 2 unfixable, 1 suggested.A 5 MB video gets committed assets/intro.mp4: file exceeds 1048576 byte(s) (actual: 5300000)Rules: file_max_size
- id: no-large-files
kind: file_max_size
paths: "**"
max_bytes: 1048576
level: erroralint checkassets/intro.mp4 ✗ error no-large-files file exceeds 1048576 byte(s) (actual: 5300000)assets/intro.mp4 ✗ error no-large-files file exceeds 1048576 byte(s) (actual: 5300000)Move it to Git LFS or a release asset. If big files belong in one directory, leave it out of the rule: paths: { include: "**", exclude: "media/**" }.
Monorepos
Rules that run once per package, crate or workspace member. The monorepo guide covers the workspace rulesets and sharing one config across repositories.
A new package lands without a README packages/emails has no README.mdRules: for_each_dir, file_exists2 findings
- id: package-readme
kind: for_each_dir
select: "packages/*"
require:
- kind: file_exists
paths: "{path}/README.md"
message: "{path} has no README.md"
level: erroralint checkpackages/emails ✗ error package-readme packages/emails has no README.md packages/web ✗ error package-readme packages/web has no README.mdpackages/emails ✗ error package-readme packages/emails has no README.md packages/web ✗ error package-readme packages/web has no README.mdThe monorepo ruleset has this rule for packages, crates, apps and services.
- shop/
- .alint.yml✓
- package.json✓
- packages/
- packages/api/
- packages/api/README.md✓
- packages/api/package.json✓
- packages/api/src/
- packages/api/src/index.ts✓
- packages/emails/
error package-readme
packages/emails has no README.md
- packages/emails/package.json✓
- packages/emails/src/
- packages/emails/src/index.ts✓
- packages/web/
error package-readme
packages/web has no README.md
- packages/web/package.json✓
- packages/web/src/
- packages/web/src/main.tsx✓
Packages disagree on the license field packages/emails/package.json: value at path does not equal expected: expected "MIT", got "ISC"Rules: json_path_equals
- id: packages-are-mit
kind: json_path_equals
paths: "packages/*/package.json"
path: "$.license"
equals: "MIT"
level: erroralint checkpackages/emails/package.json ✗ error packages-are-mit value at path does not equal expected: expected "MIT", got "ISC"packages/emails/package.json ✗ error packages-are-mit value at path does not equal expected: expected "MIT", got "ISC"json_path_matches takes a regex instead of one value.
Set "license": "MIT" in packages/emails/package.json, or change equals if ISC was on purpose.
The workspace lists a crate that's gone Cargo.toml: entry "crates/legacy-api" does not resolve to a directory on diskRules: registry_paths_resolve
- id: workspace-members-exist
kind: registry_paths_resolve
source: Cargo.toml
extract: { toml: "$.workspace.members[*]" }
expect: dir
must_contain: Cargo.toml
level: erroralint checkCargo.toml ✗ error workspace-members-exist Cargo.toml: entry "crates/legacy-api" does not resolve to a directory on disk related: declared target: crates/legacy-apiCargo.toml ✗ error workspace-members-exist Cargo.toml: entry "crates/legacy-api" does not resolve to a directory on disk related: declared target: crates/legacy-apiWith must_contain, a member directory without a Cargo.toml fails too.
Take crates/legacy-api out of members, or restore the crate.
One package imports another package's internals packages/web/src/checkout.ts: forbidden import "@shop/api/src/db/client" at this scope (matches /^@shop/[^/]+/src//)Rules: import_gate
import { getCart } from '@shop/api' ;
import { db } from '@shop/api/src/db/client' ;
export const checkout = () => [getCart(), db];- id: no-deep-imports
kind: import_gate
paths: "packages/*/src/**/*.ts"
language: js
forbid: "^@shop/[^/]+/src/"
level: erroralint checkpackages/web/src/checkout.ts ✗ error no-deep-imports 2:1 forbidden import "@shop/api/src/db/client" at this scope (matches /^@shop/[^/]+/src//)packages/web/src/checkout.ts ✗ error no-deep-imports 2:1 forbidden import "@shop/api/src/db/client" at this scope (matches /^@shop/[^/]+/src//)For layering rules across the whole repository, file_graph takes forbidden_edges.
Import from @shop/api, and export db from its entry point if other packages need it.
Files that belong together
One file's existence or contents depends on another's. These are cross-file rules.
A migration has no down file migrations/2026-08-14-093000_create_orders/up.sql has no matching partner at migrations/2026-08-14-093000_create_orders/down.sql (template: {dir}/down.sql)Rules: pair
- id: migration-has-down
kind: pair
primary: "migrations/*/up.sql"
partner: "{dir}/down.sql"
level: erroralint checkmigrations/2026-08-14-093000_create_orders/up.sql ✗ error migration-has-down migrations/2026-08-14-093000_create_orders/up.sql has no matching partner at migrations/2026-08-14-093000_create_orders/down.sql (template: {dir}/down.sql) related: expected partner: migrations/2026-08-14-093000_create_orders/down.sqlmigrations/2026-08-14-093000_create_orders/up.sql ✗ error migration-has-down migrations/2026-08-14-093000_create_orders/up.sql has no matching partner at migrations/2026-08-14-093000_create_orders/down.sql (template: {dir}/down.sql) related: expected partner: migrations/2026-08-14-093000_create_orders/down.sql- api/
- .alint.yml✓
- migrations/
- migrations/2026-08-01-120000_create_users/
- migrations/2026-08-01-120000_create_users/down.sql✓
- migrations/2026-08-01-120000_create_users/up.sql✓
- migrations/2026-08-14-093000_create_orders/
- migrations/2026-08-14-093000_create_orders/up.sql
error migration-has-down
migrations/2026-08-14-093000_create_orders/up.sql has no matching partner at migrations/2026-08-14-093000_create_orders/down.sql (template: {dir}/down.sql)
- migrations/2026-09-02-161500_index_orders/
- migrations/2026-09-02-161500_index_orders/down.sql✓
- migrations/2026-09-02-161500_index_orders/up.sql✓
Write the down migration for create_orders, or remove the rule for migrations that can only go forward.
Proto files import each other in a cycle dependency cycle (3 files): proto/billing.proto → proto/orders.proto → proto/users.proto → proto/billing.protoRules: file_graph
- id: no-proto-import-cycles
kind: file_graph
nodes: "proto/**/*.proto"
edges:
from_content:
extract: { regex: 'import\s+"([^"]+)"' }
resolve: relative_to_repo_root
require: acyclic
level: erroralint checkproto/billing.proto ✗ error no-proto-import-cycles dependency cycle (3 files): proto/billing.proto → proto/orders.proto → proto/users.proto → proto/billing.proto related: cycle member: proto/orders.proto related: cycle member: proto/users.protoproto/billing.proto ✗ error no-proto-import-cycles dependency cycle (3 files): proto/billing.proto → proto/orders.proto → proto/users.proto → proto/billing.proto related: cycle member: proto/orders.proto related: cycle member: proto/users.protorequire also takes no_orphans, no_dangling and forbidden_edges.
Move the message the three files share into a file they all import.
Two manifests disagree on the version npm/package.json value "1.3.2" != Cargo.toml value "1.4.0"Rules: cross_file
- id: npm-version-matches-crate
kind: cross_file
source:
file: Cargo.toml
extract: { toml: "$.package.version" }
targets:
files: npm/package.json
extract: { json: "$.version" }
level: erroralint checknpm/package.json ✗ error npm-version-matches-crate npm/package.json value "1.3.2" != Cargo.toml value "1.4.0" related: canonical source: Cargo.tomlnpm/package.json ✗ error npm-version-matches-crate npm/package.json value "1.3.2" != Cargo.toml value "1.4.0" related: canonical source: Cargo.tomlSet the npm package to 1.4.0, or have the release script write both files.
CI workflows
Both rules come from the ci/github-actions ruleset. only: takes the rules you want from it.
A workflow runs actions by tag, not by commit .github/workflows/ci.yml: External action, workflow, or container is not pinned to an immutable commit or digest. Pin Actions with @<full-commit-sha> and images with @sha256:<64-hex-digest> so a compromised tag cannot silently change what runs. Local ./ references are allowed.Rules: ci/github-actions
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- run: npm testversion: 1
extends:
- url: alint://bundled/ci/github-actions@v1
only: [gha-pin-actions-to-sha]Whoever controls a tag can move it. A commit SHA pins the action's own code.
alint check.github/workflows/ci.yml ⚠︎ warning gha-pin-actions-to-sha 7:15 External action, workflow, or container is not pinned to an immutable commit or digest. Pin Actions with `@<full-commit-sha>` and images with `@sha256:<64-hex-digest>` so a compromised tag cannot silently change what runs. Local `./` references are allowed. docs: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions.github/workflows/ci.yml ⚠︎ warning gha-pin-actions-to-sha 7:15 External action, workflow, or container is not pinned to an immutable commit or digest. Pin Actions with `@<full-commit-sha>` and images with `@sha256:<64-hex-digest>` so a compromised tag cannot silently change what runs. Local `./` references are allowed.alint reports these per file: one finding for each unpinned action, without its line number.
Pin it to the commit its tag points at and keep the tag in a comment, the way setup-node is pinned here. Dependabot and Renovate update pinned actions.
A workflow's token can push to the repository .github/workflows/ci.yml: Workflow grants (or defaults to) write access to the GITHUB_TOKEN. Restrict it: declare permissions: contents: read (or read-all, or {}) at the workflow level, or set per-job permissions:, so a compromised step can't push to the repo.Rules: ci/github-actions
version: 1
extends:
- url: alint://bundled/ci/github-actions@v1
only: [gha-workflow-contents-read]Without a permissions: block, the workflow gets the default token scope, which can include write access.
alint check.github/workflows/ci.yml ⚠︎ warning gha-workflow-contents-read Workflow grants (or defaults to) write access to the GITHUB_TOKEN. Restrict it: declare `permissions: contents: read` (or `read-all`, or `{}`) at the workflow level, or set per-job `permissions:`, so a compromised step can't push to the repo. docs: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token.github/workflows/ci.yml ⚠︎ warning gha-workflow-contents-read Workflow grants (or defaults to) write access to the GITHUB_TOKEN. Restrict it: declare `permissions: contents: read` (or `read-all`, or `{}`) at the workflow level, or set per-job `permissions:`, so a compromised step can't push to the repo.Add a read-only default at the top of the workflow, and give a job more only where it needs it:
permissions:
contents: readValues in config files
A field in a JSON, YAML, TOML, XML, HCL, INI, .properties or .env file has to equal a value, match a pattern or be absent. These are structured-query rules.
A crate is still on edition 2021 crates/core/Cargo.toml: value at path does not equal expected: expected "2024", got "2021"Rules: toml_path_equals
- id: crates-use-edition-2024
kind: toml_path_equals
paths: "crates/*/Cargo.toml"
path: "$.package.edition"
equals: "2024"
level: erroralint checkcrates/core/Cargo.toml ✗ error crates-use-edition-2024 value at path does not equal expected: expected "2024", got "2021"crates/core/Cargo.toml ✗ error crates-use-edition-2024 value at path does not equal expected: expected "2024", got "2021"Run cargo fix --edition in crates/core, then set edition = "2024".
A Terraform module accepts any future Terraform version infra/database/main.tf: value at path ">= 1.0" does not match regex ^~> 1\.Rules: hcl_path_matches
terraform {
required_version = ">= 1.0"
}- id: terraform-version-bounded
kind: hcl_path_matches
paths: "infra/*/main.tf"
path: "$.terraform.required_version"
matches: "^~> 1\\."
level: error>= 1.0 also accepts Terraform 2.0. This repository uses ~> constraints on the 1.x line.
alint checkinfra/database/main.tf ✗ error terraform-version-bounded value at path ">= 1.0" does not match regex ^~> 1\.infra/database/main.tf ✗ error terraform-version-bounded value at path ">= 1.0" does not match regex ^~> 1\.Change the constraint to ~> 1.9.
.env.example gives AWS_SECRET_ACCESS_KEY a value AWS_SECRET_ACCESS_KEY has a value in .env.example; leave it emptyRules: dotenv_path_matches
DATABASE_URL=postgres://localhost:5432/shop
AWS_REGION=eu-west-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY - id: no-secret-in-env-example
kind: dotenv_path_matches
paths: ".env.example"
path: "$.AWS_SECRET_ACCESS_KEY"
matches: "^$"
if_present: true
level: error
message: "AWS_SECRET_ACCESS_KEY has a value in .env.example; leave it empty"People copy .env.example to .env to get started, and a value in it spreads with every copy.
alint check.env.example ✗ error no-secret-in-env-example AWS_SECRET_ACCESS_KEY has a value in .env.example; leave it empty.env.example ✗ error no-secret-in-env-example AWS_SECRET_ACCESS_KEY has a value in .env.example; leave it emptyThe rule passes an empty AWS_SECRET_ACCESS_KEY= and a file without the key. Keep its message:: without one, alint's default message quotes the value, and a real key would end up in CI logs.
Empty the value and add a comment that says where the key comes from. If it was ever a real key, rotate it.
Names and contents
Each of these three rules has a fixer, so alint fix does the work. More in the content and naming rules.
A source file is missing its license header src/cart.rs: Every source file starts with the SPDX lineRules: file_header
- id: spdx-header
kind: file_header
paths: "src/**/*.rs"
pattern: "^// SPDX-License-Identifier: MIT"
lines: 1
level: error
message: "Every source file starts with the SPDX line"
fix:
file_prepend:
content: "// SPDX-License-Identifier: MIT \n\n"alint checksrc/cart.rs ✗ error spdx-header fixable 1:1 Every source file starts with the SPDX linesrc/cart.rs ✗ error spdx-header fixable 1:1 Every source file starts with the SPDX linealint fixerror [spdx-header]: ✓ prepended src/cart.rs 1 applied, 0 skipped, 0 unfixable.error [spdx-header]: ✓ prepended src/cart.rs 1 applied, 0 skipped, 0 unfixable.alint check✓ All 1 rule(s) passed.✓ All 1 rule(s) passed.A file hides invisible bidi characters docs/contributors.md: Unicode bidi control U+202E at line 4 col 8 (Trojan-Source defense)Rules: no_bidi_controls
- id: no-bidi-controls
kind: no_bidi_controls
paths: ["**/*.md" , "**/*.js" ]
level: error
fix:
file_strip_bidi: {}Bidi control characters change the order text is displayed in, so a reviewer can see something different from what the file holds (the Trojan Source attack). This one sits in a contributor name.
alint checkdocs/contributors.md ✗ error no-bidi-controls fixable 4:8 Unicode bidi control U+202E at line 4 col 8 (Trojan-Source defense)docs/contributors.md ✗ error no-bidi-controls fixable 4:8 Unicode bidi control U+202E at line 4 col 8 (Trojan-Source defense)oss-baseline has this rule for Markdown, text and config files; source files need their own paths, like the ones here.
alint fixerror [no-bidi-controls]: ✓ stripped bidi controls from docs/contributors.md 1 applied, 0 skipped, 0 unfixable.error [no-bidi-controls]: ✓ stripped bidi controls from docs/contributors.md 1 applied, 0 skipped, 0 unfixable.alint check✓ All 1 rule(s) passed.✓ All 1 rule(s) passed.Python module names aren't snake_case src/shop/OrderService.py: Python module names are snake_case (PEP 8)Rules: filename_case2 findings
- id: module-names
kind: filename_case
paths: "src/**/*.py"
case: snake
level: error
message: "Python module names are snake_case (PEP 8)"
fix:
file_rename: {}alint checksrc/shop/OrderService.py ✗ error module-names fixable Python module names are snake_case (PEP 8) src/shop/payment-gateway.py ✗ error module-names fixable Python module names are snake_case (PEP 8)src/shop/OrderService.py ✗ error module-names fixable Python module names are snake_case (PEP 8) src/shop/payment-gateway.py ✗ error module-names fixable Python module names are snake_case (PEP 8)Update the imports that used the old names; alint fix renames only the files.
alint fixerror [module-names]: ✓ renamed src/shop/OrderService.py -> src/shop/order_service.py ✓ renamed src/shop/payment-gateway.py -> src/shop/payment_gateway.py 2 applied, 0 skipped, 0 unfixable.error [module-names]: ✓ renamed src/shop/OrderService.py -> src/shop/order_service.py ✓ renamed src/shop/payment-gateway.py -> src/shop/payment_gateway.py 2 applied, 0 skipped, 0 unfixable.alint check✓ All 1 rule(s) passed.✓ All 1 rule(s) passed.Commits and pull requests
These compare the branch with origin/main, so CI needs that history (fetch-depth: 0 with actions/checkout). More in the git rules.
A commit subject doesn't follow Conventional Commits commit 097d4cf: commit message does not match pattern ^(feat|fix|docs|refactor|test|chore)(\(.+\))?!?: (subject: "fixed stuff")Rules: git_commit_message
- id: conventional-commits
kind: git_commit_message
pattern: "^(feat|fix|docs|refactor|test|chore)(\\(.+\\))?!?: "
subject_max_length: 72
since: origin/main
level: erroralint checkRepository-level ✗ error conventional-commits commit 097d4cf: commit message does not match pattern `^(feat|fix|docs|refactor|test|chore)(\(.+\))?!?: ` (subject: "fixed stuff")Repository-level ✗ error conventional-commits commit 097d4cf: commit message does not match pattern `^(feat|fix|docs|refactor|test|chore)(\(.+\))?!?: ` (subject: "fixed stuff")The rule checks every commit between origin/main and HEAD, not just the last one. Merge commits are skipped unless you set include_merges.
Reword the commit: git rebase -i origin/main, then mark it reword.
A file-format change ships without a version bump the changeset origin/main...HEAD changes a path matching src/format.rs but no path matching FORMAT_VERSION changed with itRules: pair_changed_together
- id: format-version-bumped
kind: pair_changed_together
if_changed: "src/format.rs"
then_changed: "FORMAT_VERSION"
since: origin/main
level: erroralint checkRepository-level ✗ error format-version-bumped the changeset `origin/main...HEAD` changes a path matching `src/format.rs` but no path matching `FORMAT_VERSION` changed with it related: changed trigger: src/format.rsRepository-level ✗ error format-version-bumped the changeset `origin/main...HEAD` changes a path matching `src/format.rs` but no path matching `FORMAT_VERSION` changed with it related: changed trigger: src/format.rsBump FORMAT_VERSION in the same pull request, and say what changed in the file format.
A pull request changes a package without a changelog entry the changeset origin/main...HEAD (its when_changed glob changed) adds no file matching .changeset/*.mdRules: changeset_requires_path
- id: changelog-entry
kind: changeset_requires_path
when_changed: "packages/**"
add_glob: ".changeset/*.md"
since: origin/main
level: erroralint checkRepository-level ✗ error changelog-entry the changeset `origin/main...HEAD` (its `when_changed` glob changed) adds no file matching `.changeset/*.md`Repository-level ✗ error changelog-entry the changeset `origin/main...HEAD` (its `when_changed` glob changed) adds no file matching `.changeset/*.md`Run npx changeset and commit the file it writes.
Coding agents
What agents leave behind, and instructions that go stale. The coding agents guide has the loop an agent runs and how to make it run alint.
An agent leaves a plan, an _old copy or a console.log behind src/cart_old.ts: Filename looks like a versioned duplicate (e.g. app_old.js, api_FINAL.py, utils_copy.ts). Replace the original instead of keeping a parallel copy.Rules: agent-hygiene4 findings
version: 1
extends:
- alint://bundled/agent-hygiene@v1 alint checkPLAN.md ⚠︎ warning agent-no-scratch-docs-at-root Scratch / planning documents should not be committed at the repo root. Move the content into a real doc (CHANGELOG, ADR, design doc, README) or delete it once the work is done. src/app.ts ⚠︎ warning agent-no-console-log 2:1 `console.log` / `.debug` / `.trace` left in non-test source. Route through the project logger or remove before merge. src/cart_old.ts ⚠︎ warning agent-no-versioned-duplicates Filename looks like a versioned duplicate (e.g. app_old.js, api_FINAL.py, utils_copy.ts). Replace the original instead of keeping a parallel copy. src/coupons.ts ⚠︎ warning agent-no-model-todos 1:1 Agent-attributed TODO marker. Resolve, convert to a tracked issue, or remove the model attribution. These outlive the session that wrote them.PLAN.md ⚠︎ warning agent-no-scratch-docs-at-root Scratch / planning documents should not be committed at the repo root. Move the content into a real doc (CHANGELOG, ADR, design doc, README) or delete it once the work is done. src/app.ts ⚠︎ warning agent-no-console-log 2:1 `console.log` / `.debug` / `.trace` left in non-test source. Route through the project logger or remove before merge. src/cart_old.ts ⚠︎ warning agent-no-versioned-duplicates Filename looks like a versioned duplicate (e.g. app_old.js, api_FINAL.py, utils_copy.ts). Replace the original instead of keeping a parallel copy. src/coupons.ts ⚠︎ warning agent-no-model-todos 1:1 Agent-attributed TODO marker. Resolve, convert to a tracked issue, or remove the model attribution. These outlive the session that wrote them.- storefront/
- .alint.yml✓
- PLAN.md
warning agent-no-scratch-docs-at-root
Scratch / planning documents should not be committed at the repo root. Move the content into a real doc (CHANGELOG, ADR, design doc, README) or delete it once the work is done.
- README.md✓
- src/
- src/app.ts
warning agent-no-console-log
console.log/.debug/.traceleft in non-test source. Route through the project logger or remove before merge. - src/cart_old.ts
warning agent-no-versioned-duplicates
Filename looks like a versioned duplicate (e.g. app_old.js, api_FINAL.py, utils_copy.ts). Replace the original instead of keeping a parallel copy.
- src/coupons.ts
warning agent-no-model-todos
Agent-attributed TODO marker. Resolve, convert to a tracked issue, or remove the model attribution. These outlive the session that wrote them.
Turn the TODO into an issue; the plan, the old copy and the console.log can go.
AGENTS.md points at files that no longer exist AGENTS.md: backticked path src/db/client.ts doesn't resolve to a file or directoryRules: markdown_paths_resolve2 findings
# Working in this repository
- The HTTP routes live in `src/routes/` ; start with `src/routes/cart.ts` .
- Database access goes through `src/db/client.ts` .
- Old checkout code is in `src/legacy/checkout.ts` . Don't extend it.
- Run `npm test` before you commit. Fixtures are in `test/fixtures/` .- id: agents-md-paths-exist
kind: markdown_paths_resolve
paths: ["AGENTS.md", "CLAUDE.md"]
prefixes: ["src/" , "test/" ]
level: warningalint checkAGENTS.md ⚠︎ warning agents-md-paths-exist 4:32 backticked path `src/db/client.ts` doesn't resolve to a file or directory ⚠︎ warning agents-md-paths-exist 5:27 backticked path `src/legacy/checkout.ts` doesn't resolve to a file or directoryAGENTS.md ⚠︎ warning agents-md-paths-exist 4:32 backticked path `src/db/client.ts` doesn't resolve to a file or directory ⚠︎ warning agents-md-paths-exist 5:27 backticked path `src/legacy/checkout.ts` doesn't resolve to a file or directoryFor the part of AGENTS.md that lists your rules, alint export-agents-md writes it from your config. Run it again after the rules change.
Correct the paths, or delete the lines about files that are gone.
Start from your stack
Each starter extends rulesets that ship with alint. A language ruleset stays quiet in a repository without that language.
Any repository
version: 1
extends:
- alint://bundled/oss-baseline@v1 15 rules. A README that isn't a stub, a license, a security policy, code owners, a code of conduct, Dependabot or Renovate, and a .gitignore; and in Markdown, text, TOML and YAML files, no merge-conflict markers, bidi controls or trailing whitespace, and a final newline.
Node
version: 1
extends:
- alint://bundled/oss-baseline@v1
- alint://bundled/node@v1
- alint://bundled/monorepo/pnpm-workspace@v1
- alint://bundled/hygiene/lockfiles@v1
- alint://bundled/ci/github-actions@v1 36 rules. For a pnpm workspace. Adds a package.json and a lockfile, a Node version pinned in .nvmrc, .node-version or .tool-versions, no tracked node_modules or dist, a README and a name for every workspace package, no nested lockfiles and the GitHub Actions checks.
Python
version: 1
extends:
- alint://bundled/oss-baseline@v1
- alint://bundled/python@v1
- alint://bundled/hygiene/lockfiles@v1
- alint://bundled/ci/github-actions@v1 33 rules. Adds a pyproject.toml that declares a name and requires-python, a lockfile, snake_case module names, no nested lockfiles and the GitHub Actions checks.
Rust
version: 1
extends:
- alint://bundled/oss-baseline@v1
- alint://bundled/rust@v1
- alint://bundled/monorepo/cargo-workspace@v1
- alint://bundled/hygiene/no-tracked-artifacts@v1
- alint://bundled/ci/github-actions@v1 42 rules. For a Cargo workspace. Adds a committed Cargo.lock and a pinned toolchain, snake_case source files, a README and a name for every workspace member, no tracked target/ or .env files, and workflows with read-only permissions and pinned actions.
Go
version: 1
extends:
- alint://bundled/oss-baseline@v1
- alint://bundled/go@v1
- alint://bundled/ci/github-actions@v1 25 rules. Adds a go.mod that declares its module path and Go version, a go.sum, source files without Trojan Source characters and the GitHub Actions checks.
Coding agents
version: 1
extends:
- url: alint://bundled/agent-context@v1
except: [agent-context-no-stale-paths]
- alint://bundled/agent-hygiene@v1
- alint://bundled/hygiene/no-tracked-artifacts@v1
- alint://bundled/ci/github-actions@v1 23 rules. The rulesets the coding agents page's config extends: what agents leave behind (_old copies, scratch plans, console.log calls, TODOs signed by a model), an AGENTS.md or CLAUDE.md that is missing or a stub, tracked build output and workflow permissions. The stale-paths rule is off: it reports a backticked path under src/, docs/ and a few other directories whether or not the path exists.
First run of the "Any repository" starter, on a Python project with a README and no license
alint checkRepository-level ⚠︎ warning oss-license-exists An open-source repo should declare a license at the root. docs: https://opensource.guide/legal/#which-open-source-license-is-appropriate-for-my-project ℹ︎ info oss-security-policy-exists Consider adding a SECURITY.md so vulnerability reporters know where to disclose. docs: https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository ℹ︎ info oss-dependency-update-tool Consider configuring Dependabot or Renovate to keep dependencies and actions up to date. docs: https://github.com/ossf/scorecard/blob/main/docs/checks.md#dependency-update-tool ℹ︎ info oss-codeowners-exists Consider adding a CODEOWNERS file so PR reviews are auto-routed. docs: https://github.com/ossf/scorecard/blob/main/docs/checks.md#code-review ℹ︎ info oss-code-of-conduct-exists Consider adding a CODE_OF_CONDUCT.md. docs: https://www.contributor-covenant.org/ Summary (5 violations): ⚠︎ 1 warning ℹ︎ 4 info 10 passing · 5 failingRepository-level ⚠︎ warning oss-license-exists An open-source repo should declare a license at the root. ℹ︎ info oss-security-policy-exists Consider adding a SECURITY.md so vulnerability reporters know where to disclose. ℹ︎ info oss-dependency-update-tool Consider configuring Dependabot or Renovate to keep dependencies and actions up to date. ℹ︎ info oss-codeowners-exists Consider adding a CODEOWNERS file so PR reviews are auto-routed. ℹ︎ info oss-code-of-conduct-exists Consider adding a CODE_OF_CONDUCT.md. Summary (5 violations): ⚠︎ 1 warning ℹ︎ 4 info 10 passing · 5 failingFrom a script to a rule
Each of these one-line checks from a scripts/ or hack/ directory is one rule. Moving check scripts to alint has the full table.
| The script does | The rule | Notes |
|---|---|---|
grep -rn 'TODO\|FIXME' src/ | file_content_forbidden | Add git_blame_age to flag only the old ones. |
find . -name .DS_Store | file_absent | With a file_remove fix; in hygiene/no-tracked-artifacts. |
for d in packages/*/; do test -f "$d/README.md"; done | for_each_dir | With a nested file_exists, like the README example above. |
jq -e '.license == "MIT"' package.json | json_path_equals | json_path_matches for a regex. |
Keep running ShellCheck, from alint scripts/deploy.sh: shellcheck failed (exit 1):Rules: command
- id: shellcheck
kind: command
paths: "scripts/**/*.sh"
command: ["shellcheck", "--format=gcc", "{path}"]
level: errorA check that needs a real tool can keep using it: the command rule runs the tool on each matching file and turns a failure into a finding.
alint checkscripts/deploy.sh ✗ error shellcheck `shellcheck` failed (exit 1): scripts/deploy.sh:4:16: note: Double quote to prevent globbing and word splitting. [SC2086]scripts/deploy.sh ✗ error shellcheck `shellcheck` failed (exit 1): scripts/deploy.sh:4:16: note: Double quote to prevent globbing and word splitting. [SC2086]Quote the variable: rsync -a dist/ "$target".
The CI step
One step in a workflow runs alint on every pull request and turns each finding into an annotation on the pull request. It's pinned to the release's commit, as the CI rules above ask of every action. The docs cover the Action, pre-commit and editors.
- uses: asamarts/alint@8d7a60f6642cb1b59537202ae81df7d951a078ad # v0.22.0Reference configs for 30 repositories
These configs were written to test alint on large, real repositories; the projects don't use alint. The pages lead with what a run on the project's tree found, where it found something.
Repositories that run check scripts
A folder of shell or Python checks, or a runner that calls many linters.
- apache/airflow101 provider packages share one layout, checked by reading the tree instead of importing each one.
- bazelbuild/bazel2 of 9 workflows skip the harden-runner step the other 7 use.
- denoland/denoFour of the seven checks Deno's lint script adds to Clippy and dlint, written as rules.
- kubernetes/kubernetesAbout two thirds of what its 50 verify scripts checked in May 2026, as rules or run from alint.
- microsoft/vscode172 proposed-API files checked against one name pattern, and a batch file with LF endings.
- python/cpythonThe blurb tool names every NEWS entry; one rule checks a hand-written name before a pull request.
- pytorch/pytorch57 lintrunner adapters: which become rules, which run from alint, which stay.
- rust-lang/rusttidy's file-level checks fit in YAML; the ones that parse Rust stay in tidy.
Conventions nothing checks yet
Layouts and manifest fields the project keeps by habit or in review.
- astral-sh/ruff8 internal crates could be published to crates.io by mistake.
- astral-sh/uv3 rules check that every crate inherits from the workspace; 4 deliberate exceptions surface.
- clap-rs/clapFive rules check that each member crate inherits the workspace's five shared fields; all do.
- facebook/reactreact-refresh points npm at the wrong folder: a
repository.directorycopied from thereactpackage. - golang/goReview keeps its conventions; as rules they found stray header text and hidden zero-width spaces.
- nodejs/nodeA test not named
test-*never runs; one rule checks the names in five test directories. - pnpm/pnpmThe fixed-value rules of a 499-line package plugin, as JSON path rules.
- tokio-rs/tokioNo check scripts; the conventions its CI assumes, such as
publish = false, all held.
Repositories that already lint heavily
What file checks add to ESLint, Ruff or golangci-lint.
- angular/angular3 TypeScript sources start with an invisible byte-order mark.
- dotnet/runtime5,856 project files and their version pins, checked as XML instead of with regexes.
- helm/helmA doc comment in
internal/plugin/plugin.gohides three zero-width spaces. - microsoft/TypeScript135 test baselines over 256 KiB, and trailing whitespace in 6 source files.
- prettier/prettierEach language plugin needs two entry files; two rules check all 8 without a build.
- vercel/turbo9 crates without a README, and an example the example runner skips.
Several languages in one repository
Checks across language boundaries that no single-language linter sees.
- apache/arrowSix languages' package layouts, checked together in one config.
- apache/spark4 Java files break the PascalCase naming the other sources follow.
- flutter/flutter5 docs files contain U+202C, an invisible bidi control.
- istio/istioA 2020 release note spells its first key
piVersion; the generator only checks new notes. - NixOS/nixpkgsTwo packages commit a Ruby bundler cache; all 20,698 package directories pass the layout check.
- protocolbuffers/protobufFour bindings write their version a second time; the config keeps the Java and Ruby copies in step.
- tensorflow/tensorflowMost CI checks that need no Bazel, as rules, plus checks on 1,185 API goldens.
- vercel/next.jsOne config for both workspaces; it found two published npm packages with no license.
Once a rule or two fits, the quickstart has the commands to run them, and the rule catalog has all 103 kinds.