Skip to content

Goldziher/polylint

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

polylint - universal linter and formatter

The polyglot lint and format pipeline for whole repositories.

Polylint ships the poly CLI: one config, one Rust pipeline, curated in-process backends, tree-sitter fallback for everything else, and repo-wide cache + parallel execution. No language runtime is required for the default path; gofmt and rustfmt are used when present, and other external tools are opt-in.

Lint + format · one poly.toml · pure Rust default · blake3 cache · rayon parallelism · hooks + commit checks · JSON + TOON + MCP

CI npm PyPI License: MIT

Install · Quickstart · What You Get · How It Works · Backends · CLI


Quickstart

$ poly fmt --check
would reformat crates/example/src/main.rs

1 file(s) will change of 1 file(s)

$ poly fmt --fix
reformatted crates/example/src/main.rs

1 changed of 1 file(s)

$ poly lint --format toon
path: crates/example/src/main.rs
diagnostics[0]: engine=ruff, code=F401, severity=warning, title="`os` imported but unused"

$ poly hooks install
✓ Installed 10 git hooks in .git/hooks
  › commit-msg
  › pre-commit
  › pre-push

poly fmt is a dry run by default (CI-friendly); add --fix to write changes, and poly lint --fix to apply lint autofixes. poly hooks install wires the git hooks once — lint, format, and commit checks then run on every git commit.


What You Get

Capability What it does Main surfaces
Repo-wide lint + format Discovers files, routes each language to the best available backend, and reports normalized diagnostics and formatting drift. poly lint · poly fmt
One config poly.toml drives linting, formatting, hooks, commit-message policy, cache settings, and optional tool catalog entries. [defaults] · [lint.*] · [fmt.*] · [hooks] · [tools]
Curated Rust backends Wraps high-quality Rust libraries in-process: oxc, ruff internals, taplo, rumdl, sqruff, malva, markup_fmt, mago, and more. Backend registry
Generic fallback Uses tree-sitter-language-pack for identified languages without a dedicated backend, reindenting supported grammars and normalizing whitespace where safe. treesitter tier
Cache + parallelism Runs per file with rayon and skips unchanged work with a blake3 content-hash cache keyed by file bytes, engine, version, and resolved config. poly cache · --no-cache · -j
Git hooks Runs first-class builtins and inline hook jobs from poly.toml, with file-safety checks and Cargo tools available as builtins. poly hooks install · poly hooks run
Commit checks Enforces Conventional Commits and strips AI-attribution trailers through the bundled gitfluff engine. poly commit
Agent-friendly output Emits structured JSON and compact TOON, and exposes lint/format/cache operations over an MCP stdio server. --format json · --format toon · poly mcp
Optional breadth tier Enables tools from the embedded mdsf catalog only when you opt in; commands are PATH-probed and skipped when absent. [tools.<name>]
Simple distribution Installs prebuilt release archives containing the poly binary, verified by release checksums. Installer · GitHub Action · Homebrew · npm · PyPI

Installation

Polylint is distributed like ruff or biome: prebuilt release artifacts plus thin installers and package wrappers. The workspace crates are not published to crates.io.

Installer Scripts

curl -fsSL https://raw.githubusercontent.com/Goldziher/polylint/main/install.sh | sh

Windows PowerShell:

irm https://raw.githubusercontent.com/Goldziher/polylint/main/install.ps1 | iex

Both installers detect the platform, download the matching release archive, verify it against sha256sums.txt, and install poly. Set POLY_VERSION=v0.4.0 to pin a version or POLY_INSTALL_DIR=/path/to/bin to choose the destination.

GitHub Actions

- uses: Goldziher/polylint@v0
  with:
    version: latest

The action resolves the requested release, caches the installed binary bundle by version and platform, and adds poly to PATH.

Package Managers

brew install Goldziher/tap/polylint
npm install -g @nhirschfeld/polylint
pip install polylint
cargo binstall --git https://github.com/Goldziher/polylint poly-cli

The npm and PyPI packages are thin wrappers that download the verified prebuilt binary bundle for your platform.

Manual or Source Builds

Download a release archive from GitHub Releases, or build from source:

git clone https://github.com/Goldziher/polylint
cd polylint
cargo build --release

