One binary. ~30 languages. No toolchain to install.
poly lints and formats whole repositories in seconds: curated Rust backends for the languages that matter, a tree-sitter fallback for everything else, and eleven coding-agent plugin profiles plus an MCP server so agents can drive it directly instead of shelling out.
Lint + format · one poly.toml · pure Rust, zero deps · blake3 cache + rayon parallelism · git
hooks & commit checks · MCP + coding-agent plugins
Install · What You Get · What Runs Out of the Box · AI Agents & MCP · Performance · Docs · Contributing
What You Get
Installation
Quick install
curl -fsSL https://raw.githubusercontent.com/Goldziher/poly/main/install.sh | sh
Windows PowerShell:
irm https://raw.githubusercontent.com/Goldziher/poly/main/install.ps1 | iex
Both detect the platform, download the matching release archive, and verify it against
sha256sums.txt. Set POLY_VERSION=0.28.2 to pin an exact release.
GitHub Actions
- uses: Goldziher/poly@v0
with:
version: v0.28.2 # omit for the latest release
Forwards to install.sh and caches the installed binary by version and platform. See
ACTION_USAGE.md for the full input/output reference.
Package managers
brew install Goldziher/tap/poly
scoop bucket add goldziher https://github.com/Goldziher/scoop-bucket
scoop install poly
Homebrew's tap is a single rolling formula with no versioned alias yet — treat it as a
get-latest channel and use the installer script or the GitHub Action where a pinned version
matters. cargo binstall --git https://github.com/Goldziher/poly poly-cli also works, resolving
the same GitHub release archives.
npm and PyPI
npm i -g @goldziher/polylint
pip install polylint
Both install the same prebuilt poly binary. The package is named polylint (the unscoped
poly name belongs to unrelated projects on both registries), but the executable is poly
on every channel — polylint also works everywhere as an alias for the same binary. Full
per-platform detail in docs/INSTALL-PACKAGES.md.
As an agent plugin
poly ships eleven runtime profiles under plugin/, with five public skills and
two prompts. MCP-capable profiles register poly mcp; Pi uses the CLI. The Claude profile includes 5 skills
and 2 slash commands (/poly-check, /poly-fix):
/plugin marketplace add Goldziher/poly
/plugin install poly@poly
Codex: add the Goldziher/poly marketplace through your client's plugin manager (manifest at
plugin/plugin.json). The plugin assumes poly is already on PATH — it does not
bundle the binary — and its version tracks the poly binary version lock-step. See
AI Agents & MCP.
As an MCP server
Any MCP-capable client can run poly directly:
{
"mcpServers": {
"poly": { "command": "poly", "args": ["mcp"] }
}
}
Quickstart
$ poly lint
./app.py
warning ruff T201 5:5 `print` found
warning ruff ANN201 4:5 Missing return type annotation for public function `main`
error ruff F401 1:8 `os` imported but unused
3 issues found.
2 files linted
1 issue fixable with the `--fix` option
$ poly fmt --check
would reformat ./src/main.rs
1 file will change.
2 files checked
$ poly fmt --fix
reformatted ./src/main.rs
1 file reformatted.
2 files checked
$ poly hooks install
✓ Installed 2 git hooks in .git/hooks
› commit-msg
› pre-commit
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.
Exit codes are a contract: 0 is clean, 2 means the run verified less than it claims, and 1
means findings — which for poly fmt --fix reports that files were rewritten, so a script
running it in fix mode should treat 1 as success. Add -q to trim the per-file detail on a
large repository; the summary keeps every count and reason.
Demos
Real recordings, not mockups — vhs runs each command live, so the timings on screen are the
timings you get. Tapes are in docs/media/tapes/.
Kubernetes: 31,303 files, one binary, no Go toolchain.

Django is not a Python repo — it is Python, JavaScript, CSS, HTML, TOML, YAML and Markdown. One tool, one config, one pass.

Every report has a machine-readable form. --format toon is compact enough to hand to an
agent without burning its context; --format json is there when you want a document to parse.

And an agent can skip the terminal entirely. poly mcp speaks MCP over stdio and advertises
the same eleven tools the CLI exposes.

