One language, or all of them
Point it at a PHP project and it is a PHP linter; point it at a TypeScript one and it is a TypeScript linter. Point it at both and they score on one metric in one pass.
Cognitive complexity, for PHP · JavaScript · TypeScript · Vue
Find the code that's hard to read.
In seconds, in one project or a whole monorepo. One static binary, with no PHP runtime, no Node runtime and nothing added to your project.

npx bonsai-lint --over 15 src/npm install -D bonsai-lintbrew install ryckakas/tap/bonsai-lintcargo install bonsai-lintEvery option, and what each one costs you, on the install page.
One language, or all of them
Point it at a PHP project and it is a PHP linter; point it at a TypeScript one and it is a TypeScript linter. Point it at both and they score on one metric in one pass.
No runtime, no plugins, no conflicts
Nothing to wire into a PHPStan or ESLint setup, no plugin versions to keep in step, no
Composer entry. A 6 MB binary, or npx and install nothing at all.
Never executes your code
Syntax-only: no autoloader, no reflection, no module resolution. Safe to point at third-party or untrusted source.
Complexity compounds through callbacks
A closure inside a loop inside a condition is scored at the depth it actually sits at, so a callback pyramid shows up as one hard function instead of several innocent-looking ones.
Sees the code other tools miss
Procedural scripts, templates, route files and module-level initialisation are scored too. On a legacy codebase that is often where the worst of it has been hiding.
Adoptable on day one
Baseline your existing violations and gate on regressions, instead of being told to fix hundreds of functions before you can turn it on.
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.