Source builds place the binary at target/release/poly.


How It Works

Pipeline

poly discovers files once, plans engines once per language, prefetches the generic tier's tree-sitter grammars, and then runs the per-file work in parallel. Each backend returns the same Diagnostic and FormatOutput shapes, so reporting, cache behavior, and MCP output stay uniform.

flowchart LR
  A["paths"]
  B["discover<br/>gitignore aware"]
  C["plan engines<br/>per language"]
  D["rayon file loop"]
  E["blake3 cache"]
  F["lint / format<br/>reports"]
  A --> B --> C --> D
  D <-->|hit / miss| E
  D --> F
Loading
Zero-dependency default

The default path does not require Python, Node, Go, a JVM, or a project-local toolchain. Most backends are Rust crates compiled into the binary. Two canonical native formatters are default-on when present: gofmt for Go and rustfmt for Rust. If either is missing, the language falls back to the generic tier. zig fmt, shfmt, shellcheck, and catalog tools are opt-in and are skipped when absent.

Cache and debug data

The result cache is keyed by file bytes, engine name, engine version(), and resolved engine configuration. A tool upgrade or config change invalidates stale entries. --debug reports per-file engine timing and cache hit/miss data in pretty output and attaches it to JSON/TOON output.


Configuration

Polylint discovers the nearest poly.toml. polylint.toml is still read as a fallback for older projects, and poly.local.toml can layer local overrides over the primary config. In a monorepo, nested poly.toml files cascade — see Nested config in a monorepo.

[defaults]
line_length = 120
line_ending = "lf"
final_newline = true
trim_trailing_whitespace = true

[discovery]
# Gitignore-style globs pruned from the file walk on every direct
# `poly lint` / `poly fmt` run (the CI and GitHub Action path), on top of
# `.gitignore` and the built-in vendored/generated prune set.
exclude = ["test_apps/**", "docs/snippets/**", "artifacts/**"]

[fmt.python.ruff]
docstring_code_format = true
docstring_code_line_length = 120

[lint.python.ruff]
select = ["E", "F", "W"]

# All tools support uniform `select`/`ignore` for rule filtering (rule codes or
# category names). Some backends (mago, R) support per-rule overrides under
# `[lint.<lang>.<tool>.rules.<id>]` for backend-specific configuration.
[lint.php.mago]
select = ["correctness", "security"]   # categories or rule codes
ignore = ["no-else-clause"]
php_version = "8.2"

[lint.php.mago.rules.cyclomatic-complexity]
level = "warning"   # error | warning | info | hint (mago, R only)
threshold = 20

# Suppress specific rules per path glob (lint-only), across every backend.
[per-file-ignores]
"tests/**" = ["F401"]
"**/*.generated.php" = ["correctness"]

[hooks]
stages = ["pre-commit", "commit-msg"]

[hooks.builtin]
polylint = true
polyfmt = true
commit = { stages = ["commit-msg"] }
file_safety = true
cargo = true

Nested config in a monorepo

Run poly from a monorepo root and each sub-project's poly.toml cascades over the root, the way ruff and eslint resolve config (see ADR 0018). A nested config declares only the diff — it inherits [defaults], the [lint.*]/[fmt.*] rule tables, and [per-file-ignores] from its ancestors, up to the workspace root:

# repo/poly.toml — the workspace root
[workspace]
root = true            # stops the upward cascade here (a repo's `.git` dir is
                       # an implicit boundary too, so this is optional in a repo)

[defaults]
line_length = 120

[lint.python.ruff]
select = ["E", "F", "W"]
# repo/frontend/poly.toml — governs repo/frontend/** only
[defaults]
line_length = 100      # overrides the root; ruff select is inherited

[per-file-ignores]
"*.spec.ts" = ["no-console"]   # glob is relative to repo/frontend/

Resolution rules:

  • Rules and defaults cascade (root → child, deep-merged; the nearest config wins).
  • [discovery] exclude globs are additive across the tree — each config's excludes prune its own subtree, so a parent exclude already covers its children.
  • [per-file-ignores] globs are relative to the directory of the config that declares them.
  • --config <path> pins one config for the whole run and bypasses nested resolution.

Optional Catalog Tools

