Skip to main content

bonsai-lint

bonsai-lint: a multi-language cognitive complexity linter

Find the code that's hard to read, in seconds, in one project or a whole monorepo.

Bonsai: the art of keeping a tree small enough to take in at a glance. Same idea, applied to your syntax trees.

A multi-language cognitive complexity linter written in Rust. One static binary that currently reads JavaScript, TypeScript, Vue single-file components, Python, Java, PHP and Go. Use it for any one of them, or all at once. No runtime or toolchain for any of them, nothing added to your project. Scans 1.26 million lines in 0.8 seconds.

The official site is bonsai.kauneckas.dev: install and editor guides, and a playground that scores code in your browser.

CI npm PyPI dependencies Rust 1.90+ License: MIT

Why this one

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

Cognitive complexity measures how hard code is to read, where cyclomatic complexity measures how hard it is to test. A switch with twenty arms is cyclomatically awful and cognitively fine; three nested ifs are the reverse.

Install

npx bonsai-lint --over 15 src/            # run it without installing anything
npm install -D bonsai-lint                # or pin it in the project
uvx bonsai-lint --over 15 src/            # the same, for a Python project
pip install bonsai-lint                   # or: uv tool install / pipx install bonsai-lint
brew install ryckakas/tap/bonsai-lint     # macOS and Linux
go install bonsai.kauneckas.dev/bonsai-lint@latest
cargo install bonsai-lint                 # from source
curl -LsSf https://github.com/ryckakas/bonsai-lint/releases/latest/download/bonsai-lint-installer.sh | sh

The npm package fetches the prebuilt binary for your platform on install. Nothing is compiled, and Node only launches it. The analysis itself is pure Rust.

Linux has two builds. One links glibc 2.35 or newer. The other is static and runs on any Linux, which covers Alpine and other musl systems as well as older glibc. The installer script, the npm package and the Go module each pick the right one for you, and the PyPI wheels always carry the static one.

In a Go module, go get -tool bonsai.kauneckas.dev/bonsai-lint@latest pins it in go.mod, and go tool bonsai-lint runs it (Go 1.24+). The Go module is a launcher with no dependencies, at the same version as the CLI. The first run of each version downloads that release's binary, checks it against a checksum recorded in the module, and caches it. The platforms and settings are in bonsai-lint-go.

From PyPI, pip install bonsai-lint, uv tool install bonsai-lint or pipx install bonsai-lint puts the binary on your PATH, and uvx bonsai-lint runs it without installing anything. Each wheel holds the release's own binary, with no Python code around it: macOS on Apple silicon and Intel, Linux on x86_64 and arm64 (glibc 2.17 or newer, or musl), and Windows on x64. There is no source distribution, so any other platform gets pip's "no matching distribution". That includes an ARM64 Python on Windows, where an x64 Python works under emulation, and so does cargo install bonsai-lint.

As a pre-commit hook, from v0.4.2:

repos:
  - repo: https://github.com/ryckakas/bonsai-lint
    rev: v0.4.2
    hooks:
      - id: bonsai-lint
        # args: [--over, "10"]

pre-commit installs the matching wheel itself and runs the hook on the staged files it can score. The hook judges each file exactly as bonsai-lint run from the repository root does, so a bonsai-lint.toml below the root counts only when the root config declares it as a domain. A commit touching only files it skips, such as generated code, passes. pre-commit autoupdate moves rev to the latest release.

There is no Maven or Gradle plugin yet. On a Java project, use Homebrew, the installer script or npx bonsai-lint.

Use it

bonsai-lint src/                     # fail on anything above 15
bonsai-lint --over 10 src/           # stricter; `--over php=10,typescript=20` per language
bonsai-lint --all src/               # every unit, ranked
bonsai-lint --format json src/       # for editors and CI
bonsai-lint --write-baseline src/    # record today's findings, exit 0
bonsai-lint --lang php src/          # one language only; `--help` lists the ids
The rest of the flags: monorepos, CI and editors
bonsai-lint --domain web                       # one declared domain only
bonsai-lint --baseline PATH .                  # one baseline file for the whole repo, wherever you choose
bonsai-lint --config packages/web src/         # discover config from here, not from the first path
bonsai-lint --no-toplevel src/                 # skip code outside any function
bonsai-lint --stdin --stdin-path src/a.php < buffer   # score an unsaved buffer as that file
bonsai-lint --jobs 4 .                         # cap the workers; 0 or absent uses every core
bonsai-lint --allow-no-files a.d.ts gen.go     # pass when none of the paths can be scored
  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  Lexer
