A fast, general linter
ESLint, Clippy and Ruff check the code inside a file. alint checks the repository around it: which files must or mustn't exist, what they're named, what they contain, the values in files like package.json or a workflow, and how files relate to each other.
Rules such as "every package has a README" or "workflows pin actions to a commit" go in one .alint.yml. alint checks them on every pull request, in pre-commit and in your editor. It takes over from Repolinter and from scripts that test files and config values.
Guides for monorepos, coding agents, check scripts and moving off Repolinter or ls-lint.
- acme/the 6 findings of alint check, marked on this Rust workspace's files
- .alint.yml✓
- .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. - .github/
- .github/CODEOWNERS✓
- .github/dependabot.yml✓
- .github/workflows/
- .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(orread-all, or{}) at the workflow level, or set per-jobpermissions:, so a compromised step can't push to the repo.warning gha-pin-actions-to-sha
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. - LICENSE✓
- README.md✓
- crates/
- crates/cli/
- crates/cli/Cargo.toml✓
- crates/cli/src/
- crates/cli/src/ParseArgs.rs
error rust-sources-snake-case fixable
Rust module filenames must be snake_case.
- crates/cli/src/main.rs✓
- crates/core/
- crates/core/Cargo.toml
error crates-use-edition-2024
value at path does not equal expected: expected "2024", got "2021"
- crates/core/src/
- crates/core/src/lib.rs
warning rust-sources-final-newline fixable
file does not end with a newline
- 6 more files: .gitignore, CODE_OF_CONDUCT.md, Cargo.lock, Cargo.toml, SECURITY.md, rust-toolchain.toml✓
Summary (6 violations): ✗ 3 errors ⚠︎ 3 warnings 34 passing · 6 failing · 2 auto-fixable → run `alint fix` to resolve 2 fixable violations.
One file configures it
The .alint.yml at the root of that workspace, and what alint check and alint fix printed for it.
version: 1
extends:
- alint://bundled/oss-baseline@v1
- alint://bundled/rust@v1
- alint://bundled/ci/github-actions@v1
- alint://bundled/hygiene/no-tracked-artifacts@v1
rules:
- id: crates-use-edition-2024
kind: toml_path_equals
paths: "crates/*/Cargo.toml"
path: "$.package.edition"
equals: "2024"
level: errorversion: 1
extends:
- alint://bundled/oss-baseline@v1
- alint://bundled/rust@v1
- alint://bundled/ci/github-actions@v1
- alint://bundled/hygiene/no-tracked-artifacts@v1
rules:
- id: crates-use-edition-2024
kind: toml_path_equals
paths: "crates/*/Cargo.toml"
path: "$.package.edition"
equals: "2024"
level: error- Lines 2 to 6
Start from rulesets. Each
extends:line adds a ruleset that ships with alint, 39 rules in all here: the open-source basics, Rust, GitHub Actions and files that shouldn't be tracked. The Rust rules only run where there's Rust. - Lines 9 to 14
Add rules of your own. This one reads
editionfrom every crate's Cargo.toml;crates/coreis still on 2021. It's a toml_path_equals rule, one of 102 kinds.
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. 2 more findings crates/cli/src/ParseArgs.rs ✗ error rust-sources-snake-case fixable Rust module filenames must be snake_case. 2 more findings Summary (6 violations): ✗ 3 errors ⚠︎ 3 warnings 34 passing · 6 failing · 2 auto-fixable → run `alint fix` to resolve 2 fixable violations..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. 2 more findings crates/cli/src/ParseArgs.rs ✗ error rust-sources-snake-case fixable Rust module filenames must be snake_case. 2 more findings Summary (6 violations): ✗ 3 errors ⚠︎ 3 warnings 34 passing · 6 failing · 2 auto-fixable → run `alint fix` to resolve 2 fixable violations.alint groups what it finds by file, names the rule behind each finding and ends with a summary. The exit code fails CI on errors.
alint fixerror [rust-sources-snake-case]: ✓ renamed crates/cli/src/ParseArgs.rs -> crates/cli/src/parse_args.rs warning [rust-sources-final-newline]: ✓ appended final newline to crates/core/src/lib.rs 4 more findingserror [rust-sources-snake-case]: ✓ renamed crates/cli/src/ParseArgs.rs -> crates/cli/src/parse_args.rs warning [rust-sources-final-newline]: ✓ appended final newline to crates/core/src/lib.rs 4 more findingsalint fix renamed the module and added the missing newline. There are 26 kinds of fix, from renames to license headers; the other 4 findings here need a person.
Where these rules live now
No code linter checks rules like these. Today they live in one of three places, and each one has a gap:
- in a reviewer's head, where they slip when that reviewer is busy or away;
- in CONTRIBUTING.md, where nothing fails when a pull request ignores them;
- in check scripts, where each one walks the tree its own way and prints its own output. Kubernetes had 50 of them in
hack/in May 2026.
#!/usr/bin/env bash
set -euo pipefail
fail=0
for d in packages/*/ ; do
if [ ! -f "${d}README.md" ]; then
echo "missing: ${d}README.md" >&2
fail=1
fi
done
exit $failbash hack/verify-readmes.shmissing: packages/emails/README.mdmissing: packages/web/README.md- 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 rule finds the same two packages. It also runs in the same walk as every other rule, reports under its own name in any of alint's 8 output formats, shows up in your editor and keeps checking when someone adds a package.
What you can check
| Rules about | For example | Rule kinds |
|---|---|---|
| Files that must or mustn't exist | A README, license and security policy; no .env, .DS_Store or build output. | file_exists file_absent |
| Names | snake_case modules, kebab-case docs, no spaces, no two files that differ only in case. | filename_case no_case_conflicts |
| Contents | License headers, banned words, merge-conflict markers, hidden Unicode and final newlines. | file_header no_bidi_controls |
| Values in config files | A field in package.json, Cargo.toml, pyproject.toml, a workflow, .env or Terraform equals a value, matches a pattern or is absent. | json_path_equals hcl_path_matches |
| Relations between files | Every package has a README and every migration a down file; generated files are fresh; imports have no cycles. | for_each_dir pair file_graph |
| Commits and pull requests | Conventional commit subjects and files that have to change together. | git_commit_message pair_changed_together |
All 102 rule kinds, and 24 rulesets to start from.
Turn it on without fixing everything first
alint baseline- Records the findings you have today. Point the config's
baseline:field at the file, and CI fails only on new findings while the old ones get fixed as you go. alint check --changed- Checks only files with changes you haven't staged yet, new or edited, which keeps a local run short. In a pull request,
--base origin/mainchecks every file the pull request touches. alint init- Writes a starter
.alint.ymlfor the languages and tools it finds. alint suggest- Looks for problems already in your tree and proposes the rules that would catch them.
How the baseline works, and what --changed still checks across the whole tree.
Runs where you work
The same rules give the same findings in CI, pre-commit, your editor and your coding agent. This is the misnamed module from the workspace, in two of alint's 8 output formats.
- uses: asamarts/alint@10d1c0a2bafd0bd316dbad9fe63ea5e1e3761c71 # v0.20.0alint check --format github::error title=rust-sources-snake-case,file=crates/cli/src/ParseArgs.rs::Rust module filenames must be snake_case. In a pull request. The GitHub Action turns each finding into an annotation on the pull request, and can write a SARIF file for code scanning. The step is pinned to the release's commit, like any other action.
In your editor and hooks. Extensions for VS Code and JetBrains IDEs, and an LSP server for Neovim, Helix, Emacs and others, show the same findings as you type. The pre-commit hook checks each commit, and --compact prints one line per finding for grep.
alint check --format agent{ "rule_id": "rust-sources-snake-case", "severity": "error", "file": "crates/cli/src/ParseArgs.rs", "agent_instruction": "error: Rust module filenames must be snake_case. To resolve: edit crates/cli/src/ParseArgs.rs (or run `alint fix --only rust-sources-snake-case` to apply the auto-fix).", "fix_command": [ "fix", "--only", "rust-sources-snake-case" ]}For a coding agent. Each finding comes with an instruction and, where one exists, the command that fixes it. Above is one finding from the report, cut to the fields an agent acts on. How agents use it.
On real repositories
The reference configs cover 30 open-source repositories, written to test alint on real trees; the projects don't use alint. Three things the configs found:
flutter/flutter oss-no-bidi-controls
U+202C, an invisible bidi control, hides in 5 Markdown files, probably pasted in with contributor names.
facebook/react react-package-repository-directory-matches
react-refresh sets
repository.directorytopackages/react, copied from a sibling package, so npm links to the wrong folder.NixOS/nixpkgs hygiene-no-ruby-bundler-cache
Two packages commit Ruby's bundler cache,
.bundle/, which none of nixpkgs' CI checks looks for.
All 30 reference configs, each with what it checks, what stays outside alint and, where a run found something, what it found.
Try it on the repository you have open
Install it. Other ways to install.
curl -fsSL https://alint.org/install.sh | bashAdd a
.alint.ymlat the root. These three lines turn on 15 rules: a README that isn't a stub, a license, a security policy, code owners, a code of conduct, a.gitignoreand Dependabot or Renovate; and in Markdown, text, TOML and YAML files, no merge-conflict markers, bidi controls or trailing whitespace, and a final newline..alint.yml version: 1 extends: - alint:// bundled/ oss-baseline@v1 Run
alint check. On a small Python project with a README and no license, it prints this:alint checkRepository-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 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 failingFix what it found, or turn a rule off with
level: off. Then add the Action so every pull request runs it.