alint
alint is a language-agnostic linter for repository structure. You declare the shape your repo should have — required files, filename conventions, content patterns, cross-file relationships — in a single .alint.yml, and alint enforces it. It walks the tree honoring .gitignore, runs rules in parallel, reports violations in human / JSON / SARIF / GitHub-annotation form, and can auto-fix what it flags. One static Rust binary, any language, any repo.
v0.3 ships ~42 rule kinds across ten families and 12 auto-fix ops — see docs/rules.md for the full catalogue. alint fills the active-maintenance gap left when Repolinter was archived in early 2026, with a superset of its rule catalogue plus first-class cross-file and conditional-rule primitives.
Core capabilities
- ~42 rule kinds across ten families (full reference: docs/rules.md):
- Existence —
file_exists,file_absent,dir_exists,dir_absent. - Content —
file_content_matches,file_content_forbidden,file_header,file_starts_with,file_ends_with,file_hash,file_max_size,file_is_text,file_is_ascii. - Naming —
filename_case,filename_regex. - Text hygiene —
no_trailing_whitespace,final_newline,line_endings,line_max_width,indent_style,max_consecutive_blank_lines. - Security / Unicode —
no_merge_conflict_markers,no_bidi_controls,no_zero_width_chars. - Encoding —
no_bom. - Structure —
max_directory_depth,max_files_per_directory,no_empty_files. - Portable metadata —
no_case_conflicts,no_illegal_windows_names. - Unix metadata —
no_symlinks,executable_bit,executable_has_shebang,shebang_has_executable. - Git hygiene —
no_submodules. - Cross-file —
pair,for_each_dir,for_each_file,dir_contains,dir_only_contains,unique_by,every_matching_has.
- Existence —
- Auto-fix — 12 file ops covering content edits (trim whitespace, append newline, normalize line endings, strip BOM / bidi / zero-width, collapse blank lines) and path-level changes (create / remove / rename / prepend / append). Preview with
alint fix --dry-run. Content-editing ops honour a configurablefix_size_limit(default 1 MiB) that skips oversize files rather than rewriting them. - Conditional rules — a bounded
when:expression language (boolean logic, comparisons,matchesregex,inlist membership) gates rules on facts evaluated once per run:any_file_exists,all_files_exist,count_files. - Four output formats —
human,json(stable schema),sarif(GitHub Code Scanning),github(inline PR annotations). - JSON Schema at
schemas/v1/config.jsonfor editor autocomplete. - Official GitHub Action —
asamarts/alint@v0.4.3.
Typical use cases
- "Every package in a monorepo has a
README.mdand aLICENSE*" —dir_containsacrosspackages/*. - "All Rust source files carry a copyright header; auto-prepend any that don't" —
file_header+file_prepend. - "No stray
*.bakor*.swpfiles in committed history; delete any that slip in" —file_absent+file_remove. - "Filename case convention enforced per language" —
filename_casewithwhen: facts.is_typescriptgating. - "Every module directory has a
mod.rs" —for_each_dirwith nestedfile_exists. - "No two files share a basename across the tree" —
unique_bywithkey: "{basename}".
The full DSL is documented in docs/design/ARCHITECTURE.md.
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
From a tagged release (recommended)
|
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.
From crates.io
From source
Quick start
Create a .alint.yml at the root of your repository:
# yaml-language-server: $schema=https://raw.githubusercontent.com/asamarts/alint/main/schemas/v1/config.json
version: 1
facts:
- id: is_rust
any_file_exists:
rules:
- id: readme-exists
kind: file_exists
paths:
root_only: true
level: error
fix:
file_create:
content: "# Project\n"
- id: no-backup-files
kind: file_absent
paths: "**/*.{bak,swp}"
level: warning
fix:
file_remove:
- id: components-pascal
kind: filename_case
paths: "components/**/*.{tsx,jsx}"
case: pascal
level: error
fix:
file_rename:
- id: rust-snake
when: facts.is_rust
kind: filename_case
paths: "src/**/*.rs"
case: snake
level: error
fix:
file_rename:
- id: java-license-header
kind: file_header
paths: "**/*.java"
lines: 20
pattern: "(?s)Copyright \\(c\\) \\d{4}"
level: error
fix:
file_prepend:
content: |
// Copyright (c) 2026 Acme Corp
Bundled rulesets (one-line baseline)
For common cases you don't want to hand-roll, alint ships a small catalogue of rulesets built into the binary. Use them via extends: without a network round-trip:
version: 1
extends:
- alint://bundled/oss-baseline@v1 # community docs + content hygiene
- alint://bundled/rust@v1 # Rust project (no-op if Cargo.toml absent)
- alint://bundled/node@v1 # Node project (no-op if package.json absent)
- alint://bundled/monorepo@v1 # packages/* / crates/* layout checks
- alint://bundled/hygiene/no-tracked-artifacts@v1 # node_modules, .DS_Store, .env, big blobs
- alint://bundled/hygiene/lockfiles@v1 # lockfiles at workspace root only
- alint://bundled/tooling/editorconfig@v1 # .editorconfig + .gitattributes hygiene
- alint://bundled/docs/adr@v1 # MADR-style Architecture Decision Records
Currently shipped (see docs/rules.md for each ruleset's full rule list):
Ecosystem + project-shape baselines
oss-baseline@v1— README / LICENSE / SECURITY.md / CODE_OF_CONDUCT.md / .gitignore existence; 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.is_rust.node@v1— package.json + lockfile; no committednode_modules/,dist/,.next/, etc.; Node-version pin via.nvmrcorengines; JS/TS source hygiene. Gated withwhen: facts.is_node.monorepo@v1— everypackages/*,crates/*,apps/*,services/*directory has a README + ecosystem manifest; unique basenames.
Namespaced utilities
hygiene/no-tracked-artifacts@v1— build outputs (node_modules,target,dist,__pycache__, …), 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.
All rulesets ship with non-blocking defaults (info / warning for recommendations, error only for unambiguous bugs). You can upgrade severity, disable individual rules with level: off, or override scope by redeclaring the rule id in your own .alint.yml. More rulesets (python, java, go, ci/github-actions, compliance/reuse) are planned for v0.5.
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.
Use in CI
GitHub Actions
Inline PR annotations (default):
- uses: asamarts/alint@v0.4.3
All inputs (all optional):
- uses: asamarts/alint@v0.4.3
with:
version: v0.4.3 # alint release tag (default: latest)
path: . # directory to lint (default: .)
format: github # human | json | sarif | github (default)
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.4.3
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.4.3
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/<version>/.
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.