bee lint
bee lint answers “is this file right” from two directions at once:
bee lint src/Catalog/Data/ProductDataSource.cs
# bee — rules for src/Catalog/Data/ProductDataSource.cs# - a DataSource never calls GetCollection# ↳ matched: *DataSource.cs## dotnet format (dotnet)# cleanThe conventions half reports the rules whose globs match these paths — asked before writing, not discovered in review. It is cheap, needs no toolchain, and is what the pre-write hook emits.
The tool half finds the project’s own linter and runs it.
bee does not analyse code
Section titled “bee does not analyse code”It never will. The repository has already chosen a linter, and a second opinion competing with the one the build enforces is worse than none. What bee adds is finding that tool, invoking it the way the project invokes it, and making the answer the same on every machine and for every agent.
Declared beats guessed
Section titled “Declared beats guessed”This is the design, not a detail. Measured across the JavaScript repositories on
one machine: thirteen of eighteen declare a lint script, and several are
nothing a detector would invent —
pnpm --filter admin-dashboard lintvue-cli-service linteslint . --max-warnings 0Running eslint . in those is not an approximation of the project’s check. It is
a different check, answered confidently.
So bee reads the manifest first, falls back only to a linter the repository has configured, and returns nothing when the project has chosen nothing. A Python project with no ruff config gets no plan rather than several hundred findings about a style nobody agreed to.
bee lint --detect # what would run, and why — runs nothing# node pnpm run lint# ↳ from: /path/to/app/package.json → scripts.lintWhat it detects
Section titled “What it detects”| stack | tool | from |
|---|---|---|
| .NET | dotnet format --verify-no-changes |
.sln / .csproj — ships in the SDK, nothing to install |
| JS/TS | the declared lint script, else a configured eslint or biome |
package.json, lockfile picks the package manager |
| Dart / Flutter | dart analyze / flutter analyze |
pubspec.yaml, analysis_options.yaml |
| Kotlin / Android | ktlintCheck, detekt or lint, via ./gradlew |
the build file — only a task that exists |
| Swift | swiftlint |
.swiftlint.yml only |
| Go | golangci-lint, else go vet ./... |
.golangci.yml / go.mod |
| Python | ruff check |
[tool.ruff] or ruff.toml only |
Adding an ecosystem is a dependency-injection registration, not an edit to a switch someone has to remember.
The boundary
Section titled “The boundary”The search for a manifest walks up from the file, and stops at the first directory that is a project of any kind.
That rule was written after a live failure. Asked about a path inside a Vue
checkout, an earlier version walked past the checkout and answered
dotnet format proj.csproj from a stray project file at the workspace root.
Every step was defensible — the file existed, the walk was bounded by the git
root, the git root really did contain a .csproj — and the answer was a .NET
formatter for a Vue file.
It also gets monorepos right for free: from apps/web/src/Thing.vue the boundary
is apps/web, so the workspace package’s own script wins over the root’s.
“Could not run” is not “clean”
Section titled ““Could not run” is not “clean””exit 0 ran, cleanexit 1 ran, found problemsexit 5 could NOT run — no linter, or it failed to startSeparate on purpose. A caller gating on lint must be able to tell “found problems” from “there was no linter” — collapsing them is how a missing tool starts reading as a passing build.
Installing what is missing
Section titled “Installing what is missing”Two flags, because they are two different decisions.
bee lint --install # the PROJECT's own dependencies (node_modules, from its lockfile)bee lint --install-machine # also install a missing linter onto THIS MACHINE--install writes inside the checkout, from a lockfile the repository committed
— what cloning it would do anyway. --install-machine changes your computer, so
it is never implied.
An install is not believed because the installer exited 0. Homebrew exits 0
for “already installed”; go install writes to GOBIN, which is regularly not on
PATH. So bee re-asks the question that failed in the first place, and reports
this honestly:
'go' finished, but 'golangci-lint' is still not on PATH —it may have installed somewhere PATH does not includeLinters, never toolchains. A missing SwiftLint is a small binary. A missing .NET or Flutter SDK is a multi-gigabyte decision about your machine, usually with a version the rest of your work depends on. bee refuses those by name:
the .NET SDK is a toolchain, not a linter — bee will not install it.One answer for the whole team
Section titled “One answer for the whole team”bee config set lint.command "pnpm -r lint"Detection is a good guess; a team’s answer is not a guess. Once lint.command is
set in the shared store, every agent on every machine pointed at it runs the same
check — which is exactly what a per-machine shell alias cannot fix, because each
machine’s alias is right about its own machine.
The value is split for quotes and never handed to a shell: it lives in a store several people can write to, and shelling it would make “set a lint command” and “run anything on a teammate’s machine” the same permission.
Everything else
Section titled “Everything else”bee lint --rules # conventions only; no toolchain neededbee lint --fix # the tool's fix mode, where it has onebee lint --json # for CI, an editor, or another agentbee lint --timeout 120 # per-tool ceiling (default 300s)