Skip to content

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.

poly publishes a JSON Schema for poly.toml at:

https://goldziher.github.io/poly/poly.schema.json

Point your editor at it for validation and autocomplete. The simplest way is the schema directive as the first line of poly.toml:

poly.toml
#:schema https://goldziher.github.io/poly/poly.schema.json
[defaults]
line_length = 120

The 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:

Terminal window
poly config schema --output poly.schema.json

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

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

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.

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.
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.<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 language
select = ["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 override
level = "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.

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

[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 latest
src = ["src"] # first-party roots; default: the project root and src/
extra_paths = ["stubs"] # searched before first-party code

Neither 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 backend that 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 unset backend keeps type checking off.
  • backend = "pyrefly" while the pyrefly component 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" or on_missing = "skip" the unavailable component is instead a counted skip, as for every component (ADR 0039), and --deny-skips applies.
  • --only and --skip take the engine name (pyrefly) or the shared table name (python-types) for either backend: --skip python-types stops whichever is configured, and --only python-types keeps it. Like any selection they only narrow: --only pyrefly with backend = "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 / --skip exclusions: 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.

[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 = true
dir = "." # the workspace, relative to the project root
ignore = ["empty-dependencies"] # skif rule ids to turn off
select = ["catalog-mismatch"] # run only these rules
ignore_packages = ["@scope/legacy-*"]
ignore_dependencies = ["left-pad"]
production = false # judge production dependencies only
js = true # false turns the source (import) analysis off
threads = 4 # skif's own pool; default: poly's job count, 1 spawns none
pnpm_lock = "pnpm-lock.yaml" # also npm_lock, yarn_lock, bun_lock

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

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.

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"]

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.

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

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.

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.