On this page
A repository runs on rules that few tools check. A package is supposed to ship a README. Build output isn't supposed to land in version control. Each CI action is supposed to be pinned to a commit SHA, and the LICENSE file is supposed to exist and hold real license text. You know these rules, and you've corrected pull requests that broke them. What you probably don't have is anything that checks them.
The rules your repository assumes but never checks
Conventions like these tend to live in a maintainer's head, in a document or in scripts, and each of those has a hole.
A maintainer's head works until the maintainer is on vacation, or leaves, or the project grows past the point where one person reads every diff.
A document, such as a CONTRIBUTING file, a wiki page or an onboarding guide, writes the rule down. Nothing fails when someone ignores it, and it drifts anyway.
Large projects add scripts. VS Code ships build/hygiene.ts, an in-tree program that checks copyright headers, indentation, product metadata and a few other things. Kubernetes had 50 hack/verify-*.sh scripts wired into make verify in May 2026. They work, but each one belongs to its repository, and many wrap other programs (gofmt, golangci-lint, ShellCheck) instead of standing alone. The convention doesn't travel to the next project.
Other projects keep that machinery outside the tree. When I looked, golang/go had no GitHub Actions workflows, no top-level Makefile and no .golangci.yml. Its conventions are real and strict all the same, enforced through Gerrit code review and the TryBots and LUCI submit infrastructure that run outside the repository. Between in-tree scripts on one side and review plus outside automation on the other, neither is a portable tool built for this layer.
In March 2025 attackers compromised tj-actions/changed-files, a GitHub Action used by over 23,000 repositories, by moving its version tags to a malicious commit (incident analysis). Repositories that had pinned it to a full commit SHA kept the old, safe code. A 2026 Datadog analysis found that 71% of organizations pin none of their actions that way, and even a direct pin doesn't cover unpinned transitive dependencies. Ordinary hygiene looks the same. A 2022 North Carolina State University study that ran OpenSSF Scorecard on about a million npm and PyPI packages found that only about 3% published a security policy, and that about a third of the npm packages had no license.
What alint is
alint is a linter for the shape of a repository. It checks which files exist, what they're named, what they contain, the values inside your config files and how files relate to one another. You describe the shape once, usually in one .alint.yml at the repo root, and alint check enforces it. It's one native executable with no language runtime to install. It reads files instead of building syntax trees, so the same engine works on a Go service, a TypeScript monorepo or a tree with six languages in it.
A small config reads like a list of the things you already wish someone checked:
# .alint.yml (repo root)version: 1rules: - id: readme-exists kind: file_exists paths: README.md level: error
- id: no-committed-build-output kind: dir_absent paths: dist level: error
- id: no-merge-conflict-markers kind: no_merge_conflict_markers paths: "**/*" level: errorUnderneath are over 90 rule kinds (the catalog). They cover existence, naming, content, values inside structured config, relationships between files, and encoding and security checks such as the Trojan Source (bidirectional control) scan. The catalog sorts them into 13 families; the six groups below are a shorter way in. You write rules by hand or extend one of over 20 bundled rulesets.
- Existencefile_exists, dir_absent, file_absentrequired or forbidden paths
- Namingfilename_regex, filename_case, no_case_conflictsnames follow your rules
- Contentfile_content_forbidden, file_content_matches, file_headerrequired or banned content
- Structured configjson_path_equals, yaml_path_equals, json_schema_passesconfig values are correct
- Cross-filepair, generated_file_fresh, import_gatefiles stay consistent
- Encoding, securityno_bidi_controls, no_zero_width_chars, no_merge_conflict_markershidden or unsafe characters
A starter config can be three lines of extends:, and a real project's config usually grows rules of its own:
# .alint.yml (repo root)version: 1extends: - alint://bundled/oss-baseline@v1 # README, LICENSE, SECURITY, hygiene - alint://bundled/rust@v1 # skips itself when there is no Rust - alint://bundled/ci/github-actions@v1 alint check runs every rule against the working tree, and alint fix repairs the mechanical findings, such as trailing whitespace and a missing final newline.
A few things alint doesn't do. It doesn't build syntax trees and won't replace ESLint, Clippy, Ruff or Semgrep; it runs next to them. It doesn't identify a license the way a license detector such as licensee does: a rule can check that a LICENSE file exists, has real content and matches a pattern such as the Apache 2.0 heading, but alint won't tell you which license an arbitrary file holds. Its command rules can hand work to tools already in your project, but those tools bring their own runtimes and costs.
Where alint fits
ESLint, Clippy and Ruff read the code inside your files. They parse a file into a syntax tree and reason about one language at a time. Semgrep and CodeQL work at a related level for security and data-flow patterns (Semgrep's Pro engine adds cross-file analysis). gitleaks and TruffleHog scan repository content and history for secrets. Each of these is anchored to code, or to a property of code.
alint sits one level out, at the filesystem shape. Whether a required file exists, whether a filename follows a convention, whether a version in package.json matches a version in a workflow: none of that lives in a syntax tree, and much of it crosses language boundaries. A per-language linter doesn't see it, because it only reads its own slice of the tree.
| Job | Reach for | Note |
|---|---|---|
| Code and style errors | ESLint, Clippy, Ruff | alint doesn't parse code |
| Security flaws in code | Semgrep, CodeQL | alint isn't a SAST scanner |
| Leaked secrets | gitleaks, TruffleHog | alint isn't a secret scanner |
| Dependency policy | cargo-deny | alint doesn't read the dependency graph |
| Repository structure | alint | which files exist, their names, contents and config values |
VS Code's hygiene.ts is a good example. alint can express several of its checks as declarative rules and hand others to the tools that already run them. A couple, a per-line indentation check and a Unicode rule with special-case exceptions, fit a script better than a rule. VS Code also ships a directory of in-tree ESLint rules, and the ones I read are source-code checks written as syntax-tree visitors, which isn't what alint is for.
yamllint, JSON Schema validators, Conftest, Vale, markdownlint, actionlint and zizmor each cover parts of the config, docs and CI checks. What alint adds is one repository-oriented policy model across all of them, with the option to hand work to the specialists where they're stronger. Which tool for which job goes through them one by one.
What it finds in real repositories
I wrote alint configs for 30 well-known open-source repositories, ran each against the project's tree in May 2026 and wrote each one up (the reference configs). The projects don't use alint. Each example below links the commit I checked.
Reading raw bytes finds things code linters skip. In Flutter, alint flagged 5 documentation files (21 occurrences in all), among them archived release notes, containing U+202C, a bidirectional Unicode control character. That's the class behind CVE-2021-42574, the Trojan Source attack, where text renders differently from how it's interpreted. The researchers who described it demonstrated it across 12 language contexts and reported it working against most of the compilers, editors and repositories they tested; GitHub began warning on bidirectional Unicode in files and pull requests around the disclosure. These are probably harmless, likely contributor names pasted from pull requests, and they'd been there for years. Harmless or not, catching them takes a tool that decodes text and looks at its code points, which a code linter reading for syntax doesn't do.
Structured-config drift is the second pattern. In facebook/react, the published react-refresh package points its package.json repository.directory at packages/react instead of packages/react-refresh. The fix is one word, and it reads like metadata copied from a sibling package. A study of what code review catches (open manuscript) found that about 75% of the defects industrial reviewers raised had no visible effect on program behavior and instead improved evolvability and understandability; a wrong path in a manifest is that kind of defect. A for_each_dir rule with json_path_matches holds each package's repository.directory to its own path, at pull-request time.
The third pattern is where the per-language argument gets concrete: some invariants only exist across languages. In protocolbuffers/protobuf, the language bindings don't share one layout, which rules out a single parity check. The config asserts targeted invariants instead: the Java and Ruby versions in protobuf_version.bzl agree with the Maven BOM and the gemspec, and the generated file lists match what their generators produce. Flutter keeps a BSD header across source files in several languages. No per-language linter sees these, because each knows only its own subtree; alint checks them from one config.
A related pattern is the build that passes while doing less than you meant. VS Code exposes its proposed APIs as declaration files in src/vscode-dts/, one per proposal, each named vscode.proposed.<name>.d.ts; a correctly named file is what regenerates the registry the build reads. Nothing checks the names: one typo in that prefix leaves a .d.ts the discovery glob never matches. The build still succeeds, the proposal is missing, and nothing says so. Every declaration file in that folder is either the stable vscode.d.ts or a vscode.proposed.<name>.d.ts, so a filename_regex rule over every .d.ts in the folder, holding each to one of those two shapes, would turn the silent omission into a failing check.
Istio showed the other shape of it: a check that runs but is too loose to catch what it should. Its copyright check, lint_copyright_banner.sh, requires each source file to contain the strings Copyright and Apache License, Version 2. istioctl/pkg/precheck/precheck.go opened with the placeholder header that the Cobra CLI scaffolder drops in to be replaced, Copyright © 2021 NAME HERE <EMAIL ADDRESS>, above the standard Apache block. Both substrings are there and the check accepts it, though it names no copyright holder. A file_header rule anchored to the expected banner would reject the placeholder. In the same tree, a release note, releasenotes/notes/27430.yaml, declared piVersion where it meant apiVersion. Istio's release-note tool now validates new notes against a schema that requires apiVersion, which would stop this typo in a new note; the 2020 note still has it. A yaml_path_equals rule on apiVersion checks every note, old ones included.
Where a project already scripts its checks, a config can take most of them over. The config I wrote for Kubernetes covers about two thirds of what its 50 verify scripts checked in May 2026: most as alint rules, and the code generators by running Kubernetes' own scripts from alint.
Why not a tool that already exists
Other people have built tools for this. The closest was Repolinter, a repository-policy linter from the Linux Foundation's TODO Group. It could check file existence, contents and more, and it was archived in February 2026.
Its issue tracker records limitations that shaped alint's design: it never honored .gitignore, it hit memory ceilings on large repositories (one report describes a 16 GiB heap), and it leaned on both Node.js and Ruby-backed Licensee and Linguist tooling. Adding a new primitive usually meant changing the core, not the config.
alint is built to avoid them. It's one native executable: installing it is installing one file. It honors .gitignore (walker docs), and it writes SARIF and GitHub annotations itself. It adds what Repolinter didn't have: filename-pattern rules, rules that run for each directory, and cross-file relationships as built-in rule kinds. The bundled oss-baseline ruleset is a starting point for projects coming from Repolinter. The migration guide maps Repolinter's 19 rule types and the 26 checks in its default ruleset; alint fully covers 24 of those 26, and the guide gives the default ruleset as an alint config. The migration is that file plus a review of what to keep from your old setup.
Narrower tools overlap at the edges. ls-lint checks file and directory names, and whether files exist. An earlier version of this post said it did that faster than alint, but the benchmark behind the claim was broken (it ran ls-lint without its config), so I've taken the claim out until I measure it again. Conftest evaluates values inside structured inputs with policy rules, but doesn't walk the tree or know which files exist. Semgrep matches patterns, within a file or across files depending on edition and mode. None of the alternatives I tried combined the same set: language-agnostic, cross-file, content, structure and naming together, in one fast native executable that honors your ignore file.
alint and AI coding agents
Coding agents make this worse. If you use one, you've probably watched it bring back what you just cleaned up: a scratch file in the root, a new package with no README, a workflow that doesn't pin its actions. You can't count on an agent to keep your conventions in mind. Some were never written anywhere a machine can read, and the ones that are, in an AGENTS.md or a CONTRIBUTING file, an LLM follows unevenly. Its adherence slips as the context window fills up (Chroma's 2025 study of 18 models found reliability degrading with input length, even on simple tasks, in line with the older lost-in-the-middle finding that models use the middle of a long context worst), and it slips again as the number of rules it has to satisfy at once grows (accuracy falls with each added instruction). A convention can sit right in the prompt and still be skipped deep into a session.
Some of the drift is measurable, though the evidence is young. On five frontier models, a 2026 preprint (summary) measured invented-package rates of 4.62% to 6.10%. It's a preprint, and older open-source models scored much higher. Separately, a vendor analysis of 211 million changed lines reports rising code duplication and less of the refactoring that keeps a tree coherent.
alint doesn't detect duplicated code today (a duplicate-code detector is on the roadmap), and it doesn't catch invented dependencies in general. What it can do now is make some of the consequences checkable: an allowlist of dependencies, a manifest or lockfile that has to stay consistent, a required path, a generated file that must be fresh, a package that must exist where a config says it does. And it can hand the agent the same rules you enforce everywhere else.
AGENTS.md is the emerging convention for telling an agent how a project works. It helps, but it drifts from what CI enforces. alint export-agents-md --inline --output AGENTS.md writes the active rules into a marked block in that file and leaves your prose around it alone. By default it lists the rules above level: info (--include-info adds those too), each phrased from its message or a description of its kind rather than every parameter. Regenerate the block when .alint.yml changes, and check it in CI if you want to catch drift. Separately, alint check --format agent prints each finding as JSON with the exact fix command where one exists, which an agent can act on directly.
--inline
- **`hygiene-no-macos-junk`**: macOS Finder metadata must not be committed.
- **`gha-pin-actions-to-sha`**: Third-party action is not pinned to a commit SHA.
<!-- alint:end -->
export-agents-md --inline writes: a marked block of the active rules between your own prose. The three lines are from a real run, shortened at the end.alint export-agents-md --inline --output AGENTS.mdalint check --format agentHow fast, and how to start
alint reads files instead of building syntax trees, and does most of its work in one parallel walk, so it's cheap to run. (Cross-file, git-aware and command rules do some work beyond that walk.) The benchmark machine is an old laptop kept for the job: an Intel Core i7-6700HQ with 4 cores, 8 threads and 15 GB of RAM. On it, alint 0.16.0 runs a 50-rule workspace config, the bundled oss-baseline, rust, node, python, monorepo and monorepo/cargo-workspace rulesets on a polyglot tree, in about 156 ms on 10,000 synthetic files, 1.46 s on 100,000 and about 16.5 s on a million. Rules that only look at names and file metadata take about 380 ms on 100,000 files. On a real tree, the 72 rules that didn't call other tools in my nixpkgs config, as it was in May 2026, checked a 52,354-file checkout in 332 ms (alint 0.9.17). The trees and the method are public and rerunnable, and the benchmarks page has each release that changes the linter. At those timings there's little reason not to run it on every pull request; a config that calls heavier tools costs more.
You don't have to clean a repository before adopting alint. alint baseline records today's findings in .alint-baseline.json. Point your config at it (baseline: .alint-baseline.json in .alint.yml, or --baseline on the command line), and from then on alint check fails only on new findings. The recorded ones are hidden from the default output and shown as a count; --show-baselined lists them. You can turn the check on today and pay the backlog down on your own schedule (baseline docs).
alint is pre-1.0. It lints its own repository in CI, and the reference configs are public. The config format can still change, and settling it is what 1.0 is for.
If a convention your repository relies on can't be expressed yet, open an issue; that's how new rule kinds get prioritized.
curl -fsSL https://alint.org/install.sh | bash # Linux or macOSalint init # detects your ecosystem and writes a starter .alint.ymlalint check # run itThe installation guide covers Homebrew, crates.io, PyPI, Docker and building from source, and alint also publishes an npm package and a GitHub Action. The rule catalog, the 30 reference configs and the benchmarks are online, and the source is on GitHub under Apache-2.0 or MIT.
Questions and answers
- Is alint a replacement for ESLint, Clippy or Ruff?
- No. Those parse the code inside your files and reason about one language's syntax and semantics at a time. alint reads the shape of the repository around the code: which files exist, what they're named, what they contain and the values inside config files. It runs next to your existing linters.
- Does alint work with my language?
- It's language-agnostic. It reads files, directory structure and structured config in JSON, YAML, TOML, XML, dotenv,
.properties, INI and HCL. One configuration model covers a polyglot repository and a single-language one alike; scopes, formats and any external commands you wire in still differ per project. It also ships bundled rulesets for Rust, Node, Python, Go, Java, .NET and PHP projects, and for GitHub Actions, monorepos and license compliance; the language rulesets switch themselves off when their language isn't in the tree. - How is it different from Repolinter?
- Repolinter was the closest prior tool, and it was archived in February 2026. alint covers much of the same ground and differs in how: it's one native executable with no Node or Ruby runtime, it honors
.gitignore, it adds cross-file and filename rules Repolinter didn't have, and it writes SARIF and GitHub annotations itself. alint's migration guide maps Repolinter's 19 rule types and the 26 checks in its default ruleset: alint fully covers 24 of those 26 checks, and 5 of the rule types have no alint equivalent. Plan on reviewing your old setup rather than swapping one file for another. - Can I adopt it on a repository that already has violations?
- Yes.
alint baselinerecords today's findings, and from then onalint checkfails only on new ones. - Is it ready for production use?
- It runs in its own repository's CI, and I've written configs for 30 large open-source repositories to test it against real trees. It's pre-1.0, so the config format can still change between minor versions.
- Does alint send any telemetry?
- No. alint makes no network calls at runtime except to fetch configuration you reference with an
https://entry underextends:, and those must be pinned by hash.
Comments