Configuration reference
poly reads a single poly.toml. This page is the key-by-key reference; the
Configuration guide explains how the pieces fit
together.
Editor setup (JSON Schema)
Section titled “Editor setup (JSON Schema)”poly publishes a JSON Schema for poly.toml at:
https://goldziher.github.io/poly/poly.schema.jsonPoint your editor at it for validation and autocomplete. The simplest way is the
schema directive as the first line of poly.toml:
#:schema https://goldziher.github.io/poly/poly.schema.json
[defaults]line_length = 120The directive is an ordinary TOML comment that taplo
and the editors built on it understand, and poly fmt leaves it in place. You
can also generate the schema locally — it is derived from the same types the
parser uses:
poly config schema --output poly.schema.jsonThe per-engine [engines.<id>] tables are autocomplete hints: each backend
lists the keys it declares, but the tables stay open because a language id may be
any tree-sitter grammar and some backends derive their key set from an upstream
type. poly lint remains the authority — it reports unknown-config-key for any
key no engine reads. The v2 schema is committed as schemas/poly-v2.schema.json;
poly config schema prints the one the running binary uses.
Top-level sections
Section titled “Top-level sections”| Section | Purpose |
|---|---|
required_version, root |
Name the poly versions CI installs; stop the config cascade. |
[components] |
Which downloadable engine components a run may use. |
[defaults] |
Line width, line endings, whitespace — the base every formatter layers on. |
[discovery] |
The file walk: excludes, the prune set, generated files. |
[engines.<id>] |
Per-engine rule and formatter configuration, including ast-grep’s rule directories. |
[workspace] |
When the whole-project phase of poly lint runs. |
[per_file_ignores] |
Suppress rules by path glob. |
[tools] |
Opt into catalog tools. |
[cache] |
Result-cache and sccache configuration. |
[commit] |
poly commit message linting and cleanup. |
[hooks] |
Git-hook stages, builtins and inline jobs. |
extends |
Share configuration from local or pinned-remote bases. |
required_version and root
Section titled “required_version and root”| Key | Type | Default | Meaning |
|---|---|---|---|
required_version |
string | — | The poly versions this repository expects (">=0.34, <0.35"). Components version in lock-step with poly, so it selects them too. Read by the GitHub Action’s version: from-config, which installs the newest release in the range, and written by poly migrate --to-v2. poly refuses to load a config the running version does not satisfy. |
root |
boolean | false |
Stop the upward config cascade here (a .git directory is an implicit boundary). Replaces v1’s [workspace] root. |
[components]
Section titled “[components]”Selection and policy for downloadable engine components. See Components and offline use.
| Key | Type | Default | Meaning |
|---|---|---|---|
enable |
"auto" or array |
"auto" |
"auto" derives components from the discovered files and configured engines; an array names components or presets. |
disable |
array | [] |
Components never to use. An unknown name, or an engine table of a disabled component, is a warning (set enabled = false in the table to silence it); an unknown enable name, or a name in both lists, is a config error. |
install |
"auto" | "never" |
"auto" |
Whether a run may download a missing component. |
on_missing |
"fail" | "skip" |
"fail" |
Exit 2, or skip the files and charge them to the skip budget, when a needed component is unavailable. --on-missing overrides it. |
[defaults]
Section titled “[defaults]”The opinionated base layer. Each key is applied wherever a tool exposes the setting.
| Key | Type | Default | Meaning |
|---|---|---|---|
line_length |
integer | 120 |
Target maximum line length. |
line_ending |
string | "lf" |
"lf" or "crlf". |
final_newline |
boolean | true |
Enforce a single trailing newline. |
trim_trailing_whitespace |
boolean | true |
Strip trailing whitespace. |
[discovery]
Section titled “[discovery]”| Key | Type | Default | Meaning |
|---|---|---|---|
exclude |
string or array | [] |
Gitignore-style globs pruned from the walk. A leading / anchors to the config directory; otherwise it matches at any depth. Accumulates across extends (use exclude_mode = "replace" to opt out). |
force_exclude |
boolean | true |
Apply exclude to explicitly named paths too. |
no_prune |
string or array | [] |
Directory names to keep despite the built-in prune set (build, dist, …). |
generated |
boolean | true |
Whether poly lints and formats machine-generated files. |
Engines
Section titled “Engines”[engines.<id>]: one table per engine, keyed by the engine id (ruff, oxc, taplo, rumdl, typos,
astgrep, quality, uncomment, …). See Languages and engines
page for which engine serves each language. Options that apply everywhere sit directly in the
table; narrow with sub-tables:
[engines.ruff] # every phase, every languageselect = ["E", "F"]
[engines.ruff.format] # the format phase only (also: .lint)line_length = 100
[engines.ruff.lang.python] # one language (also: .lang.python.lint / .format)target_version = "py312"
[engines.ruff.rules.ANN201] # a per-rule overridelevel = "error"Precedence, lowest to highest: tool defaults, the tool’s native config files, [engines.<id>], its
lint or format sub-table, its lang.<language> sub-table, then lang.<language>.lint or
.format. The names enabled, lint, format, lang and rules are reserved.
Every engine table accepts enabled (boolean; an absent key means the engine’s
own default — typos is off unless [engines.typos] enabled = true). Linter backends that
participate in rule selection accept the uniform triple:
| Key | Type | Meaning |
|---|---|---|
select |
array | Replace the default rule set with these codes / categories. |
extend_select |
array | Add to the default rule set. |
ignore |
array | Remove from the selected set. |
rules.<id> |
table | Per-rule override: [engines.<id>.rules.<code>] level = "error" | "warning" | "info" | "hint", plus rule parameters. |
Each engine also reads its own keys ([engines.ruff] line_length, [engines.mago] php_version, …).
poly lint reports a key that no engine reads as unknown-config-key with a source span, and
poly config validate checks the keys offline from the component index.
ast-grep rules
Section titled “ast-grep rules”[engines.astgrep] also carries the ast-grep rule settings that v1 kept in a top-level [rules] table:
| Key | Type | Default | Meaning |
|---|---|---|---|
rule_dirs |
array | [".poly/rules"] |
Directories of custom ast-grep *.yml rule files, relative to the config. Set to [] to disable. |
builtin_pack |
boolean | true |
Load poly’s built-in ast-grep rule pack. |
Python type checking
Section titled “Python type checking”[engines.python-types] is opt-in Python type checking, a project-check engine. backend picks the checker: "ty" (in the
python component) or "pyrefly" (its own opt-in pyrefly component, fetched when a config selects it). The two share one
table, so switching is one value.
[engines.python-types]backend = "ty" # or "pyrefly"python_version = "3.12" # default: the interpreter's, else the checker's latestsrc = ["src"] # first-party roots; default: the project root and src/extra_paths = ["stubs"] # searched before first-party codeNeither backend starts a process: poly resolves the virtualenv and its site-packages itself and hands the paths over
(pyrefly’s interpreter query is forced off). ty’s ty.toml and [tool.ty], and pyrefly’s pyrefly.toml and
[tool.pyrefly], are not read. pyrefly’s findings carry its own rule codes (bad-assignment, missing-import, …); the
component is built without pyrefly’s bundled third-party stubs, so only typeshed’s stdlib stubs come with it.
A run never ends up checking nothing when type checking was asked for:
- A
backendthat is not a string (backend = 1,true, an array) is reported as an unknown value by ty, the default backend, and the run exits 2. An unsetbackendkeeps type checking off. backend = "pyrefly"while thepyreflycomponent is disabled ([components] disable) or missing from the build (the bundled binary links no pyrefly) is an error that names the component and says how to enable it, exit 2. Under--offline,install = "never"oron_missing = "skip"the unavailable component is instead a counted skip, as for every component (ADR 0039), and--deny-skipsapplies.--onlyand--skiptake the engine name (pyrefly) or the shared table name (python-types) for either backend:--skip python-typesstops whichever is configured, and--only python-typeskeeps it. Like any selection they only narrow:--only pyreflywithbackend = "ty"runs nothing. That is reported as a skip that names the backend, or says it is unset (`--only pyrefly` selects nothing here: `[python-types] backend` is "ty"), and counted by--deny-skips/--max-skips, unlike other--only/--skipexclusions: you asked for a check and got none, so the run must not read as a pass.- pyrefly uses the run’s thread budget (
-j/--jobs, else every core), as passed over the component protocol.
JavaScript and TypeScript workspaces
Section titled “JavaScript and TypeScript workspaces”[engines.skif] is opt-in dependency, manifest, version-policy and dead-code linting for npm, pnpm, yarn and bun
workspaces, a project-check engine in the web component (preset aliases skif and js-deps). A project’s own
skif.toml is honored; the keys below override it.
[engines.skif]enabled = truedir = "." # the workspace, relative to the project rootignore = ["empty-dependencies"] # skif rule ids to turn offselect = ["catalog-mismatch"] # run only these rulesignore_packages = ["@scope/legacy-*"]ignore_dependencies = ["left-pad"]production = false # judge production dependencies onlyjs = true # false turns the source (import) analysis offthreads = 4 # skif's own pool; default: poly's job count, 1 spawns nonepnpm_lock = "pnpm-lock.yaml" # also npm_lock, yarn_lock, bun_lockskif reads only the files poly hands it: sources (including Vue, Svelte and Astro components), manifests, tsconfig,
pnpm-workspace.yaml, skif.toml and every .gitignore and .ignore under dir, plus the lock files, never
node_modules. It always sees the whole tree: .gitignore and the pruned directories apply, but [discovery] exclude, --exclude and nested poly.toml scopes do not cut the import graph; they only decide which files get
findings. The result is cached on all of them, so editing any member package.json re-runs the check, and it does
not depend on what is installed on the machine. Every run reads and hashes the whole tree to compute that key, so on
a very large monorepo the cost of a warm run is the read, not skif.
Three rules judge a dependency by what an installed package provides and are off unless you ask for them:
unlisted-binaries, unused-dev-dependencies and unused-dependencies. Without node_modules, a dependency used
only through a binary named otherwise (@acme/toolkit running kit) or only as another dependency’s peer is
reported as unused (a lock file supplies peers, not pnpm binary names). Turn one on with select, with
[engines.skif.rules.<id>] level = "error", or in your skif.toml [rules].
poly lint --fix applies skif’s safe fixes to the manifest a finding is in; fixes that are unsafe, need a choice or
edit another file stay suggestions. After the fixes the check runs once more and that run is the report. Set
[engines.skif.rules.<id>] level to move a finding’s severity.
[workspace]
Section titled “[workspace]”| Key | Type | Default | Meaning |
|---|---|---|---|
run |
"auto" | "always" | "never" |
"auto" |
When poly lint runs the whole-project phase: for repository-wide runs only (auto), even for named paths (always), or never. --workspace overrides it. |
[per_file_ignores]
Section titled “[per_file_ignores]”A table of path glob → rule codes to suppress for files matching the glob. Globs are relative to the config that declares them.
[per_file_ignores]"tests/**" = ["F401"][tools]
Section titled “[tools]”Opt into tools from the vendored catalog, keyed by catalog name
([tools.<name>]). Every entry is inert until enabled = true.
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
boolean | false |
Whether the tool runs. |
command |
string | — | Which catalog command to invoke. |
args |
array | — | Replace the catalog command’s argv entirely. |
stages |
array | [] |
Git stages the tool runs in. |
files |
string or array | — | Include glob(s). |
exclude |
string or array | — | Exclude glob(s). |
env |
table | {} |
Environment variables injected when running the tool. |
root |
string | — | Working directory, relative to the config root. |
extra_args |
array | [] |
Flags added to every role’s argv, just before the file list. Keeps the catalog’s own flags, so this is where configuration goes (["-c", "checkstyle.xml"]). |
format_extra_args |
array | [] |
extra_args for the formatting role only. |
lint_extra_args |
array | [] |
extra_args for the linting role only. |
bin |
string | — | Executable to run instead of the catalog binary on PATH. For a JVM tool, the java. |
version |
string | — | Pinned release to use. Looked up in the catalog’s checksummed releases. |
url, sha256 |
string | — | Download and digest for a version the catalog does not know. |
install |
string | "never" |
"auto" downloads the pinned version into poly’s cache when the tool is missing. A version that cannot be provisioned makes the tool unavailable; poly never runs a different version from PATH. |
values_files |
array | [] |
helm: values files (-f) passed to every chart, relative to the config root. |
chart_values |
table | {} |
helm: chart-directory glob to the values files passed for the charts it matches. |
skip_charts |
array | [] |
helm: chart-directory globs the tool does not run on (library charts). |
prepare |
bool | true |
helm: run helm dependency build once per chart before linting, unless charts/ already satisfies Chart.lock or POLY_OFFLINE is set. |
prepare_attempts |
integer | 5 |
helm: attempts at the prepare step before checking with the dependencies on disk. 1 disables the retry. |
prepare_backoff_secs |
integer | 4 |
helm: wait after the first failed attempt; doubles after each further failure. |
prepare_jitter_secs |
integer | 4 |
helm: up to this many random extra seconds added to every wait. |
verify_skip_charts |
bool | true |
helm: still run each skip_charts chart and fail when the tool now passes on it; false skips them without running. |
new_from_rev |
string | — | golangci-lint: report only findings introduced since this git revision (--new-from-rev). |
goos |
array | [] |
golangci-lint: run once per operating system with GOOS set and merge the findings. |
Tools with a recipe (checkstyle, palantir-java-format, buildifier, buf, actionlint, gau,
hadolint, golangci-lint, helm, helm-template, tflint, tofu, terraform,
pyproject-fmt, npm-groovy-lint) run batched, parse the tool’s report into file/line/rule
diagnostics, and are described in Tool catalog.
[cache]
Section titled “[cache]”| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
boolean | true |
Master switch for both cache tiers. |
dir |
string | — | Override the cache root directory. |
[cache.results] hooks |
string | "safe" |
"off", "safe" or "aggressive". |
[cache.sccache] enabled |
boolean | false |
Enable tier-2 sccache for compiler hooks. |
[cache.sccache] bin |
string | — | sccache binary (bare name or absolute path). |
[cache.sccache] dir |
string | — | sccache storage directory. |
[cache.sccache] max_size |
string | — | Maximum cache size (e.g. "10G"). |
[commit]
Section titled “[commit]”| Key | Type | Meaning |
|---|---|---|
preset |
string | Named preset to seed the rules from (e.g. "conventional"). |
write |
boolean | Whether the cleaner rewrites the message in place. |
[commit.rules] message |
table | { pattern, description, enforce_length, enforce_case } — a required subject pattern. A pattern replaces the Conventional Commits structural check; enforce_length and enforce_case keep its length limits and its subject case / trailing full stop checks. |
[commit.rules] excludes |
array of tables | { pattern, message } — subjects that skip linting. |
[commit.rules] cleanup |
array of tables | { find, replace, description } — find/replace cleanups. |
[commit.rules] single_line |
boolean | Reject multi-line subjects. |
[commit.rules] require_body |
boolean | Require a non-empty body. |
[commit.rules] exit_nonzero_on_rewrite |
boolean | Exit non-zero when the cleaner rewrote the message. |
[commit.rules] no_emojis |
boolean | Reject emojis. |
[commit.rules] ascii_only |
boolean | Require ASCII-only content. |
[commit.rules] title_prefix |
string | Required subject prefix. |
[commit.rules] title_prefix_separator |
string | Separator after the prefix. |
[commit.rules] title_suffix |
string | Required subject suffix. |
[commit.rules] title_suffix_separator |
string | Separator before the suffix. |
[commit.rules] ai_cleanup |
boolean | Set to false to keep AI attribution trailers and banners instead of removing them (default true). |
[hooks]
Section titled “[hooks]”The [hooks] table holds reserved keys plus one key per git stage
([hooks.pre-commit], [hooks.commit-msg], …).
Reserved keys:
| Key | Type | Meaning |
|---|---|---|
stages |
array | Default stages for builtin hooks that do not set their own. commit-msg and prepare-commit-msg apply only to commit; a file-checking builtin runs there only when it lists the stage itself. |
env |
table | Environment merged into every job. |
sources |
array of tables | [[hooks.sources]] producer catalogs: { id, path | git, revision, hooks }. |
snapshot_include |
string or array | Untracked paths pulled into the staged snapshot (ADR 0019). |
isolate |
boolean | Force hooks onto the staged snapshot (unset uses the per-invocation default). |
snapshot |
"auto" | "full" |
auto (default) materializes only the staged files and their directory context unless a workspace or cargo job will run; full always materializes the whole index. |
partial_stage |
"fail" | "restage" |
What a stage_fixed job does when its fix lands on a file that also has unstaged edits. fail (default) withholds the fix and names the commands to resolve it; restage stages only the fixed content and leaves the edits in the worktree. |
builtin |
table | See below. |
[hooks.builtin] — poly’s in-process hooks. Each of lint, fmt, commit is a
bare boolean or a table; file_safety and cargo are a boolean or a tuned
group table.
| Key | Type | Meaning |
|---|---|---|
lint / fmt / commit |
boolean or table | { enabled, stages, files, exclude }; lint and fmt also take fix, priority and jobs (see the hooks guide). |
file_safety |
boolean or table | { enabled, stages, files, exclude, max_added_file_kb, merge_conflict, added_large_files, private_key, case_conflict, executables_have_shebangs, shebang_scripts_are_executable, aws_credentials, requirements_txt_fixer, priority, jobs }. |
cargo |
boolean or table | { enabled, stages, clippy, sort, machete, deny, clippy_args, cache, lint }. |
A stage table ([hooks.pre-commit]) accepts:
| Key | Type | Meaning |
|---|---|---|
parallel / piped / follow |
boolean | Concurrency and output-streaming behaviour. |
exclude_tags |
array | Tags whose jobs are excluded. |
files / exclude |
string or array | Stage-level include/exclude globs. |
skip / only |
boolean or array | Guard: a bare bool or [{ run = "<command>" }]. |
fail_on_changes |
boolean | Fail if jobs dirty the working tree. |
precondition |
string | Guard command; non-zero skips the stage with a warning. |
before / after |
string or array | Commands run before/after the jobs. |
jobs |
array of tables | Ordered jobs ([[hooks.pre-commit.jobs]]). |
commands / scripts |
tables | Named jobs ([hooks.pre-commit.commands.<name>]). |
A job accepts name, run xor script, runner, args, glob,
files, exclude, file_types, root, skip, only, precondition,
before, tags, env, fail_text, timeout, priority, serial,
stage_fixed, show_output, workspace, interactive, use_stdin and cache.
root runs the job from a subdirectory and passes file arguments relative to it (as lefthook
and pre-commit do): a job with root = "products/em/backend" receives src/x.py, not
products/em/backend/src/x.py, in "$@" and in {staged_files} / {all_files}. Files outside
root are not passed, and a job none of whose files are under root is skipped.
show_output = true prints the job’s output even when it passes; by default only a failing job’s
output is shown, so a warning from a job that exits 0 is otherwise dropped.
poly hooks run --verbose (-v) does the same for every job.
extends
Section titled “extends”A top-level list of base configs to inherit from (ADR 0020). Each entry is a
local path string or a { path | git, revision, file } table; a single entry may
be given without the array.
extends = [ { git = "https://github.com/acme/poly-baseline", revision = "<40-hex-oid>", file = "poly.toml" }, "./poly.overrides.toml", # later entry = higher precedence]exclude_mode = "replace" in a table next to an exclude list makes that list
replace the inherited one rather than accumulate. A symbolic (branch/tag) git
ref must be locked with poly lock first.