Opt into tools from the embedded mdsf catalog only when you want them:

[tools.prettier]
enabled = true
languages = ["javascript", "typescript"]

[tools.black]
enabled = true
languages = ["python"]

Catalog tools are capability-probed on PATH; a missing binary is skipped instead of making the whole run fail.

Hooks

Install poly's git hooks once — they then run on every git commit:

poly hooks install

Hooks come from poly.toml: builtins plus inline jobs. poly never clones or runs foreign pre-commit repositories.

Builtin hooks
Builtin Runs
polylint poly lint over the staged files
polyfmt poly fmt --check over the staged files
commit Conventional Commit + AI-trailer check on the commit message (gitfluff)
file_safety Pure-Rust checks: merge-conflict markers, added large files, private keys, case conflicts, and shebang/executable parity
cargo Whole-workspace cargo clippy, cargo sort, cargo machete, and cargo deny — each PATH-probed and skipped when absent

Add an inline job for anything else — it wraps an existing script or task target, no plugin needed:

[hooks.pre-commit.scripts.docs]
script = "scripts/check-docs.sh"
runner = "bash"
files = "**/*.md"

Backend Coverage

Polylint uses a tiered model:

  1. Curated Rust backends for high-fidelity lint and format support.
  2. Native-toolchain backends for canonical first-party formatters when configured or present.
  3. Tree-sitter generic formatting for identified languages without a dedicated backend.
  4. Optional catalog tools from the embedded mdsf registry.
Language or files Backend Lint Format
JavaScript / TypeScript / JSX / TSX oxc yes yes
JSON / JSONC oxc parse diagnostics + formatter yes yes
Python ruff internals yes yes
TOML taplo yes yes
Markdown rumdl yes yes
SQL sqruff yes yes
YAML saphyr + pretty_yaml yes yes
CSS / SCSS malva (format) + biome (lint) yes yes
Less malva no yes
HTML / Vue / Svelte / Astro / Angular / templates / XML markup_fmt no yes
GraphQL graphql-parser + pretty_graphql (parse-error lint + format) + biome (rule lint) yes yes
HCL / Terraform hcl-edit + hcl-rs, tree-sitter for comment-preserving format fallback yes yes
Dockerfile dockerfile-parser hadolint-style rules yes no
Nix alejandra no yes
Ruby rubyfmt no yes
PHP mago yes yes
R tree-sitter generic tier no best effort
Go gofmt when present, tree-sitter fallback otherwise no yes
Rust rustfmt when present, tree-sitter fallback otherwise no yes
Zig opt-in zig fmt, tree-sitter fallback otherwise no yes
Shell opt-in shellcheck + shfmt, tree-sitter fallback otherwise optional optional
All text files typos spell-check yes no
Other identified grammars tree-sitter generic tier no best effort

Unsupported or unknown file types are skipped unless tree-sitter-language-pack can identify them. Some whitespace-sensitive data, template, or patch grammars intentionally no-op rather than risk a destructive rewrite.

Beyond the dedicated backends above, the generic tree-sitter tier identifies and best-effort formats hundreds of grammars — including first-class detection for Java, Kotlin, C/C++, Elixir, Protobuf, and the long tail covered by tree-sitter-language-pack.

Optional Tool Catalog

For everything else, opt into tools from the embedded mdsf catalog. Entries are PATH-probed and skipped when absent, so enabling one never breaks a run:

[tools.prettier]
enabled = true
languages = ["javascript", "typescript"]
Embedded tool catalog (348 tools across 175 languages)

Opt in per tool with [tools.<name>] enabled = true. Each command is probed on PATH and skipped when absent, so listing one never makes a run fail.

