Skip to content

bee lint

bee lint answers “is this file right” from two directions at once:

Terminal window
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)
# clean

The 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.

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.

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 lint
vue-cli-service lint
eslint . --max-warnings 0

Running 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.

/path/to/app
bee lint --detect # what would run, and why — runs nothing
# node pnpm run lint
# ↳ from: /path/to/app/package.json → scripts.lint
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 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.

exit 0 ran, clean
exit 1 ran, found problems
exit 5 could NOT run — no linter, or it failed to start

Separate 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.

Two flags, because they are two different decisions.

Terminal window
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 include

Linters, 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.
Terminal window
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.

Terminal window
bee lint --rules # conventions only; no toolchain needed
bee lint --fix # the tool's fix mode, where it has one
bee lint --json # for CI, an editor, or another agent
bee lint --timeout 120 # per-tool ceiling (default 300s)