Linting the Shape of a Repository

aleqs1 pts0 comments

Linting the shape of a repository | alint blog<br>Linting the shape of a repository<br>August 6, 2026<br>On this page<br>The rules your repository assumes but never checks<br>What alint is<br>Where alint fits<br>What it finds in real repositories<br>Why not a tool that already exists<br>alint and AI coding agents<br>How fast, and how to start<br>Frequently asked questions<br>On this pageThe rules your repository assumes but never checks<br>What alint is<br>Where alint fits<br>What it finds in real repositories<br>Why not a tool that already exists<br>alint and AI coding agents<br>How fast, and how to start<br>Frequently asked questions<br>Most repositories run on rules that few tools enforce automatically. A package is supposed to ship a README. No build output is 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 actually contain license text. You know these rules. You have corrected pull requests that broke them. What you probably do not have is anything that checks them.

The rules your repository assumes but never checks

Conventions like these tend to live in one of three places, and each one has a hole.

The first is a maintainer’s head. That works until the maintainer is on vacation, or leaves, or the project grows past the point where one person reads every diff.

The second is a document: a CONTRIBUTING file, a wiki page, an onboarding guide. Prose is not enforcement. The rule is written down and drifts anyway, because nothing fails when someone ignores it.

The third is a pile of bespoke scripts, and this is what large or mature projects often add. You can see it in the biggest repositories on GitHub. VS Code ships build/hygiene.ts, an in-tree program that checks copyright headers, indentation, product metadata, and a few other things. Kubernetes has around fifty hack/verify-*.sh scripts wired into make verify. These work. The catch is that each one is specific to its repository, and many of them wrap other programs (gofmt, golangci-lint, shellcheck) rather than standing alone, so the convention does not transfer to the next project.

At the other extreme, some projects keep little of that machinery in the tree. At the revision I inspected, golang/go had no GitHub Actions workflows, no top-level Makefile, and no .golangci.yml; its structural conventions are not expressed as in-tree declarative policy. They are real and strict all the same, enforced through Gerrit code review together with the TryBots and LUCI submit infrastructure that run outside the repository tree. Between bespoke in-tree scripts on one side and review-plus-external-automation on the other, few projects run a single portable tool aimed squarely at this layer.

This is neither small nor hypothetical. In March 2025 a single compromised GitHub Action, tj-actions/changed-files, was pulled into over 23,000 repositories through a moving version tag (incident analysis); the repositories that had pinned it to a full commit SHA got the old, safe code and were untouched. A 2026 Datadog analysis found that 71 percent of organizations pin none of their actions that way, and even a direct pin does not cover unpinned transitive dependencies. Step from security to ordinary hygiene and the shape repeats. An OpenSSF scan of about 1.02 million packages (roughly 832,000 on npm and 191,000 on PyPI) found that only about three percent published a security policy, and that the missing-license rate splits sharply by ecosystem: about 32 percent on npm against about 12 percent on PyPI. These are conventions that are widely agreed on and unevenly enforced.

What alint is

alint is a linter for the shape of a repository, and it is the tool I built to close that gap. It checks which files exist, how they are named, what they contain, the values inside your config files, and how files relate to one another, among other repository-level checks. You describe the intended shape once, most often in a single .alint.yml at the repo root, and alint check enforces it. It ships as one native executable with no language runtime to install, and because it reads files rather than building source-language syntax trees, 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: 1

rules:

- 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: ["**/*.md"]

level: error

Underneath, alint ships 89 rule kinds as of v0.14.2 (full catalogue). They cover existence, naming, content, values inside structured config, relationships between files, and encoding or security checks such as the Trojan-Source (bidirectional-control) scan. Those are six ways to group the kinds, drawn from a formal catalogue of 13 families. You compose...

alint repository checks shape rules repositories

Related Articles