The same run as JSON
bonsai-lint --format json .
{
  "thresholds": {
    "go": 15,
    "java": 15,
    "php": 15,
    "python": 15,
    "typescript": 15,
    "vue": 15
  },
  "breaches": 3,
  "findings": [
    {
      "path": "packages/billing/src/invoice-mapper.service.ts",
      "line": 47,
      "name": "InvoiceMapperService::mapLineItems",
      "score": 69,
      "language": "typescript",
      "domain": "root"
    },
    {
      "path": "services/api/src/Controller/CheckoutController.php",
      "line": 207,
      "name": "CheckoutController::applyDiscounts",
      "score": 25,
      "language": "php",
      "domain": "root"
    },
    {
      "path": "packages/web/src/parser/lexer.js",
      "line": 19,
      "name": "Lexer",
      "score": 22,
      "language": "typescript",
      "domain": "root"
    }
  ]
}

findings is already ranked, worst first, so a consumer does not have to sort it. thresholds is keyed by language id and reports what the scan actually applied, which is the domain's threshold rather than the root's when the scan was scoped to one. breaches counts the findings over that threshold, and is what the exit code follows; with --all the array also carries everything under it, and breaches still counts only the ones that failed.

domain names the domain a file resolved to — root when there is no bonsai-lint.toml declaring any — and path is always forward-slashed, so a report generated on Windows compares against one generated in CI.

A clean run prints nothing and exits 0. Exit 1 means a breach, or that the scan was untrustworthy, because a path could not be read or matched no supported file. A gate that cannot read what it was pointed at must not report success.

Supported languages and extensions
Extensions Parsed as Language id
.js, .mjs, .cjs, .jsx, .tsx TypeScript with JSX typescript
.ts, .mts, .cts TypeScript typescript
.vue the <script> blocks only vue
.py, .pyw Python python
.java Java java
.php, .phtml PHP php
.go Go go
*.min.js, *.min.mjs, *.min.cjs skipped, generated output
*.d.ts, *.d.mts, *.d.cts skipped, signatures only
.pyi not scanned, typing stubs
.py whose header says "generated" and "do not edit" skipped, generated output
.java whose header says "generated" and "do not edit" skipped, generated output
.go headed // Code generated … DO NOT EDIT. skipped, generated output

The language id is what --lang, --over LANG=N and a [section] in the config take, so typescript covers every JavaScript and TypeScript file however it is parsed. An id that is not one of these is an error, not a silent no-op.

.js is parsed with the TypeScript grammar, which accepts a superset of JavaScript. Flow annotations are the one thing this misparses.

A declaration file and a minified one are both skipped outright, for opposite reasons: the first holds only signatures so every unit scores zero, and the second is generated output whose whole body rolls up into one unit nobody will refactor. The skipped names are whole suffixes, listed by the TypeScript and TSX descriptors' unscored_suffixes, so app.mini.js, jasmine.js and a file simply called min.js are all still scanned.

A .vue file scores its <script> and <script setup> blocks, with lang="ts" choosing the TypeScript grammar and anything else (including no lang) choosing the JSX-capable one, which is the grammar .js already uses. Each block is parsed on its own, and their file-level code is added together into the one <toplevel> a component reports. Reported lines are lines in the .vue file, so editing a template moves a finding without changing its baseline key.

Construct Scored
<script> and <script setup>, any lang yes, as vue
a render() function written in a script block yes, as vue
JSX inside <script lang="tsx"> yes, as vue
<template>: v-if, v-for, {{ }}, @click="a && b()" no
<style>, and custom blocks such as <docs> or <i18n> no
<script src="./logic.ts"> no, but logic.ts is scanned on its own, as typescript

Two consequences are worth knowing:

  • The template is deliberately not scored. A v-if chain is real branching, but the the metric is defined over script code, and counting a template would make a component incomparable with the same logic written in TypeScript. A render() function is scored, because it is ordinary script code, so moving logic out of a template and into render() makes it visible, and a component that never had a template was never hidden.
  • A src= block belongs to the file it points at. That file is scored as typescript, so a [vue] threshold does not reach it.

A Python method is keyed by its class, OrderService::process. A property's getter, setter and deleter redefine one name on purpose, so the accessor a decorator declares joins the key: Cart::total, Cart::total.setter and Cart::total.deleter are baselined apart. A def whose body is only ..., such as an @overload stub or a protocol method, declares a signature and is not a unit. A .py file is skipped as generated by the same header rule as Java below, which covers protobuf, gRPC and Thrift output. A Django migration says only "Generated by Django", because it is meant to be edited, so it stays scored.

