Components, offline and mirrors
polylint’s engines ship as signed, downloadable components: bundles of engines that a core binary fetches the first time a run needs them. The engine remains the unit you configure; a component only decides where an engine’s code lives. Host tools and recipe tools are not components; see Toolchains, runtimes and recipes.
Build flavors
Section titled “Build flavors”| Flavor | How it is built | What it does |
|---|---|---|
| bundled | The poly-bundled crate in the components/ workspace |
Every engine family and the MCP server are linked in, except the two below. Nothing is downloaded. |
| core | poly-cli --no-default-features --features core |
Config, discovery, cache, runner, hooks and the tree-sitter tier, plus the quality engine and the native-toolchain and catalog tiers. Every other engine is a component. |
x86_64-unknown-linux-musl is the only release archive that is a bundled build: it embeds no
component lock and downloads nothing. The other four (Linux gnu x86_64 and aarch64, macOS aarch64,
Windows x86_64) are core builds with the release’s signed component lock embedded. Intel macOS
has no release binary after 0.34.0.
poly rules and the MCP rules tool work in a core build by installing the astgrep component on
demand; --offline and [components] install = "never" stop that download, and the command then fails.
poly doctor has a components section with the flavor, the cache root and what the current
directory needs.
On a bundled build poly components install only prints a note: there is nothing to fetch.
Two components are not part of a bundled build, and poly components list says so:
sh: a Go executable cannot be linked into the binary, so a bundled build formats shell with the hostshfmtwhen it is onPATH, and with the tree-sitter tier otherwise (ADR 0045). Installshfmtfor shfmt’s output, or use a core build, which downloads theshsidecar.pyrefly: it is built in a workspace of its own, so no bundled build links it, and a bundled build embeds no lock to download it.[engines.python-types] backend = "pyrefly"therefore fails the run (exit2) with that reason. Keep the defaultbackend = "ty", or use a core build.
The components
Section titled “The components”| Component | Engines |
|---|---|
web |
JavaScript, TypeScript, JSON (oxc), YAML, TOML, Markdown, CSS (malva), HTML and the other markup languages, adrlint, skif |
python |
ruff, and ty for type checking |
pyrefly |
pyrefly type checking (opt-in, fetched when [engines.python-types] backend = "pyrefly") |
sql |
sqruff |
php |
mago |
biome |
biome CSS and GraphQL lint |
ruby |
rubyfmt |
nix |
alejandra |
astgrep |
ast-grep and the built-in rule pack |
misc |
dockerfile, dotenv, hcl, graphql, ini |
sh |
shfmt (mvdan.cc/sh), a Go executable poly runs as a sidecar |
uncomment |
comment removal (opt-in) |
typos |
typos (opt-in, see below) |
mcp |
the MCP server, a frontend component |
poly components list prints the authoritative list from the index compiled into the binary.
Presets name groups: all (every engine component except the opt-in typos, uncomment and pyrefly, and mcp), and language names
such as python, ts, markdown or css.
Selecting components
Section titled “Selecting components”A run needs the components for the languages it discovers and for the [engines.*] tables you
configured. Tune that with [components]:
[components]enable = "auto" # or ["python", "web"]disable = ["ruby"]install = "auto" # "auto" | "never"on_missing = "fail" # "fail" | "skip"| Key | Effect |
|---|---|
enable |
"auto" derives components from the discovered files and configured engines. A list names components or presets explicitly. |
disable |
Components never to use, even when auto would select them. An engine table for a disabled component ([engines.<id>], [lint.<lang>.<id>], [fmt.<lang>.<id>]) is ignored and draws a warning; set enabled = false in it (or in its [lint.<lang>] / [fmt.<lang>] table) to silence the warning. |
install |
"auto" downloads a missing component when a run needs it. "never" forbids downloads. |
on_missing |
"fail" exits 2 when a needed component is unavailable. "skip" skips the files it would have handled and charges them to the skip budget (--deny-skips, --max-skips). |
Two problems are config errors for every command that loads the config (lint, fmt, components sync, …), not only for poly config validate: exit 2, nothing is checked. They are a name in
enable that is neither a component nor a preset, and a name in both enable and disable.
Two others are warnings on stderr, printed once per run, because a config inherited through extends,
a parent monorepo config or poly.local.toml may legitimately configure an engine that a child config
disables: an unknown name in disable, and an engine table under a disabled component. The table is
ignored. To silence the warning, set enabled = false in it:
[components]disable = ["python"]
[engines.ruff]enabled = false # inherited table, switched off herepoly config validate still reports both warning cases as errors, since it checks one file on its own.
poly config show and poly components status never refuse a config on these checks, so a bad
[components] section can always be inspected, and components list and uninstall do not read it.
on_missing = "skip" is an explicit opt-in, so a run whose components are all missing still exits
0 (unless --deny-skips or --max-skips say otherwise). It is never silent: a warning: on stderr
names each component and says its files were skipped, not checked, and summary.missing_components
in the JSON and TOON documents counts them. A run that must not pass without checking keeps the
default, on_missing = "fail".
--on-missing fail|skip overrides on_missing for one run. poly never falls back to the
tree-sitter tier for a language whose component is missing: output would differ between machines.
typos is opt-in
Section titled “typos is opt-in”typos is its own component and is off unless you turn it on:
[engines.typos]enabled = trueWhen downloads happen
Section titled “When downloads happen”Only these commands may use the network for components:
- a
poly lintorpoly fmtrun that discovers files for a missing component (withinstall = "auto"); poly components installandpoly components sync;poly components import, from a file you supply (no network).
poly components list, status, config validate, config show and doctor never download.
Components are fetched before a run is planned, never while files are being checked. Every download
is verified against the lock’s checksum before it is loaded.
poly components status . # what this directory needs, and whether it is usablepoly components status . --verify # also mark each installed archive as verifiedpoly components sync . # install everything this directory needspoly components install python web # or: install allpoly components uninstall rubyOffline and air-gapped use
Section titled “Offline and air-gapped use”--offline (or POLY_OFFLINE=1) stops every network access: components, tree-sitter grammars,
catalog-tool installs, remote extends and hook sources. Anything that would need the network is a
skip or an error according to on_missing, never an attempt.
poly --offline components sync . # a check only: exits 2 if something is missingpoly --offline lint .To seed a host with no network access, download the component archives from the release on a connected machine and import them:
poly components import <archive>The archive is verified against the lock the binary carries before it is installed.
Mirrors and the cache location
Section titled “Mirrors and the cache location”| Variable | Effect |
|---|---|
POLY_COMPONENT_MIRROR |
Base URL that replaces the origin (scheme://host[:port]) of each component download. The original path is kept, so mirror the release’s directory layout. Integrity never depends on the URL, because the lock pins each archive’s SHA-256. |
POLY_COMPONENT_CACHE |
Component cache root. Default: components/ inside poly’s cache home (POLY_CACHE_HOME, else the per-user cache directory). |
POLY_OFFLINE |
Same as --offline. |
POLY_COMPONENT_MIRROR=https://artifacts.example.com/poly poly components sync .Damaged installs
Section titled “Damaged installs”A cached component is re-verified against the lock (digest, size, manifest and signature) before a run uses it. A copy that fails is not trusted:
listandstatusshow it ascorrupt, andstatusexits1.installandsyncdownload it again and reportrepaired; with--offlinethey fail and say how to repair it.- A run does the same: online it downloads the component again and logs a warning, offline or with
install = "never"it fails withverify_failedand the remedy.
poly cannot yet say which check failed, because alef’s cache drops that error. Only a failed check of the contents counts as damage. If poly cannot read the copy at all (permissions, a failing disk, too many open files) the run reports that error and leaves the copy alone rather than downloading it again. A check that fails is repeated once after a moment, so a repair running in another process is not mistaken for damage.
Tamper detection relies on alef’s verified stamp. After a full check, poly records the library’s size,
modification time and, on Unix, inode and change time, and trusts the copy while those still match.
A modification that preserves all of them is not noticed until the next full verification. To force
one, remove the copy and install it again (poly components uninstall <name>, then
poly components install <name>), which re-downloads it and checks every digest. status --verify
does not do this: it reports the same stamped check a run uses.
Cache maintenance
Section titled “Cache maintenance”poly components prune removes cached archives the lock no longer references:
poly components prune --dry-run # list exactly what a real prune removespoly components prune --keep-days 7 # keep unreferenced archives used in the last 7 dayspoly components prune --all # remove every archiveBy default it keeps archives referenced by the lock for any target, and archives used within the last
30 days. It also clears the staging directories and temporary files an interrupted install left
behind, once they are an hour old: a younger one may belong to an install running in another
process, so prune leaves it alone. It reports a file that a loaded library still holds (Windows) as
skipped for a later run.
The GitHub Action installs components before the first run and caches them by version and lock, so a second run downloads nothing:
- uses: Goldziher/polylint@v0 with: version: from-config # the version required_version in poly.toml names components: auto # or all, none, or "sql,php"- run: poly lint .Set required_version in poly.toml (for example ">=0.34, <0.35") to fix the poly version the
Action installs, and with it every component: components are versioned in lock-step with poly, so
there is no separate component lock to commit. poly itself refuses to load a config whose
required_version the running binary does not satisfy.
See ACTION_USAGE.md for the inputs.