Tool Type Languages
action-validator linter yaml
actionlint linter yaml
air formatter r
alejandra formatter nix
alex spell-check markdown
ameba linter crystal
ansible-lint linter ansible
api-linter linter protobuf
asmfmt formatter go
astyle formatter c, c#, c++, java, objective-c
atlas formatter hcl
auto-optional formatter python
autocorrect spell-check
autoflake linter python
autopep8 formatter python
bashate formatter bash
beancount-black formatter beancount
beautysh formatter bash, shell
bibtex-tidy formatter bibtex
bicep formatter bicep
biome formatter, linter javascript, json, typescript, vue
black formatter python
blade-formatter formatter blade, laravel, php
blue formatter python
bpfmt formatter blueprint
brighterscript-formatter formatter brighterscript, brightscript
brittany formatter haskell
brunette formatter python
bslint linter brightscript, brightscripter
buf formatter protobuf
buildifier formatter bazel
c3fmt formatter c3
cabal formatter cabal
cabal-fmt formatter cabal
cabal-gild formatter cabal, haskell
cabal-prettify formatter cabal
caddy formatter caddy
caramel formatter caramel
cedar formatter cedar
cfn-lint linter cloudformation, json, yaml
checkmake linter makefile
clang-format formatter c, c#, c++, java, javascript, json, objective-c, protobuf
clang-tidy linter c++
clj-kondo linter clojure, clojurescript
cljfmt formatter clojure
cljstyle formatter clojure
cmake-format formatter cmake
cmake-lint linter cmake
codeql formatter codeql
codespell spell-check
coffeelint linter coffeescript
cppcheck linter c, c++
cpplint linter c++
crlfmt formatter go
crystal formatter crystal
csharpier formatter c#
css-beautify formatter css
csscomb formatter css
csslint linter css
cue formatter cue
cueimports formatter cue
curlylint linter django, html, jinja, liquid, nunjucks, twig
d2 formatter d2
dart formatter, linter dart, flutter
dcm formatter, linter dart, flutter
deadnix linter nix
deno formatter, linter javascript, json, typescript
dfmt formatter d
dhall formatter dhall
djade formatter django, python
djangofmt formatter django, html, python
djlint formatter, linter handlebars, html, jinja, mustache, nunjucks, twig
docformatter formatter python
dockerfmt formatter docker
dockfmt formatter docker
docstrfmt formatter python, restructuredtext, sphinx
doctoc formatter markdown
dotenv-linter linter env
dprint formatter
dscanner linter d
dune formatter dune, ocaml, reasonml
duster formatter, linter php
dx formatter rsx, rust
easy-coding-standard formatter, linter php
efmt formatter erlang
elm-format formatter elm
eradicate linter python
erb-formatter formatter erb, ruby
erg linter erg
erlfmt formatter erlang
eslint linter javascript, typescript
fantomas formatter f#
fish_indent formatter fish
fixjson formatter, linter json, json5
floskell formatter haskell
flynt formatter python
fnlfmt formatter fennel
forge formatter solidity
fortitude linter fortran
fortran-linter formatter, linter fortran
fourmolu formatter haskell
fprettify formatter fortran
futhark formatter futhark
fvm formatter, linter dart, flutter
gci formatter go
gdformat formatter gdscript
gdlint linter gdscript
gersemi formatter cmake
ghokin formatter behat, cucumber, gherkin
gleam formatter gleam
gluon formatter gluon
gofmt formatter go
gofumpt formatter go
goimports formatter go
goimports-reviser formatter go
golangci-lint formatter, linter go
golines formatter go
google-java-format formatter java
gospel spell-check go
grafbase linter graphql
grain formatter grain
hadolint linter dockerfile
haml-lint linter haml
hclfmt formatter hcl
hfmt formatter haskell
hindent formatter haskell
hledger-fmt formatter hledger
hlint linter haskell
hongdown formatter markdown
html-beautify formatter html
htmlbeautifier formatter erb, html, ruby
htmlhint linter html
hurlfmt formatter hurl
imba formatter imba
inko formatter inko
isort formatter python
janet-format formatter janet
joker formatter, linter clojure
jq formatter json
jqfmt formatter jq
js-beautify formatter javascript
json5format formatter json, json5
json_repair linter json
jsona formatter, linter jsona
jsonlint formatter, linter json
jsonnet-lint linter jsonnet
jsonnetfmt formatter jsonnet
jsonpp formatter json
juliaformatter_jl formatter julia
just formatter just
kcl formatter kcl
kdlfmt formatter kdl
kdoc-formatter formatter kotlin
keep-sorted formatter
ktfmt formatter kotlin
ktlint linter kotlin
kube-linter linter kubernetes, yaml
kulala-fmt formatter http
leptosfmt formatter rust
liquidsoap-prettier formatter liquidsoap
luacheck formatter lua
luaformatter formatter lua
luau-analyze linter luau
mado linter markdown
mago formatter, linter php
markdownfmt formatter markdown
markdownlint linter markdown
markdownlint-cli2 linter markdown
markuplint linter html
mbake formatter, linter make
md-padding formatter markdown
mdformat formatter markdwon
mdsf formatter markdown
mdslw formatter markdown
meson formatter meson
mh_lint linter matlab
mh_style formatter matlab
mise tool
misspell spell-check
mix formatter elixir
mojo formatter mojo
muon formatter, linter meson
mypy linter python
nasmfmt formatter assembly
nginxbeautifier formatter nginx
nginxfmt formatter nginx
nickel formatter nickel
nimpretty formatter nim
nixfmt formatter nix
nixpkgs-fmt formatter nix
nomad formatter hcl
nph formatter nim
npm-groovy-lint formatter, linter groovy
nufmt formatter nushell
ocamlformat formatter ocaml
ocp-indent formatter ocaml
odinfmt formatter odin
oelint-adv linter bitbake
opa formatter rego
openapi-format formatter json, openapi, yaml
ormolu formatter haskell
oxfmt formatter javascript, typescript
oxlint linter javascript, typescript
packer formatter hcl
panache formatter markdown, pandoc, quarto, rmarkdown
pasfmt formatter delphi, pascal
perflint linter python
perltidy formatter perl
pg_format formatter sql
php-cs-fixer formatter, linter php
phpcbf formatter php
phpinsights linter php
pint formatter, linter php
pkl formatter pkl
prettier formatter angular, css, ember, graphql, handlebars, html, javascript, json, less, markdown, scss, typescript, vue
prettierd formatter angular, css, ember, graphql, handlebars, html, javascript, json, less, markdown, scss, typescript, vue
pretty-php formatter php
prettypst formatter typst
prisma formatter prisma
proselint spell-check
protolint linter protobuf
ptop formatter pascal
pug-lint linter pug
puppet-lint linter puppet
purs-tidy formatter purescript
purty formatter purescript
pycln formatter python
pycodestyle linter python
pydoclint linter python
pydocstringformatter formatter python
pydocstyle formatter python
pyflakes linter python
pyink formatter python
pylint linter python
pymarkdownlnt formatter, linter markdown
pyment formatter python
pyrefly linter python
pyupgrade linter python
qmlfmt formatter qml
qmlformat formatter qml
qmllint linter qml
quick-lint-js linter javascript
raco formatter racket
reek linter ruby
refmt formatter reason
reformat-gherkin formatter gherkin
refurb linter python
regal linter rego
reorder-python-imports formatter python
rescript formatter rescript
revive linter go
roc formatter roc
rstfmt formatter restructuredtext
rubocop formatter, linter ruby
rubyfmt formatter ruby
ruff formatter, linter python
rufo formatter ruby
rumdl formatter, linter markdown
rune formatter rune
runic formatter julia
rustfmt formatter rust
rustywind formatter html
salt-lint linter salt
scala formatter scala
scalafmt formatter scala
scalariform formatter scala
selene linter lua
semistandard formatter, linter javascript
shellcheck linter bash, shell
shellharden linter bash, shell
shfmt formatter shell
sleek formatter sql
slim-lint linter slim
smlfmt formatter standard-ml
snakefmt formatter snakemake
solhint linter solidity
sphinx-lint linter python, restructredtext
sql-formatter formatter sql
sqlfluff formatter, linter sql
sqlfmt formatter sql
sqlint linter sql
sqruff formatter, linter sql
squawk linter postgresql, sql
standardjs formatter, linter javascript
standardrb formatter, linter ruby
statix linter nix
stylefmt formatter css, scss
stylelint linter css, scss
stylish-haskell formatter haskell
stylua formatter lua
superhtml formatter html
svlint linter systemverilog
swift-format formatter swift
swiftformat formatter swift
swiftlint linter swift
taplo formatter toml
tclfmt linter tcl
tclint linter tcl
templ formatter go, templ
terraform formatter terraform
terragrunt formatter hcl
tex-fmt formatter latex
textlint spell-check
tlint linter php
tofu formatter terraform, tofu
tombi formatter, linter toml
toml-sort formatter toml
topiary formatter
tryceratops linter python
ts-standard formatter, linter typescript
tsp formatter typespec
tsqllint linter sql
twig-cs-fixer formatter, linter twig
twigcs linter php, twig
txtpbfmt formatter protobuf
ty linter python
typos spell-check
typstfmt formatter typst
typstyle formatter typst
ufmt formatter python
uiua formatter uiua
unimport formatter python
usort formatter python
v formatter v
vacuum linter json, openapi, yaml
verusfmt formatter rust, verus
veryl formatter veryl
vhdl-style-guide formatter vhdl
vint linter vimscript
wa formatter wa
wfindent formatter fortran
write-good linter
xmlformat formatter xml
xmllint linter xml
xo linter javascript, typescript
xq formatter html, xml
yamlfix formatter yaml
yamlfmt formatter yaml
yamllint linter yaml
yapf formatter python
yard-lint linter ruby
yew-fmt formatter rust
yq formatter yaml
zig formatter zig
ziggy formatter ziggy
zprint formatter clojure, clojurescript
zsweep linter zsh
zuban linter python

