ls-lint: a Go binary that enforces directory and filename conventions from one YAML file
An extremely fast directory and filename linter - Bring some structure to your project filesystem
At a glance
- What is it?
- A filesystem linter that checks names rather than code, distributed through npm, Homebrew, Docker and a GitHub Action, with almost no third-party dependencies.
- Who is it for?
- ls-lint solves a narrow problem properly: naming, not style, not imports, not types. Take it when a team keeps hitting review comments about casing in paths, and skip it when the mess you care about lives in file contents.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 7, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Naming conventions as a configuration file, not a hook you write
The concept is narrower than most linters and that is its appeal. ls-lint reads a `.ls-lint.yml` file and checks directory and file names against the rules it declares. Nothing about your code is parsed. The README lists support for directory and file names, all extensions, and full unicode support, and claims it lints thousands of files and directories in milliseconds.
The configuration format nests rules under path patterns. Here is the demo configuration from the README:
ls:
packages/*/{src,__tests__}:
.js: kebab-case
.ts: camelCase | PascalCase
.d.ts: camelCase
.spec.ts: camelCase | PascalCase
.mock.ts: camelCaseRead the structure carefully. The top level `ls` key holds path patterns, written as globs, including brace expansion such as `{src,__tests__}`. Under each pattern, the keys are extensions and the values are rules. A pipe separates alternatives, so `.ts: camelCase | PascalCase` accepts either. Extension keys can overlap: `.d.ts`, `.spec.ts` and `.mock.ts` all end in `.ts`, and the more specific entries exist precisely because a plain `.ts` rule would otherwise capture them.
That specificity is the design idea. A rule that says JavaScript files are kebab-case is a good default, but declaration files, spec files and mock files are conventionally written differently, and the configuration format lets you say so without needing a second tool.
How strict and relaxed modes differ
The second half of the demo configuration shows the two features that are not about casing:
components/*:
.ts: regex:${0}
tests:
.*: exists:0
.test.ts: regex:${1}`regex:${0}` means the file name must match a regular expression, with `${0}` referencing a named capture group declared in the same configuration. `.test.ts: regex:${1}` uses a second one. The `${1}` syntax is the linter's mechanism for passing values between rules, and it is the part that requires the documentation site rather than the README.
The `exists:0` rule on `*` inside the `tests` directory is the other feature, and it is the one that trips people up on existing code. Under the nested `tests` path, `.*` matches everything with `exists:0`, which asserts that no such file exists, and then `.test.ts` re-permits that one extension. The effect is a rule that says a tests directory contains test files and nothing else.
This is where strict and relaxed mode matter, and the README does not explain the difference. In strict mode a name that does not match any rule is reported. In relaxed mode only names that do match a rule are checked, and unmatched names pass. For a new project you want strict, because it catches the stray `MyFile.tsx`. For an existing repository you usually want relaxed, or you will spend your first afternoon renaming files.
The documentation for both modes lives at ls-lint.org, under the 2.2 configuration pages for the basics and the rules.
Five ways to install it, and two dependencies
Distribution is unusually broad for a small Go tool. The README lists support for Windows, macOS and Linux, and then names five channels: an NPM package at `@ls-lint/ls-lint`, a GitHub Action, a Homebrew formula, and Docker, plus the binary itself. That list matters more than it looks, because a naming convention is only enforced if the check runs in CI, and the CI system is whatever your team already uses.
The GitHub Action is the one that turns a convention into a rule. Renaming files to satisfy a linter is wasted work if nothing checks it, and an action that fails the build does check it.
The dependency story is the strongest claim in the README and the one `go.mod` backs up:
module github.com/loeffel-io/ls-lint/v2
go 1.24
require (
github.com/bmatcuk/doublestar/v4 v4.8.1
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sync v0.14.0
)The README advertises almost zero third-party dependencies and names only two: go-yaml and doublestar. The manifest lists three, which adds `golang.org/x/sync`, the standard synchronization primitives module. Both statements are true in the sense that matters, since there is no transitive tree to audit and no web framework or CLI library dragged in behind a YAML parser. The doublestar dependency is what makes the brace expansion in those path globs work.
The repository layout explains the rest: `cmd/` for the entry point, `internal/` for the implementation, `examples/` with a `nuxt_nuxt_js` directory, `third_party/` and `tools/` alongside Bazel build files, `deployments/` for packaging, and a `scripts/` directory. A `.ls-lint.yml` sits at the repository root, which is the tool checking its own naming, and there is an `AI_POLICY.md` at the top level.
Adoption signals, release cadence, and how it compares
The README names three projects that use the tool with their configuration files in the repository: Renovate, Terser, and it says many more. That is more useful evidence than a download count, because each of those is a repository with contributors who would have objected to a convention they disagreed with.
The release history tells a quieter story. v2.3.1 was published on 2025-06-04 and its notes are almost entirely Renovate dependency bumps, with a handful of small refactors, including renaming receivers for the `Error` struct methods, and one functional change, a Windows runner action for the project itself. There is no release body describing a new rule or a behaviour change, which is what you would expect from a tool whose feature set has been stable for a while.
The project then went quiet in terms of tags: the last push was on 2026-09-25, well after v2.3.1, with no new release since. That combination is worth reading carefully. Commits without a release usually mean work on the default branch, on packaging or on internal refactors, rather than a sign of abandonment, and the repository is not archived.
As for what else you would use: a code style linter such as ESLint or Biome checks the contents of files, and ls-lint never looks inside them. A file naming policy enforced by a shell script in CI does the same job with more maintenance. Editor conventions or a rename-on-save plugin catch naming at the moment it is typed rather than at review time. ls-lint's case is that it is the narrowest of these and runs in a few milliseconds, which is what makes it practical to enforce on every change across a large repository.
Editorial conclusion
ls-lint solves a narrow problem properly: naming, not style, not imports, not types. Take it when a team keeps hitting review comments about casing in paths, and skip it when the mess you care about lives in file contents. One decision needs settling before you commit, which is whether you want the strict path mode or the relaxed one. The strict mode rejects names that do not match, while the relaxed mode checks that names which match the pattern do not violate the rule, which sounds similar and behaves very differently on an existing repository. The repository is not archived and the last push was on 2026-09-25, past the v2.3.1 release from 2025-06-04.
Frequently asked questions
What does ls-lint do that ESLint does not?
It checks directory and file names rather than file contents. ESLint and Biome parse your code and report on what is inside it. ls-lint reads a `.ls-lint.yml` file and reports names that do not match the declared casing, regex or existence rules for a given path pattern.
How do I install ls-lint?
The README lists five channels: a binary for Windows, macOS and Linux, an NPM package named `@ls-lint/ls-lint`, a GitHub Action, a Homebrew formula and Docker. Full installation instructions, including the curl route, are on the installation page at ls-lint.org.
What is strict mode versus relaxed mode in ls-lint?
Strict mode reports names that do not match any declared rule, so a stray file with the wrong casing gets caught. Relaxed mode only checks names that do match a rule and lets unmatched names pass, which is the setting most existing repositories need before introducing naming rules.
Can ls-lint enforce naming rules in CI?
There is a GitHub Action for it, listed among the installation channels alongside npm, Homebrew and Docker. The README also names Renovate and Terser as projects using it, both of which keep their `.ls-lint.yml` in their own repositories.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/loeffel-io-ls-lint)