Java overloads freely, so a Java method or constructor is keyed by its parameter types as well as its name: OrderService::process(Order, User). Each type is its simple name, without type arguments or annotations, so two overloads are baselined apart and switching between an import and a qualified name never re-keys one. Java has no single generated-code convention, so a .java file is skipped when the comments before its first line of code say both "generated" and "do not edit". That covers protobuf, Thrift, Avro and JavaCC output. Most generated sources sit in a gitignored target/ or build/ directory and are never scanned in the first place.

Go marks generated code in the file rather than its name, so a .go file is read first and then turned away if a // Code generated … DO NOT EDIT. line comes before its package clause, which is the convention the Go toolchain itself follows. Such a file is neither scored nor counted, on disk and through --stdin alike. vendor/ and testdata/ get no special treatment: if a repository commits them, exclude = ["vendor/**", "**/testdata/**"] keeps them out.

A Go method is keyed by its receiver type, so func (s *Stack[T]) Push() reports as Stack::Push and keeps its baseline entry when the receiver switches between pointer and value.

The same code scores the same in every language
foreach ($orders as $order) {          //  +1
    if ($order->isActive()) {          //  +2
        array_map(function ($item) {   //  +0, nesting is now 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   syncLineItems

Identical logic, identical number. That is what makes one threshold meaningful across a monorepo.

Configure it, or don't

Nothing on disk is required. Defaults are a threshold of 15 for every language, .gitignore respected, top-level code scored. One bonsai-lint.toml at the repository root covers a normal project:

threshold = 15
exclude   = ["vendor/**", "**/*.generated.ts"]

[typescript]
threshold = 20
Every configuration key
Key Default Meaning
threshold 15 Fail above this score. A section named after a language id, such as [typescript], overrides it per language.
exclude [] Globs, relative to the config's own directory. * stops at / and ** crosses it, as in .gitignore.
toplevel true Score code outside any function as <toplevel>.
baseline .bonsai-lint-baseline.json Where this directory's baseline lives, relative to it.
domains none Root config only: globs naming directories that own their own config and baseline.
name its path from the root, such as packages/web A domain's name in output and for --domain. Two domains cannot share one.

A misspelt key is an error rather than a silent default, named with a suggestion when one is close, and so is a negated glob: !pattern is not supported.

Monorepos: per-team thresholds and baselines

Add domains and each team owns its own thresholds and its own baseline, without editing a shared file. The root declares where domains may live, so a stray config cannot quietly create one.

# bonsai-lint.toml at the repository root
domains   = ["packages/*", "services/*"]
threshold = 15
# packages/web/bonsai-lint.toml
name      = "web"
threshold = 20
packages/web/.bonsai-lint-baseline.json       # each domain keeps its own
services/billing/.bonsai-lint-baseline.json

--write-baseline then writes one file per domain and prints what it wrote, or says that there was nothing to record. Baseline keys are relative to the domain root, so they survive being checked out anywhere, on any platform. A domain's own exclude is relative to its root too; the root's applies everywhere.

--baseline PATH keeps every domain in one file of your choosing instead. Its keys are relative to the repository root, so two domains with a src/index.ts cannot collide. Which to use is your call: one file is simpler for a small repository, while per-domain files stay short and put each team's accepted debt in the directory that team owns.

Wherever you point the CLI, the workspace is the same one. Discovery walks up from the first scanned path and takes the outermost bonsai-lint.toml that declares domains as the root, so bonsai-lint packages/web applies the same root excludes, inherited thresholds and domain list as bonsai-lint ., and so does an editor buffer. Without a domains declaration anywhere, the nearest config is the root. --config PATH starts the walk somewhere else.

Baselines: adopt it without fixing everything first
bonsai-lint --write-baseline src/

Records everything currently above the threshold as accepted. Later runs fail only on scores that got worse, or on units the baseline has never seen. An unknown key is treated as a regression, never as an acceptance. A renamed function is reported rather than silently inheriting someone else's amnesty. Two units in one file that share a key, such as a def redefined under if/else, share one entry holding the higher score. Entries that match nothing are reported too, so a baseline cannot quietly rot into a permanent exemption, but only by a scan that covered the whole domain, so a single file from a pre-commit hook or an editor buffer never cries stale.

Without a bonsai-lint.toml, the directory holding the baseline is the project root, so bonsai-lint --write-baseline . followed by bonsai-lint src/Foo.php finds the same entries.

Suppressing a finding: with a reason, or not at all
// bonsai-lint-ignore: hand-tuned state machine, splitting it hurts more than it helps
function parse(string $input): Ast { /* ... */ }

Works above the declaration, inside the docblock, or trailing the signature line, in every supported language. In Python it goes in a # comment above the def or its decorators: a docstring is a string inside the body, not a comment, so it cannot carry a marker. It works for a bound function too, such as const handler = () => {}, Python's handler = lambda e: …, a Java field Runnable handler = () -> {}, $handler = function () {} or Go's var handler = func() {}, even when a call wraps it, as in const useCart = defineStore('cart', () => {}). A marker after a closing brace on its own line belongs to nobody. A marker without a reason is refused, reported on stderr, and the finding stands. Suppression hides a finding but never changes a score, and --all always shows the real number.

A <toplevel> finding is reported on line 1, so its marker goes in the comment block at the top of the file: on the first line of a script, behind a shebang if there is one; in Python, behind a shebang or coding line and above the module docstring; just above or trailing the package line in Java or Go; or trailing <?php, or in the file's docblock, in PHP. One comment silences one unit, so a marker directly above the first function is that function's; write it on the package or <?php line to address the file instead.

Scoring

Three rules: shorthand that doesn't break reading flow is free; +1 for each break in linear flow; +nesting when a flow-breaker sits inside other flow-breakers. Full table in docs/scoring-rules.md.

Code outside a function is still code

Most complexity tools only score functions and methods, so a 200-line procedural template, a route table or a module-level bootstrap is invisible to them. bonsai-lint scores whatever is left over as a <toplevel> unit, one per file:

  84  services/api/templates/report/summary.php:1  <toplevel>
  37  services/api/scripts/migrate-tenants.php:1  <toplevel>
A cognitive complexity warning on a PHP template whose logic sits at file scope, reported as toplevel

Files with no top-level logic report nothing, so this costs you no noise. Turn it off with --no-toplevel or toplevel = false.

Choices that move the numbers

Numbers from bonsai-lint will not always match another implementation. Every difference is a deliberate choice, and this is all of them, so you can judge which suits you:

Elsewhere bonsai-lint
Closure inside a function scored separately, from zero carries the nesting it sits at
??, a?.b +1 free
JSX {cond && <X/>} exempt +1, the same as the equivalent ternary
const x = a || [] exempt +1
Code outside any function not scored scored as <toplevel>
An if inside a plain else block no deeper than the else one level deeper, like any branch body
a && (b || c) && d 2, the group counted as its own run 3, grouping is transparent
a && b || c && d 2, each operator counted once 3, each switch of operator starts a run
A later link of an else if chain one level deeper per link one level below its if, like the first
A loop or switch header one level deeper at the construct's own depth
A method calling itself through its receiver not recursion +1
A builtin sharing a method's name, append() inside append +1, as recursion free
A Python comprehension, [x for x in xs if x] each for and if clause +1, or ignored free, but nests like the callback chain it replaces
A Python loop's or try's else: free +1, like any else
A Python match free +1, like a switch
f(key=a or b), (x or "") + y, f"{a and b}" not counted +1, wherever the operator sits
A function holding only a nested function and its return the inner one keeps its level one level deeper, like any closure
A nested function under an if or with, or a method of a local class no deeper than the code around it one level deeper, like any closure
A bare m() inside Python method m +1, as recursion free: the name is a global
A Python function recursing twice +1 once, and not through self +1 per call, through self, cls or its class
A local closure sharing its function's name resolved to the local +1 per call, matched by name
A Java overload taking as many arguments resolved by type, not recursion +1, matched by name and argument count
A call through another instance of the same type, left.count() +1, as recursion free

The last three rows are limits rather than preferences: recursion is recognised by syntax alone, with no scope or type analysis.

The first row moves numbers the most. Scoring every function from zero means a pyramid of callbacks costs almost nothing:

if (a) { for (const x of xs) { xs.forEach(item => { if (b) { … } }); } }

bonsai-lint reports that as a single unit scoring 7. Scored per function it is two units, at 3 and 1. Neither is wrong. They answer different questions. Per-function tells you how hard each piece is on its own; bonsai-lint tells you how hard the whole thing is to read where it stands. If you care about callback depth, the second is the more useful number.

Migrating? Expect your numbers to move, mostly upward, for these reasons and no others.

Editor

A cognitive complexity warning underlining a TypeScript function name, with the Problems panel showing one diagnostic

The VS Code extension reports diagnostics for every editor language it is configured for. It analyses the buffer as you type, not the file on disk, and it deliberately does not pass a threshold unless you set one, so the editor shows exactly what CI would fail on, your bonsai-lint.toml and baselines included.

Performance

Corpus Time
1.26 million lines of JavaScript, TypeScript and PHP 0.77s
1.51 million lines of Python, CPython's standard library and Django 0.92s
2.29 million lines of Java, Spring Framework and Guava 1.08s
2.85 million lines of Go, its own standard library 0.79s

That is roughly 1.6 million lines per second on the monorepo, three languages in one pass, 1.6 million on Python, 2.1 million on Java and 3.6 million on the Go standard library, all on a ten-core M5. No warm-up, no daemon, no language server. One process, start to finish.

Files are read, parsed and scored in parallel; --jobs bounds that, and --jobs 1 is the same scan on one core, at 3.15s. The report is byte for byte identical either way — findings are collected and ranked after the scan, never printed as they arrive — so a diff of two runs is always a real change, not a scheduling artefact.

One binary, about 2 MB to download and about 8 MB on disk, with every language built in. There is no variant to choose and nothing to enable.

Documentation

License

MIT

Release files for bonsai-lint 0.4.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for bonsai-lint 0.4.2
File
bonsai_lint-0.4.2-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
bonsai_lint-0.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.musllinux_1_1_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64, Linux musl 1.1+ x86-64 Details
bonsai_lint-0.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl Python 3 none Linux glibc 2.17+ ARM64, Linux musl 1.1+ ARM64 Details
bonsai_lint-0.4.2-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
bonsai_lint-0.4.2-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details

Total release size: 9.8 MB

Release files / bonsai_lint-0.4.2-py3-none-win_amd64.whl

Download URL bonsai_lint-0.4.2-py3-none-win_amd64.whl
Size 2.0 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
c3e8e33d625504fca723880e092e67fa1b3a5392f28b9674e58b6ff7682a70bb
BLAKE2b-256 checksum
How to use checksums
bb80ba4e0a85d8dafa2fabd74d76acdb5cc9e56f9000e8f773665c3e4fe4db6b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / bonsai_lint-0.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.musllinux_1_1_x86_64.whl

Download URL bonsai_lint-0.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.musllinux_1_1_x86_64.whl
Size 2.1 MB
Tags Linux glibc 2.17+ x86-64 Linux musl 1.1+ x86-64 Python 3
SHA-256 checksum
How to use checksums
6aa6bd8c05d5c146ac00653abf7daed4f3c7c7defcfa905f2b25cbc1beffadde
BLAKE2b-256 checksum
How to use checksums
a4b135b1ff0122260455266422ff9420e8a6c17030c14739ff1e247947802aa9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / bonsai_lint-0.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl

Download URL bonsai_lint-0.4.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl
Size 1.9 MB
Tags Linux glibc 2.17+ ARM64 Linux musl 1.1+ ARM64 Python 3
SHA-256 checksum
How to use checksums
5846e502d6363665fe7438fc41138ea6484266d8fd3ead98cc526713f64f3e6e
BLAKE2b-256 checksum
How to use checksums
125ff04427cbbb1665fe202e6112ae9f040a4b72c2acba30890c48b6de348151
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / bonsai_lint-0.4.2-py3-none-macosx_11_0_arm64.whl

Download URL bonsai_lint-0.4.2-py3-none-macosx_11_0_arm64.whl
Size 1.8 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
067aa040f62e68d6661e2bc7271f5fe0b98aae2a6d69fe8afcc48edb0e0036da
BLAKE2b-256 checksum
How to use checksums
8c1e1b064e121d001d58a7f2d1469b00e34b618fbf6ad1d8c781c442cdcb98f7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / bonsai_lint-0.4.2-py3-none-macosx_10_12_x86_64.whl

Download URL bonsai_lint-0.4.2-py3-none-macosx_10_12_x86_64.whl
Size 1.9 MB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
5ce7c5e1a5e21268e7975e1bcea7403de5a90aa27015e13ca811b7ac34ee0b4e
BLAKE2b-256 checksum
How to use checksums
9d9ef4a846aff3fb8b8fb798e070ad2df8d3de9f576172b54fd1553531e384aa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

5 release files

0.4.3

5 release files

This release

0.4.2 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page