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
.alint.yml
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              committed

The 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
.alint.yml
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
.alint.yml
- id: no-large-files
  kind: file_max_size
  paths: "**"
  max_bytes: 1048576
  level: error
alint check
assets/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
.alint.yml
- id: package-readme
  kind: for_each_dir
  select: "packages/*"
  require:
    - kind: file_exists
      paths: "{path}/README.md"
      message: "{path} has no README.md"
  level: error
alint check
packages/emails  ✗  error    package-readme              packages/emails has no README.md packages/web  ✗  error    package-readme              packages/web has no README.md
packages/emails  ✗  error    package-readme              packages/emails has no              README.md packages/web  ✗  error    package-readme              packages/web has no              README.md

The 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
.alint.yml
- id: packages-are-mit
  kind: json_path_equals
  paths: "packages/*/package.json"
  path: "$.license"
  equals: "MIT"
  level: error
alint check
packages/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
.alint.yml
- id: workspace-members-exist
  kind: registry_paths_resolve
  source: Cargo.toml
  extract: { toml: "$.workspace.members[*]" }
  expect: dir
  must_contain: Cargo.toml
  level: error
alint check
Cargo.toml  ✗  error    workspace-members-exist              Cargo.toml: entry "crates/legacy-api" does not resolve to              a directory on disk              related: declared target: crates/legacy-api
Cargo.toml  ✗  error    workspace-members-exist              Cargo.toml: entry              "crates/legacy-api" does              not resolve to a directory              on disk              related: declared target: crates/legacy-api

With 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
packages/web/src/checkout.ts
import { getCart } from '@shop/api';
import { db } from '@shop/api/src/db/client';

export const checkout = () => [getCart(), db];
.alint.yml
- id: no-deep-imports
  kind: import_gate
  paths: "packages/*/src/**/*.ts"
  language: js
  forbid: "^@shop/[^/]+/src/"
  level: error
alint check
packages/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
.alint.yml
- id: migration-has-down
  kind: pair
  primary: "migrations/*/up.sql"
  partner: "{dir}/down.sql"
  level: error
alint check
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)              related: expected partner: migrations/2026-08-14-093000_create_orders/down.sql
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)              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
.alint.yml
- 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: error
alint check
proto/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.proto
proto/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.proto

require 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
.alint.yml
- 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: error
alint check
npm/package.json  ✗  error    npm-version-matches-crate              npm/package.json value "1.3.2" != Cargo.toml value "1.4.0"              related: canonical source: Cargo.toml
npm/package.json  ✗  error    npm-version-matches-crate              npm/package.json value              "1.3.2" != Cargo.toml              value "1.4.0"              related: canonical source: Cargo.toml

Set 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
.github/workflows/ci.yml
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 test
.alint.yml
version: 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
.alint.yml
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:

.github/workflows/ci.yml
permissions:
  contents: read

Values 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
.alint.yml
- id: crates-use-edition-2024
  kind: toml_path_equals
  paths: "crates/*/Cargo.toml"
  path: "$.package.edition"
  equals: "2024"
  level: error
alint check
crates/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
infra/database/main.tf
terraform {
  required_version = ">= 1.0"
}
.alint.yml
- 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 check
infra/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
.env.example
DATABASE_URL=postgres://localhost:5432/shop
AWS_REGION=eu-west-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
.alint.yml
- 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 empty

The 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
.alint.yml
- 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 check
src/cart.rs  ✗  error    spdx-header   fixable              1:1  Every source file starts with the SPDX line
src/cart.rs  ✗  error    spdx-header   fixable              1:1  Every source file starts              with the SPDX line
alint fix
error [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
.alint.yml
- 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 check
docs/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 fix
error [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
.alint.yml
- 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 check
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)
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 fix
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.
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
.alint.yml
- id: conventional-commits
  kind: git_commit_message
  pattern: "^(feat|fix|docs|refactor|test|chore)(\\(.+\\))?!?: "
  subject_max_length: 72
  since: origin/main
  level: error
alint check
Repository-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
.alint.yml
- id: format-version-bumped
  kind: pair_changed_together
  if_changed: "src/format.rs"
  then_changed: "FORMAT_VERSION"
  since: origin/main
  level: error
alint check
Repository-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.rs
Repository-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.rs

Bump 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
.alint.yml
- id: changelog-entry
  kind: changeset_requires_path
  when_changed: "packages/**"
  add_glob: ".changeset/*.md"
  since: origin/main
  level: error
alint check
Repository-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
.alint.yml
version: 1
extends:
  - alint://bundled/agent-hygiene@v1
alint check
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.
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 / .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

    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
AGENTS.md
# 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/`.
.alint.yml
- id: agents-md-paths-exist
  kind: markdown_paths_resolve
  paths: ["AGENTS.md", "CLAUDE.md"]
  prefixes: ["src/", "test/"]
  level: warning
alint check
AGENTS.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 directory
AGENTS.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 directory

For 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

.alint.yml
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.

First run of the "Any repository" starter, on a Python project with a README and no license
alint check
Repository-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 failing
Repository-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 failing

From 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 doesThe ruleNotes
grep -rn 'TODO\|FIXME' src/file_content_forbiddenAdd git_blame_age to flag only the old ones.
find . -name .DS_Storefile_absentWith a file_remove fix; in hygiene/no-tracked-artifacts.
for d in packages/*/; do test -f "$d/README.md"; donefor_each_dirWith a nested file_exists, like the README example above.
jq -e '.license == "MIT"' package.jsonjson_path_equalsjson_path_matches for a regex.
Keep running ShellCheck, from alint scripts/deploy.sh: shellcheck failed (exit 1):Rules: command
.alint.yml
- id: shellcheck
  kind: command
  paths: "scripts/**/*.sh"
  command: ["shellcheck", "--format=gcc", "{path}"]
  level: error

A 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 check
scripts/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.

.github/workflows/ci.ymlthe alint step
- uses: asamarts/alint@8d7a60f6642cb1b59537202ae81df7d951a078ad # v0.22.0

Reference 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.

Moving check scripts to alint

Conventions nothing checks yet

Layouts and manifest fields the project keeps by habit or in review.

Repositories that already lint heavily

What file checks add to ESLint, Ruff or golangci-lint.

How alint compares

Several languages in one repository

Checks across language boundaries that no single-language linter sees.

Monorepos and multiple repositories

Once a rule or two fits, the quickstart has the commands to run them, and the rule catalog has all 103 kinds.