CLI Reference

lint and format
poly lint [PATHS]...
poly fmt [PATHS]...

  --fix                        Apply lint fixes or formatting in place.
  --check                      Explicit fmt dry run. This is the default.
  --format <pretty|json|toon>  Output format. Default: pretty.
  --config <PATH>              Use an explicit config file.
  --exclude <GLOB>             Exclude paths from discovery (repeatable; merged
                               with `[discovery] exclude`).
  --no-cache                   Bypass the result cache.
  -j, --jobs <N>               Parallel jobs. Default: logical cores.
  --no-color                   Disable colored output.
  --verbose                    Pretty output includes descriptions, URLs, and metadata.
  --debug                      Include cache hit/miss and timing data.

Exit codes:

Code Meaning
0 No issues, no formatting drift, or all writes succeeded
1 Lint findings remain, or dry-run formatting would change files
2 Internal error such as config or I/O failure
commit, hooks, cache, and MCP
poly commit "feat: add backend"
poly hooks install
poly cache stats
poly cache size
poly cache gc
poly cache clean
poly mcp --config /path/to/poly.toml
poly migrate               # dry-run: report what would move into poly.toml
poly migrate --write       # absorb tool configs into poly.toml, remove redundant files

poly migrate folds settings from ruff/taplo/markdownlint/typos config files (and pyproject.toml [tool.ruff]/[tool.typos]/[tool.codespell]) into poly.toml, then deletes or strips only the sources poly can fully honor — files it delegates to (rustfmt.toml, .golangci.yml, clippy.toml, …) and anything not fully representable are kept. It is a dry-run report by default; --write applies, --recurse walks nested projects, and --verify re-runs lint/format after writing.