How It Works
poly discovers files once (respecting .gitignore), plans the engine list once per language, and runs the per-file
work in parallel on a rayon pool. Every backend — a linked-in Rust crate, the tree-sitter tier, or a wrapped CLI —
returns the same Diagnostic and FormatOutput shapes, so reporting, caching, 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
The default path needs no Python, Node, Go, or JVM — the backends are Rust crates compiled into the binary, and a
language without a dedicated backend falls through to a tree-sitter generic tier that is still pure Rust. The result
cache is keyed by file bytes, engine name, engine version, and resolved engine config, so a tool upgrade or a config
change invalidates exactly the entries it affects. --debug prints per-file engine timing and cache hit/miss data.
What Runs Out of the Box
poly is useful with an empty poly.toml. This is what a run with no configuration actually does; anything opt-in is
marked as such.
Three tiers that run on every language
typos— spell-checks identifiers, comments and strings. Always on; it has no enable key.- The code-quality engine — tree-sitter metric rules, listed below.
- The built-in ast-grep pack — 26 rules across C#, Elixir, Go, Java, Kotlin, Python, Ruby, Rust and Swift. 13
are on by default; the other 13 ship
severity: offand are one config line away. Your own rules under[rules] dirssit above the pack — a rule with the sameidreplaces the built-in one outright.
A fourth cross-cutting engine, uncomment (comment removal), is off by default.
The quality engine never duplicates a backend rule: where ruff or oxlint already covers a metric, it defers. It counts as lint coverage only for languages whose control flow it can model — Python, Rust, Go, JavaScript, JSX, TypeScript, TSX, Java, Kotlin, C, C++, C# and Ruby — while other languages still get the line-count rules.
Everything these three tiers report is warning severity, and warnings alone do not fail a run, so turning poly on does not redden an unconfigured CI.
Native toolchain CLIs
gofmt, rustfmt and shellcheck run automatically when found on PATH; shellcheck is the only one of the three
that lints. zig fmt, shfmt, google-java-format, ktfmt, styler (R), swift-format, dart format and
gleam format are opt-in. When a tool is absent the language falls through to the tree-sitter tier, so the
zero-dependency promise holds either way.
Backend Coverage
The Lint column is the language's own backend; the three cross-cutting tiers add findings on top, including where
that column says no. Beyond this table, an opt-in catalog of 348 tools across 175 languages covers the long tail.
Full per-language reference: Backends.
AI Agents & MCP
poly ships its agent integration in the box rather than expecting one to be bolted on.
/plugin marketplace add Goldziher/poly
/plugin install poly@poly
That installs 5 skills and 2 slash commands (/poly-check, /poly-fix) that teach an agent poly's tiered backend
model and when to reach for lint vs. format vs. hooks. Codex clients add the same marketplace through their own
plugin manager.
Pi
Install the poly binary first, then install the Pi package from Git:
pi install git:github.com/Goldziher/poly
The package contains 5 skills and the /poly-check and /poly-fix prompt templates.
Pi runs poly through its shell tool; this package does not register an MCP server or install the binary.
Use pi install -l git:github.com/Goldziher/poly to save the package in the current project's configuration.
The same bundle targets @goldziher/opencode-poly for OpenCode and Pi. After its first npm release,
pi install npm:@goldziher/opencode-poly provides the registry installation path and Pi catalog discovery.
poly mcp is a stdio MCP server exposing eleven tools that mirror the CLI 1:1 — lint, format_check, rules,
config_show, cache_stats and version (read-only), lint_fix, format_write and cache_clean (mutating), and
workspace_lint / workspace_lint_fix. The last two run the multi-minute whole-project phase (cargo clippy,
cargo-sort, cargo-machete, cargo-deny) and are exposed as async Tasks: the call returns a handle the client
polls with tasks/get, falling back to a synchronous result for clients that do not declare the capability.
Every result carries a poly identity block (version, build id, channel, executable, pid, engines — a blake3
digest of every compiled-in backend's version) and separates three per-file outcomes — checked, skipped
(poly declined the file) and errored (poly failed on a file it accepted). isError is set whenever anything
errored, so an agent can gate on it before trusting the payload.
Full parameter reference: poly-mcp.
See all runtime installation instructions for Claude, Cursor, Codex, Gemini,
Kimi, OpenCode, Factory, Hermes, Pi, Agent Plugins and Copilot. Public skills can be installed
individually with npx skills add Goldziher/poly --skill poly-mcp.
Performance
Release build, Apple Silicon, cold cache (--no-cache), best of three runs on an idle machine.
poly lint --no-workspace and poly fmt --check over the whole repository, counting the files
poly actually inspected. These are poly's own numbers — not a comparison against ruff,
oxlint, biome or anything else; no such benchmark was run.
Kubernetes is the one repository where formatting costs more than linting: its Go files go
through gofmt, the one backend that is a subprocess rather than a linked-in crate, and ~20,000
process spawns dominate the run.
The blake3 cache and rayon parallelism (see How It Works) are what keep repeat runs fast — Django re-lints in 0.71s warm against 0.82s cold: a cache hit skips the engine entirely, and everything else is split across cores.
Documentation
A single poly.toml drives linting, formatting, git hooks, and commit-message policy; poly.local.toml layers local
overrides on top, nested files cascade in a monorepo, and poly config show prints the effective merged result.
Unknown keys, unknown sections and wrongly-typed values are reported as warnings, never silently ignored.
- Quickstart — install, first run, first config.
- Configuration — every key, rule selection, inline suppression, monorepo cascading, shared and remote config.
- Hooks —
poly hooks install, builtin hooks, staged isolation, timeouts, concurrency, caching. - CLI reference — every subcommand and flag, and the exit-code contract.
- Backends — full language table and the tool catalog.
Contributing
Keep changes small and test-backed. A new or changed backend needs known-bad and known-unformatted fixtures under
crates/poly-core/tests/, and must preserve the uniform Engine boundary. Before committing:
poly hooks install # wires lint/format/cargo checks into git; they run on every commit
cargo test --workspace --no-fail-fast
For anything touching the runner, an engine, or the rule pack, also run the hardening harness. poly's
own fixtures are small and chosen to exercise a known path; the harness runs poly over real
third-party trees and checks the things fixtures cannot — that no file errored, that formatting
converges in two passes, that the cache never serves a different answer than a cold run, and that no
language poly claims to lint reports no lint rules for it:
task harden # pinned third-party repositories
task harden:local # the sibling working trees next door, read-only
task harden:generated # machine-generated code (corpus C)
It also reports per-rule finding counts per repository, which is how a pack rule earns a default
severity. See docs/harden-corpus.md for what each corpus is allowed to
assert, and why a count needs a pinned input while an invariant does not.
License
MIT - see LICENSE.