One language or all of them
Point it at any project with nothing to configure. A mixed codebase scores in one pass on one metric, and the same logic gets the same number in every language. That is tested.
Multi-language cognitive complexity linter
Find the code that's hard to read.
In seconds, in one project or a whole monorepo. One static binary that reads JavaScript, TypeScript, Vue, Python, Java, PHP and Go, with no runtime or toolchain for any of them and nothing added to your project.

npx bonsai-lint src/npm install -D bonsai-lintpip install bonsai-lintcomposer require --dev bonsai-lint/bonsai-lintbrew install ryckakas/tap/bonsai-lintgo install bonsai.kauneckas.dev/bonsai-lint@latestcargo install bonsai-lintEvery option, and what each one costs you, on the install page.
One language or all of them
Point it at any project with nothing to configure. A mixed codebase scores in one pass on one metric, and the same logic gets the same number in every language. That is tested.
No runtime, no plugins, no conflicts
Nothing to wire into each language’s own linter and no plugin versions to keep in step.
Never executes your code
Syntax only: no autoloader, reflection or module resolution, so untrusted source is safe to scan.
Callbacks compound
A closure in a loop in a condition scores at its real depth, so a callback pyramid is one hard function, not several easy ones.
Sees the code other tools miss
Scripts, templates, route files and module-level setup are scored too. On a legacy codebase that is often where the worst of it hides.
Adoptable on day one
Baseline existing findings and fail only on regressions.
Worst first. Score, location, and the qualified name of the unit. That is the whole format.
A clean run prints nothing and exits 0.
69 packages/billing/src/invoice-mapper.service.ts:47 InvoiceMapperService::mapLineItems 25 services/api/src/Controller/CheckoutController.php:207 CheckoutController::applyDiscounts 22 packages/web/src/parser/lexer.js:19 LexerThe default threshold is 15, a conventional starting point. Change it for a
run with --over, or for a project in bonsai-lint.toml, because a threshold is a claim about
what your team will tolerate and that claim is yours to make.
backend/Orders.php
foreach ($orders as $order) { // +1 if ($order->isActive()) { // +2 array_map(function ($item) { // +0, nesting 3 if ($item->qty > 0) { // +4 return $item->qty > 10 ? 'bulk' : 'single'; // +5 } }, $order->items()); }}frontend/orders.ts
for (const order of orders) { // +1 if (order.isActive()) { // +2 order.items().map((item) => { // +0, nesting 3 if (item.qty > 0) { // +4 return item.qty > 10 ? 'bulk' : 'single' // +5 } }) }} 12 backend/Orders.php:3 OrderRepository::syncLineItems 12 frontend/orders.ts:1 syncLineItemsIdentical logic, identical number. That is what makes one threshold meaningful across a monorepo, and it is a tested property, not an aspiration.
Files are read, parsed and scored in parallel, and the report is assembled afterwards rather than streamed as results arrive, so two runs of the same commit are byte-for-byte identical however many workers ran. A diff between runs is always a real change.
The VS Code extension analyses the buffer as you type, and agrees with CI by design: same binary, same config, same baselines.

In GitHub Actions, --format github annotates each finding on its line of the diff, and fails
exactly when the text report does. The CI guide has the one-step workflow.