The MCP server exposes tools for lint, format, and cache operations. Read-only tools are lint, format_check, and cache_stats; mutating tools are lint_fix, format_write, and cache_clean. The lint/format tools accept paths, exclude (gitignore-style glob patterns, merged with config), and config (explicit config file path) parameters for full feature parity with the CLI. Every MCP operation returns the same JSON shape as the corresponding CLI command with --format json.


Workspace Layout

crates/
├── polylint-core/   # Engine trait, registry, discovery, runner, reports
├── poly-config/     # poly.toml schema and config loading
├── poly-cli/        # poly umbrella CLI
├── gitfluff/        # Conventional Commit linter
├── poly-hooks/      # git-hook runner
├── poly-mcp/        # MCP stdio server
├── poly-cache/      # blake3 result cache
├── poly-catalog/    # embedded mdsf tool catalog
└── conformance/     # differential test harness

Contributing

Keep changes small and test-backed. New or changed backends should include representative known-bad and known-unformatted fixtures under crates/polylint-core/tests/, and should preserve the uniform Engine boundary. Before committing, run:

poly hooks install   # wires lint/format/cargo checks into git; they run on every commit
cargo test --workspace

License

MIT - see LICENSE.

About

Universal zero-dependency linter & formatter — one pure-Rust CLI (poly/polylint/polyfmt) for 300+ languages, in-process, one config.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors