alint
Fast, language-agnostic linter for repository structure, files, and content. Declare the shape your repo should have (required files, filename conventions, content patterns, values inside package.json / Cargo.toml / GitHub workflows, cross-file relationships) in a single .alint.yml. alint enforces it.
A tracked target/, an unpinned GitHub Action, a scratch planning doc at the repo root, and whitespace drift. alint finds all of it, fixes what is mechanical, and leaves what needs a decision. No per-language linter sees any of it.
- ⚡ Fast at scale. ~1.1 s on a 100K-file workspace bundle, ~12 s at 1M files. Public benchmarks per release.
- 🤖 Agent-aware. First-class
agentoutput format with per-violationagent_instructionstrings; bundledagent-hygieneandagent-contextrulesets for AI-touched repos. - 🧰 Powerful + extensible. 89 rule kinds across 13 families, 22 bundled ecosystem rulesets, 12 auto-fix ops, 8 output formats, structured-query rules with full RFC 9535 JSONPath, cross-file relational rules, conditional
when:gates over per-run facts, andextends:composition with SRI-pinned URLs. - 📦 One static Rust binary. Any language, any repo. No plugin install, no Node/JVM/Python runtime needed.
Working .alint.yml configs for 30 OSS repos (single-language workspaces, polyglot monorepos, scale stress-tests) live under examples/, each with a writeup of what alint catches that the repo's existing tooling misses.
60-second quickstart
# Install (Linux + macOS + Windows tarballs):
|
# Initialise a config in the current repo (uses bundled oss-baseline + auto-detected ecosystem rulesets):
# Check:
# Auto-fix what's mechanically fixable (preview first):
Most bundled rulesets are gated by ecosystem facts (has_rust, has_node, has_python, and so on), so listing one for an ecosystem you don't have is a silent no-op; the compliance and Apache-governance sets (reuse, apache-2, apache/governance) and agent-hygiene carry no fact gate and always fire, so extending them is itself the opt-in. See docs/rules.md for the full rule catalogue and alint.org for narrative docs.
Core capabilities
- 89 rule kinds across 13 families: existence, content, naming, structured query (RFC 9535 JSONPath over JSON/YAML/TOML/XML), text hygiene, security/unicode, encoding, structure, portable metadata, Unix metadata, git hygiene, cross-file relations, plugin (
commandshellout). Full reference:docs/rules.md. - 22 bundled rulesets, compiled into the binary with no network round-trip:
oss-baseline(a strict superset of Repolinter's default ruleset, for anyone migrating off it now that it is archived), seven language sets,ci/github-actions, amonorepobase plus Cargo / pnpm / Yarn workspace overlays, hygiene, tooling, ADR docs, compliance (reuse,apache-2), Apache TLP governance, and two agent-aware sets. Per-ruleset detail: Bundled rulesets below. - Auto-fix: 12 ops covering content edits (whitespace, newlines, line endings, BOM/bidi/zero-width strip, blank-line collapse) and path ops (create/remove/rename/prepend/append). Preview with
alint fix --dry-run. Configurablefix_size_limit(default 1 MiB) skips oversize files rather than rewriting them. - Conditional rules: a bounded
when:expression language (boolean logic, comparisons,matches,in) gates rules on facts evaluated once per run (predicate kinds:all_files_exist,any_file_exists,count_files,file_content_matches,git_branch,custom). - Composition:
extends:pulls in other configs by local path, HTTPS URL (SRI-pinned), oralint://bundled/<name>@<rev>. Children override field-by-field. Monorepos can opt intonested_configs: truefor auto-discovered subtree-scoped.alint.ymlfiles. - 12 subcommands:
check(default; supports--changedfor PR-fast-path linting),fix,baseline(snapshot current violations socheck --baselinegates only on new ones),init(auto-detect ecosystem + scaffold),validate-config(parse-only; for editor LSP / pre-commit / fail-fast CI),explain <rule>,list(rules from this repo's config;--category <slug>to filter),suggest(scan for antipatterns and propose rules),facts(debugwhen:clauses),export-agents-md(syncAGENTS.mdfrom active rules),lsp(language server over stdio for editor integrations),rules(browse the rule-kind catalog by category; config-independent, unlikelist). - 8 output formats:
human,json(stable schema),sarif(GitHub Code Scanning),github(inline PR annotations),markdown(PR comments),junit(CI test reports),gitlab(Code Quality),agent(LLM-shaped JSON withagent_instructionper violation). - JSON Schemas: config at
schemas/v1/config.jsonfor editor autocomplete; report shapes atschemas/v1/check-report.jsonandschemas/v1/fix-report.jsonfor downstream tooling. - Telemetry-free. No network access at runtime, except the user-explicit
extends: https://...URLs (SRI-pinned). Reproducible builds (Cargo.lockcommitted, pinned toolchain). See SECURITY.md for the threat model. - Official GitHub Action:
asamarts/alint@v0.14.1.
Where alint shines
The 30 example configs that ship with alint were not cherry-picked. We wrote a working config for each of those repos to find out where alint actually pulls its weight, and five recurring shapes of project came out of it.
- Repos with verify-script sprawl. Kubernetes hand-rolled 50 verify scripts; 17 declarative rules cover the structural ones. apache/airflow runs 109 pre-commit hooks, and roughly 40% map cleanly onto alint. python/cpython has 12 separate validation surfaces that collapse into a single config. About 75% of microsoft/vscode's
build/hygiene.tsis expressible declaratively. - Repos that rely on convention without checking it. tokio has no hand-rolled validation scripts at all, and alint catches 15 conventions its pipeline silently assumes. uv's 67-crate workspace conventions are enforced nowhere in CI today. pnpm maintains an in-tree
meta-updaterplugin to do this work. Same shape: facebook/react, nodejs/node. - Repos with mature tooling but no structural layer. microsoft/typescript already runs eslint, dprint and knip. astral-sh/ruff ships 900+ Python lint rules, yet none cover ruff's own internal-crate
publish = falsediscipline. dotnet/runtime carries roughly 2,300 XML manifests whose structural invariants no existing tool checks. Same shape: prettier, helm. - Repos that built their own lint-orchestration layer. Around 86% of pytorch's 57
lintrunneradapters are structural. alint sits underneath, and lintrunner keeps the AST-aware tail. bazel is the same shape, with hand-rolled CI scripts standing in for lintrunner. - Tightly curated, minimal-tooling projects. golang/go has no
.github/workflows/, noMakefile, no.golangci.yml. Its conventions live in code-review discipline. The 31-rule alint config writes that structural contract down for the first time.
Polyglot repos are a sixth shape. When one tree spans languages or platforms (apache/arrow's 6 languages, vercel/next.js in TS and Rust, NixOS/nixpkgs at 39k files, flutter's Dart framework with 6 native embedders, protobuf's 11 language bindings), no per-language linter sees the conventions that cut across them. alint is the layer that does.
If your repo matches none of these, alint is likely still useful, since the rule catalogue is broad. Start from whichever example is closest to your shape to see what a working config looks like.
Non-goals
alint is deliberately not:
- a code / AST linter (use ESLint, Clippy, ruff)
- a SAST scanner (use Semgrep, CodeQL)
- an IaC scanner (use Checkov, Conftest, tfsec)
- a commit-message linter (use commitlint)
- a secret scanner (use gitleaks, trufflehog)
Scope is the filesystem shape and contents of a repository, not the semantics of the code inside it.
Install
install.sh (Linux + macOS + Windows tarballs)
|
Detects platform (Linux / macOS, x86_64 / aarch64), downloads the matching tarball, verifies the SHA-256, and installs to $INSTALL_DIR (default ~/.local/bin). Windows users download the Windows tarball from the Releases page.
Homebrew (macOS + Linuxbrew)
The asamarts/homebrew-alint tap is auto-updated on every alint release. The formula downloads the matching pre-built binary, verifies its SHA-256, and installs to the Homebrew cellar.
From crates.io
From npm
# project-local
# global (puts `alint` on PATH)
The @asamarts/alint package is a thin shim that downloads the matching pre-built binary at install time, verifies its SHA-256 against the same .sha256 companion install.sh and Homebrew use, and stages it under the package's bin-platform/. The package itself ships zero JS runtime behaviour. Set ALINT_SKIP_INSTALL=1 to suppress the postinstall network hop in CI environments that snapshot node_modules.
From source
Docker
A distroless multi-arch image (linux/amd64, linux/arm64) is published to ghcr.io on each release:
# Lint the current directory:
# Pin to an exact version:
The image runs as the distroless nonroot user (UID 65532); host files must be world-readable. To apply fixes and preserve host ownership, pass -u:
Also published: :<major>.<minor> (e.g. :0.13) and the raw git tag (:v0.14.1).
Usage
The fastest on-ramp is alint init. It scans your repo for the obvious markers (Cargo.toml, package.json, pnpm-workspace.yaml, and so on) and writes a .alint.yml with the right extends: lines:
For an existing repo with prior debt, follow up with alint suggest. It scans for *.bak files, scratch docs at root, console.log residue in production source, and TODO markers older than 180 days, then proposes the bundled rulesets and rule entries that would catch them. Output is review-only; suggest never edits your config:
For agent-driven workflows where AGENTS.md / CLAUDE.md / .cursorrules carries the directives the agent reads at session start, alint export-agents-md renders the active rule set as a markdown directive block. alint becomes the single source of truth, and the agent reads what alint enforces:
--inline writes only between <!-- alint:start --> / <!-- alint:end --> markers; everything else in AGENTS.md is human-owned prose. Re-runs are idempotent (when the on-disk content already matches, no write happens), and missing markers auto-init with a stderr warning so the second run splices cleanly.
The generated file is editable: start there, override or extend as needed. If you'd rather hand-roll, the minimum viable shape is:
# .alint.yml
# yaml-language-server: $schema=https://raw.githubusercontent.com/asamarts/alint/main/schemas/v1/config.json
version: 1
extends:
- alint://bundled/oss-baseline@v1 # README/LICENSE/SECURITY.md, merge markers, hygiene
Then run:
Output formats:
Exit codes: 0 no errors; 1 one or more errors; 2 config error; 3 internal error. Warnings do not fail by default; use --fail-on-warning to flip that.
Cookbook
Copy-pasteable recipes for composing bundled rulesets, structured-query rules over package.json / Cargo.toml / GitHub workflows, monorepo overlays + nested .alint.yml, conditional rules gated on per-run facts, custom command shellouts, fast PR-mode linting with --changed, auto-fix on commit, cross-file relationships, and per-iteration when_iter: filters.
Read at alint.org/docs/cookbook/ (source: docs/site/cookbook/).
Bundled rulesets
Twenty-two rulesets ship in the binary, with zero network round-trip, pinned to the version of alint you're running:
Ecosystem + project-shape baselines
oss-baseline@v1: README / LICENSE / SECURITY.md / CODE_OF_CONDUCT.md / .gitignore existence; minimum sensible file sizes; merge-marker + bidi-control bans; trailing-whitespace and final-newline hygiene (auto-fixable).rust@v1: Cargo.toml / Cargo.lock / rust-toolchain.toml existence; no committedtarget/; snake_case source filenames; Trojan-Source defenses. Gated withwhen: facts.has_rust.node@v1: package.json + lockfile; no committednode_modules/,dist/,.next/, etc.; Node-version pin via.nvmrcorengines; JS/TS source hygiene. Gated withwhen: facts.has_node.python@v1: manifest (pyproject.toml / setup.py / setup.cfg) exists; lockfile (uv / poetry / Pipenv / PDM); pyproject.toml declaresproject.name+project.requires-pythonvia structured-query; PEP 8 snake_case module filenames; Trojan-Source defenses. Gated withwhen: facts.has_python.go@v1: go.mod + go.sum at root; go.mod declaresmodule <path>+go <version>; Trojan-Source defenses on*.go. Gated withwhen: facts.has_go.java@v1: Maven (pom.xml) or Gradle (build.gradle/build.gradle.kts) manifest; build wrapper (mvnw/gradlew); no committedtarget//build/(usinggit_tracked_onlyso locally-built dirs stay silent); no committed*.class; PascalCase Java filenames; Trojan-Source defenses. Gated withwhen: facts.has_java.dotnet@v1: rootglobal.jsonexists + pins a concrete SDK version; SDK-style.csproj; nullable reference types enabled; Central Package Management actually enabled whenDirectory.Packages.propsexists; no committedbin//obj/; root.editorconfig. Gated withwhen: facts.has_dotnet.php@v1: composer.jsonnameis a valid lowercasevendor/package; the Composer-fatals invariants (everyautoload.psr-4directory,autoload.filesentry, andbinscript resolves on disk); no committedvendor/. Gated withwhen: facts.has_php.monorepo@v1: everypackages/*,crates/*,apps/*,services/*directory has a README + ecosystem manifest; unique basenames.
Workspace-aware overlays (use when_iter: to scope per-member checks to actual package directories, so non-package dirs under crates/ / packages/ don't fire false positives)
monorepo/cargo-workspace@v1: Cargo workspaces. Gated byfacts.is_cargo_workspace(rootCargo.tomlhas[workspace]). Verifiesmembers = [...]is declared and every workspace member has a README +[package].name.monorepo/pnpm-workspace@v1: pnpm workspaces. Gated byfacts.is_pnpm_workspace(rootpnpm-workspace.yamlexists). Verifies thepackages:declaration and per-member README +name.monorepo/yarn-workspace@v1: Yarn / npm workspaces. Gated byfacts.is_yarn_workspace(rootpackage.jsonhas"workspaces"). Per-member README +name, scoped to{packages,apps}/*.
Compliance & governance (no fact gate; extending signals intent)
compliance/reuse@v1: FSFE REUSE Specification compliance: top-levelLICENSES/directory + every source file declares bothSPDX-License-Identifier:andSPDX-FileCopyrightText:in its first ~10 lines.compliance/apache-2@v1: Apache-2.0 compliance: LICENSE contains the Apache 2.0 text, root NOTICE file present, and every source file carries the canonical "Licensed under the Apache License, Version 2.0" header.apache/governance@v1: Apache Top-Level-Project (TLP) governance discipline: the release-artefact + governance baseline that arrow / spark / airflow each re-implement by hand. Rule ids are namespacedapache-gov-*, so it composes withcompliance/apache-2@v1(license redistribution) without collision; severities are tiered (legally load-bearing artefactserror, release disciplinewarning, nice-to-havesinfo).
Namespaced utilities
hygiene/no-tracked-artifacts@v1: build outputs (node_modules,target,dist,__pycache__, and more), OS junk (.DS_Store,Thumbs.db), editor backups (*~,*.swp), secret-shaped files (.envand locals), and files over 10 MiB. Several rules auto-fixable viafile_remove.hygiene/lockfiles@v1: enforce lockfiles (yarn.lock,pnpm-lock.yaml,package-lock.json,bun.lock,Cargo.lock,poetry.lock,uv.lock) live only at the workspace root.tooling/editorconfig@v1: root.editorconfig+.gitattributeswith line-ending normalization.docs/adr@v1: MADR-style Architecture Decision Records underdocs/adr/:NNNN-kebab-title.mdfilename + required## Status/## Context/## Decisionsections.ci/github-actions@v1: GitHub Actions hardening guided by OpenSSF Scorecard: workflow-levelpermissions.contents: read, pin third-party actions to full commit SHAs, every workflow declares aname:. Scoped to.github/workflows/*.y{,a}ml, so it no-ops in repos that don't use GitHub Actions.
Agent-aware
agent-hygiene@v1: AI-coding-era cruft that shows up disproportionately in agent-authored commits: versioned-duplicate filenames, scratch / planning docs, AI-affirmation prose, debug residue (debugger;in non-test source), and model-attributed TODO markers. Composes with the hygiene rulesets; fires on every repo (no fact gate).agent-context@v1: hygiene for the agent-instruction files coding agents read each session (AGENTS.md,CLAUDE.md,.github/copilot-instructions.md,.cursorrules,GEMINI.md): existence, stale-reference, stub, and a ~200-300-line bloat guard. Gated withwhen: facts.has_agent_context, so it's a silent no-op in repos that ship no agent-context file.
All rulesets ship with non-blocking defaults (info / warning for recommendations, error only for unambiguous bugs). Override severity or scope by redeclaring the rule id in your own .alint.yml, or disable with level: off. Per-ruleset rule lists in docs/rules.md.
Use in CI
GitHub Actions
Inline PR annotations (default):
- uses: asamarts/alint@v0.14.1
All inputs (all optional):
- uses: asamarts/alint@v0.14.1
with:
version: v0.14.1 # alint release tag (default: latest)
path: . # directory to lint (default: .)
format: github # human | json | sarif | github | markdown | junit | gitlab | agent (default: github)
config: | # extra config path(s), one per line
.alint.yml
fail-on-warning: false
args: "" # extra CLI args appended verbatim
Upload findings to GitHub Code Scanning:
- uses: asamarts/alint@v0.14.1
id: alint
with:
format: sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: ${{ steps.alint.outputs.sarif-file }}
pre-commit
Add to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/asamarts/alint
rev: v0.14.1
hooks:
- id: alint
The hook runs alint check against the repo's .alint.yml. For auto-fix, add id: alint-fix. It's registered under stages: [manual] so it only runs when invoked explicitly (pre-commit run alint-fix), since fixers mutate the tree.
Docs
- docs/rules.md: per-rule user reference, one entry per rule kind with a YAML example and fix-op cross-reference.
- ARCHITECTURE.md: rule model, DSL, execution model, crate layout, plugin model.
- ROADMAP.md: scope per version from v0.1 through v1.0.
- CHANGELOG.md: per-version changes, breaking and otherwise.
- docs/benchmarks/METHODOLOGY.md: how benchmarks are measured and published.
- Per-version, per-platform benchmark results under
docs/benchmarks/macro/results/(end-to-end wall-time) anddocs/benchmarks/micro/results/(criterion micro-benches).
Development
End-to-end tests live in crates/alint-e2e/scenarios/ as declarative YAML; adding a new scenario only requires a new file. CLI snapshot tests live in crates/alint/tests/cli/ under trycmd. Property-based invariants are in crates/alint-e2e/tests/invariants.rs.
CI is self-hosted with per-job bash scripts under ci/scripts/ that run locally or in GitHub Actions unchanged. See ci/env.example for runner setup.
License
Dual-licensed under either of:
at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in alint shall be dual-licensed as above, without any additional terms or conditions.