Skip to content

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.

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 host shfmt when it is on PATH, and with the tree-sitter tier otherwise (ADR 0045). Install shfmt for shfmt’s output, or use a core build, which downloads the sh sidecar.
  • 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 (exit 2) with that reason. Keep the default backend = "ty", or use a core build.
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.

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 here

poly 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 its own component and is off unless you turn it on:

[engines.typos]
enabled = true

Only these commands may use the network for components:

  • a poly lint or poly fmt run that discovers files for a missing component (with install = "auto");
  • poly components install and poly 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.

Terminal window
poly components status . # what this directory needs, and whether it is usable
poly components status . --verify # also mark each installed archive as verified
poly components sync . # install everything this directory needs
poly components install python web # or: install all
poly components uninstall ruby

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

Terminal window
poly --offline components sync . # a check only: exits 2 if something is missing
poly --offline lint .

To seed a host with no network access, download the component archives from the release on a connected machine and import them:

Terminal window
poly components import <archive>

The archive is verified against the lock the binary carries before it is installed.

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.
Terminal window
POLY_COMPONENT_MIRROR=https://artifacts.example.com/poly poly components sync .

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:

  • list and status show it as corrupt, and status exits 1.
  • install and sync download it again and report repaired; with --offline they 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 with verify_failed and 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.

poly components prune removes cached archives the lock no longer references:

Terminal window
poly components prune --dry-run # list exactly what a real prune removes
poly components prune --keep-days 7 # keep unreferenced archives used in the last 7 days
poly components prune --all # remove every archive

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