configuration
This guide covers all configuration options for Lintro and the underlying tools it integrates. Learn how to customize behavior, set tool-specific options, and optimize
Configuration Guide
This guide covers all configuration options for Lintro and the underlying tools it integrates. Learn how to customize behavior, set tool-specific options, and optimize Lintro for your project.
TL;DR: Lintro uses your existing tool configs (
.prettierrc,pyproject.toml, etc.) automatically. It only provides fallback defaults when no native config exists. Useenforce.line_lengthto ensure consistent settings across all tools via CLI injection.
Configuration Model: 5-Tier System
Lintro uses a clear 5-tier configuration model that separates concerns:
| Tier | Purpose | When Applied |
|---|---|---|
| execution | What tools run and how | Always |
| enforce | Cross-cutting settings (line_length, target_python) | Always (via CLI flags) |
| defaults | Fallback config when no native config exists | Only when no native config |
| tools | Per-tool enable/disable and config source | Always |
| ai | AI-powered summaries and fixes | When enabled + API key set |
The five tiers above form the LintroConfig model’s core configuration story
(lintro/config/lintro_config.py). Additional optional sections — review (diff-review
checklist) and watch (lintro watch defaults; see Watch Mode) —
configure specific commands rather than tool resolution.
Key Principles
- Native configs are respected by default - Tools use their own
.prettierrc,pyproject.toml [tool.ruff], etc. enforcesettings override via CLI flags - Line length and target Python are injected as CLI arguments to ensure consistencydefaultsprovide fallbacks - Only used when a tool has no native config file- Simple and transparent - Users know exactly which config is used
Tiered Configuration Flow
The configuration system works in a specific order:
-
Execution Tier - Determines which tools run and in what order
enabled_tools: Empty list means all enabled tools run when a config file is present (.lintro-config.yamlor a non-empty[tool.lintro]table inpyproject.toml). A no-config first run scopes tools to languages detected from manifests and source files (a directory of*.py/*.jswith nopyproject.toml/package.jsonstill selects ruff/prettier). Use--tools allorlintro initfor the full registry. An empty[tool.lintro]table is not a config, so language scoping still applies. An explicit named--toolslist on the CLI bypasses this allowlist; default runs and--tools allremain filtered by it. This is a feat/minor default change: kitchen-sink CI that relied on the unscoped no-config toolset should pass--tools allor add a config file. Detection uses the chk/fmt scan paths (a file uses its parent directory; multiple paths are unioned). When no paths are given the default is.(the current working directory). Tools that are not in the language map (for examplecommitlint) are still selected when their native config file is present at a scan root. Nested YAML/Markdown/shell files count; a lone rootREADME.mddoes not enable Markdown tools.fail_fast: Whether to stop on first tool failureparallel: Whether to run tools in parallel (default:true)max_workers: Maximum parallel workers, 1-32 (default: CPU count)auto_install_deps: Auto-install Node.js dependencies if missing, for the tools that need a project dependency tree (tsc,vue-tsc,svelte-check,astro-check). Unset by default, in which case Lintro falls back to container auto-detection (enabled inside containers, disabled otherwise)
-
Enforce Tier - Cross-cutting settings injected as CLI flags
- These settings override native configs via CLI arguments
- Example:
line_length: 88becomes--line-length 88for ruff/black - Applied to all tools that support the setting
-
Defaults Tier - Fallback configuration when no native config exists
- Only used if tool’s native config file is not found
- Example: If
.prettierrcdoesn’t exist, usedefaults.prettier - Generated as temporary config files when needed
-
Tools Tier - Per-tool enable/disable, config source, and auto-install
enabled: Whether the tool is enabledconfig_source: Optional explicit path to native config fileauto_install: Per-tool auto-install override (true/false/omit to inherit global)
-
AI Tier - AI-powered summaries and fix suggestions (opt-in)
enabled: Whether AI features are active (default:false)provider: AI provider to use (anthropic,cursororopenai) — required onceai.lintorai.reviewis enabled; there is no default- Applied only when enabled and a valid API key is set
Configuration Resolution Example
For a tool like Prettier:
- Check if tool is enabled (
tools.prettier.enabled) - Check for native config (
.prettierrc,.prettierrc.json, etc.) - If native config found:
- Use native config
- Inject
enforce.line_lengthas--print-widthCLI flag - Ignore
defaults.prettier
- If no native config found:
- Generate temp config from
defaults.prettier - Inject
enforce.line_lengthas--print-widthCLI flag - Use generated config
- Generate temp config from
This ensures consistent behavior while respecting tool-specific configurations.
Configuration Source Precedence
Independently of the tier model above (which describes how each section is applied),
Lintro resolves where config values come from in a fixed order. Later sources override
earlier ones key-by-key via a deep merge, including nested ai: and tools:
sections:
- Built-in defaults — the empty baseline configuration.
- User-level global config —
~/.lintro-config.yaml(see below). Supplies base values shared across all your projects. - Project config — the first of: an upward-searched
.lintro-config.yamlvariant, or[tool.lintro]inpyproject.toml. Overrides the global config per key.
A value set in the global config survives only where the project config does not
override that exact key path. An auto-discovered global file that is absent or empty is
never an error — only an explicit LINTRO_GLOBAL_CONFIG path that does not exist is
(see below).
In the tools: section a scalar entry such as ruff: false is a complete statement
about the tool, so a project scalar replaces the global mapping wholesale. A project
mapping that never mentions enabled is only a partial statement, so a global
ruff: false still keeps the tool disabled while the project’s other keys apply.
User-Level Global Config
Place a ~/.lintro-config.yaml in your home directory to share settings across every
project. It uses the exact same schema as a project .lintro-config.yaml, including
plugins: and licenses:. Those sections are loaded from the resolved global file as
the base tier and overlaid by a non-global project file; the home dotfile and the active
global file are never treated as a project config, even when LINTRO_GLOBAL_CONFIG=off.
Project config always wins on a per-key basis, so the global file is best for personal
defaults (for example an ai: block, a preferred enforce.line_length, or a
plugins.trusted allowlist) that individual projects can still override.
Resolution order (first existing file wins):
LINTRO_GLOBAL_CONFIG— an explicit file path, oroff(also0,false,no,none, or empty) to disable the global tier entirely. Useful for CI and hermetic test environments that must not inherit a developer’s personal defaults. An explicit path that does not exist is an error rather than a silent fallback, so a typo cannot leave you running without the defaults you asked for; useoffto opt out deliberately.~/.lintro-config.yaml— the primary, authoritative location.$XDG_CONFIG_HOME/lintro/config.yaml— an XDG fallback, where$XDG_CONFIG_HOMEdefaults to~/.configwhen unset.
The home-directory dotfile deliberately takes precedence over the XDG fallback when both
exist, so ~/.lintro-config.yaml is always authoritative.
If your project lives inside your home directory, the upward search for a project
.lintro-config.yaml can reach ~/.lintro-config.yaml itself. That file is counted as
the global tier only — never as both tiers — so it is reported once and its keys are
still listed as global contributions rather than looking like a project override of
itself. This holds even when the global tier is disabled: with
LINTRO_GLOBAL_CONFIG=off the home dotfile is ignored outright rather than demoted to a
project config, which is what makes the switch genuinely hermetic. The same switch
applies to the plugin-trust and licenses loaders: they search upward on their own, so
without it a cwd under $HOME would still pick up plugins: / licenses: from the
home file.
[!IMPORTANT] Upgrading with an existing
~/.lintro-config.yaml. Before user-level global config existed, that file only took effect when the upward search happened to reach it — that is, for projects under your home directory with no config of their own. It is now a base layer for every project, so keys your project config omits (anai:block,enforce.line_length, tool enables) start applying where they previously did not. If you were using it as a no-project-config fallback and want the old behavior, move it into the projects that need it, or setLINTRO_GLOBAL_CONFIG=off.
A global file that only sets ai: or enforce: does not change which tools a default
run selects — language scoping for an otherwise unconfigured project still applies. A
global tools: section or execution.enabled_tools is a deliberate tool selection, so
it opts the run out of language scoping just as a project config does.
Run lintro config to see a Global Config section reporting whether a global file
was found, its resolved path, and which effective values it contributed (the keys your
project config did not override). The same details appear under global_config in
lintro config --json. Only keys that survive section parsing are listed there — an
unrecognized key such as output.typo, or a section Lintro does not read from config at
all, is never reported as a contribution it does not actually make.
# ~/.lintro-config.yaml — applies to all your projects unless overridden
enforce:
line_length: 100
ai:
enabled: true
provider: anthropic # or cursor / openai — required, no default
transport: api # or cli — no default; doctor flags it unset
Lintro Configuration
Configuration File: .lintro-config.yaml
Create a .lintro-config.yaml in your project root:
# Tier 1: EXECUTION - What tools run and how
execution:
enabled_tools: [] # Empty = all enabled tools (config present); no-config first run is language-scoped
fail_fast: false
parallel: true # Run tools in parallel (default: true)
max_workers: 10 # Max parallel workers, 1-32 (default: CPU count)
auto_install_deps: false # Auto-install Node.js deps if missing
# Tier 2: ENFORCE - Cross-cutting settings injected via CLI flags
# These OVERRIDE native configs for consistency
enforce:
line_length: 88 # Injected as --line-length, --print-width, etc.
target_python: py313 # Injected as --target-version
# Tier 3: DEFAULTS - Fallback config when NO native config exists
# Only used if tool's native config file is not found
defaults:
prettier:
semi: true
singleQuote: true
yamllint:
extends: default
# Tier 4: TOOLS - Per-tool enable/disable, config source, and auto-install
tools:
ruff:
enabled: true
prettier:
enabled: true
config_source: '.prettierrc' # Optional: explicit native config path
tsc:
auto_install: true # Override global auto_install for this tool only
Each tools.<name> value must be a mapping (ruff: {} or ruff: {enabled: true}) or a
boolean (ruff: true / ruff: false). A bare tools.ruff: is YAML null and is
rejected by check, format, and config show with exit 1. Use
lintro config validate for a structured report.
Configuration Report Command
Use lintro config to view the current configuration status for all tools:
# View configuration report
lintro config
# Show detailed output including native configs
lintro config --verbose
# Output as JSON for scripting
lintro config --json
The config command shows:
- Enforce settings: Central
line_length,target_python - Tool execution order: Derived from each tool’s declared claims
- Per-tool configuration: Whether enabled, native config found
- Defaults applied: Which tools are using fallback defaults
Severity Counts and the Count Delta
lintro check reports what it found by severity, and how that changed since the
previous check in the same workspace:
Issues: 3 errors, 1 warning, 0 info
Change since last run: -12 errors, +1 warning
The change line is coloured by the direction of improvement, not by the arithmetic
sign: fewer issues is better, so -12 errors is green and +3 errors is red. Only the
severities that actually moved are listed; a run with no movement reads no change.
Severities are compared most-severe first, so trading an error for a warning still reads
as an improvement.
The comparison baseline is stored as severity-baseline.json at the root of the log
directory — LINTRO_LOG_DIR, default .lintro — rather than inside a run-*
directory, so run pruning never removes it.
Only a run that actually measured the project reads or writes it, and the same rule
governs both sides. A run qualifies when it is a check (not format or test, which
measure something else), is not a fmt --dry-run preview (those report as checks but
count only the auto-fixable subset), and had at least one tool actually inspect files.
Runs that do not qualify — an empty directory, an all-skipped toolset, a toolset that
declined to run for want of configuration, an early exit — leave the previous baseline
in place rather than overwriting it with zeroes, so the next comparison may be against
an older run rather than the immediately preceding one. A missing or unreadable baseline
simply omits the change line, and never fails a run.
A tool timeout is treated differently by the baseline and by the badge, on purpose.
A check where one tool timed out but another inspected files still records a baseline:
the alternative would let a single flaky semgrep or gitleaks timeout freeze the
baseline, so the next successful run would report its delta against an arbitrarily old
measurement. lintro badge refuses that same run, because a badge is a public claim and
“0 issues” would assert something about findings that were never collected.
Removed in favour of this (issue #1739).
lintroused to compute a 0-100 “health score” along withcheck --scoreandcheck --fail-under N. The score had no size normalization, so ten errors scored the same in a 200-line project and a 500k-line one, and enabling more tools mechanically lowered it.--fail-undergated CI on that fabricated number; it is gone with no replacement, becausechkalready exits non-zero when issues exist.
The badge
lintro badge # markdown shields.io badge for the issue counts
lintro badge --style flat # shields.io style variant
lintro badge --url # bare badge URL
lintro badge --json # counts, message, color, url, and markdown as JSON
--json and --url are mutually exclusive. lintro badge runs a check and prints a
shields.io snippet such as
. Badge
colour is bright green for a clean run, red when any error was found, and yellow when
only warnings or info issues remain.
Passing any of --errors N / --warnings N / --info N skips the live check entirely
and treats the severities you omit as zero.
A live badge refuses to publish rather than assert a quality claim the run did not support. It exits non-zero, printing no snippet, when the check exited early, when any tool timed out, and when nothing actually inspected a file — which covers an empty directory, an all-skipped toolset, and a toolset that declined to run for want of configuration.
JSON output
In --output-format json the tallies appear under summary, alongside total_issues,
total_net_resolved, total_remaining, timed_out_tools and residual_unknown_tools
(the tools the two derived totals could not cover; see
Mutate-then-verify). summary.total_fixed carries
the same value as total_net_resolved and is deprecated: read the new key. It will
be removed in a later release. The summary.health_score object is gone — this is a
breaking change for anything that read it. severity_delta appears only when a
comparable baseline exists; on a first run the key is absent rather than zero. The
stdout document looks like this:
{
"summary": {
"total_issues": 3,
"total_net_resolved": 0,
"total_fixed": 0,
"total_remaining": 3,
"severity_counts": { "error": 1, "warning": 2, "info": 0, "total": 3 },
"severity_delta": { "error": -4, "warning": 2, "info": 0, "total": -2 }
}
}
--output files and configured JSON artifacts carry the same two severity keys, but the
rest of their summary object differs from the stdout document by design — it adds
timestamp and tools_run, and omits total_remaining:
{
"timestamp": "2026-09-06T00:00:00+00:00",
"action": "check",
"summary": {
"total_issues": 3,
"total_net_resolved": 0,
"total_fixed": 0,
"tools_run": 12,
"timed_out_tools": [],
"severity_counts": { "error": 1, "warning": 2, "info": 0, "total": 3 },
"severity_delta": { "error": -4, "warning": 2, "info": 0, "total": -2 }
}
}
Tool Timeouts in the JSON Report
A tool timeout is an execution failure, never a lint finding. Every tool accounts for one the same way, so a consumer can tell an infrastructure flake apart from a genuine finding using evidence about its own run.
A timed-out tool reports:
success: false— the run failed, and the process exit code stays non-zero.timed_out: true— the machine-readable marker to classify on. No need to regex-match the human-readableoutputstring.issues_countcounting only genuine findings. No syntheticTIMEOUTpseudo-issue is invented, and the timeout never reachessummary.total_issues. Issues legitimately detected before the timeout (for example the pre-fix check of aformatrun) are still reported.
summary.timed_out_tools lists the tools that timed out, in execution order:
{
"summary": {
"total_issues": 0,
"total_net_resolved": 0,
"total_fixed": 0,
"total_remaining": 0,
"timed_out_tools": ["mypy"]
},
"results": [
{
"tool": "mypy",
"success": false,
"issues_count": 0,
"timed_out": true,
"output": "mypy execution timed out (300s limit exceeded)..."
}
]
}
This makes the conservative CI classification expressible: at least one tool timed out
AND total_issues == 0. A run with a real finding still reports a non-zero
total_issues, and a non-timeout tool failure reports timed_out: false, so neither is
ever mistaken for a flake.
Artifacts Under GitHub Actions
When GITHUB_ACTIONS=true is detected, lintro auto-emits two side-channel artifacts
regardless of --output-format, so the console/grid output every existing consumer
parses is untouched and no second invocation is needed:
| Format | Path | Purpose |
|---|---|---|
| SARIF | .lintro/artifacts/sarif/results.sarif.json | GitHub Code Scanning ingestion |
| JSON | .lintro/artifacts/json/results.json | Structured CI evidence (see above) |
SARIF omits clean tools and omits failures that produced no issues, so it cannot be used
to classify a timeout without failing open. The JSON report covers every tool that ran
and carries the per-tool timed_out flag.
Any format listed in execution.artifacts is emitted in addition to these, in every
environment.
Output Presentation
Purely cosmetic console output is controlled by the output section. These settings
never affect machine-readable documents (JSON/SARIF) or on-disk artifacts.
# .lintro-config.yaml
output:
art: true # Show decorative ASCII art after a run (default: true)
The decorative ASCII art printed after check/fmt is only emitted to an interactive
terminal. It is always suppressed when:
- stdout is not a TTY (piped output, CI logs, redirected files), or
output.art: falseis set in config, or- the
--no-artflag is passed tolintro check/lintro format.
The art is never written to .lintro/run-*/report.md, console.log, or any
--output-format stream regardless of these settings.
lintro check --no-art # suppress art for this run only
lintro format --no-art
Command-Line Options
Global Options
# Output options
lintro check # Use grid formatting
lintro check --output results.txt # Save output to file
lintro check --group-by [file|code|none|auto|category] # Group issues
# Tool selection
lintro check --tools ruff,prettier # Run specific tools only
lintro check --tools all # Run all available tools
# File filtering
lintro check --exclude "*.pyc,venv" # Exclude patterns
lintro check --include-venv # Include virtual environments
lintro check path/to/files # Check specific paths
Confirmation Prompt Options
# Skip confirmation prompts (auto-accept)
lintro check --yes
lintro check -y
# Useful in scripts and CI where no interactive input is available
lintro format --yes --tools ruff,prettier
The --yes/-y flag is available for check, format, and test commands. It skips
any confirmation prompts that would otherwise pause execution.
Note: Confirmation prompts are automatically skipped in non-TTY environments (CI
pipelines, Docker containers, scripts with redirected output), so --yes is typically
only needed when running interactively and you want to avoid prompts.
Node.js Dependency Options
# Auto-install Node.js dependencies if node_modules is missing
lintro check --auto-install --tools tsc
# Useful for TypeScript projects where dependencies aren't installed
lintro check src/ --tools tsc,oxlint --auto-install
The --auto-install flag is available for both check and format commands. When
enabled, Lintro runs bun install (or npm install if bun is unavailable) before
running the tools that need a project’s dependency tree to work: tsc, vue-tsc,
svelte-check and astro-check.
It does not apply to standalone Node.js binaries such as prettier, oxlint,
oxfmt, stylelint or markdownlint-cli2. Those run fine without a local
node_modules: if the project has not installed them, resolution falls through to a
binary on PATH and then to a version-pinned bunx/npx fetch — see
Node.js Tool Resolution. When one of them cannot be resolved
at all, Lintro reports a ⏭️ SKIP row with the reason instead of a pass — installing
project dependencies would not have helped.
You can also enable this globally via configuration:
# .lintro-config.yaml
execution:
auto_install_deps: true
Per-tool auto-install lets you override the global setting for individual tools:
# .lintro-config.yaml
tools:
tsc:
auto_install: true # Always auto-install deps for tsc
prettier:
auto_install: false # Never auto-install deps for prettier
oxlint:
# Omit auto_install to inherit the global setting
Resolution order: per-tool auto_install > global auto_install_deps > false
Container Auto-Detection
Lintro automatically detects container environments (Docker, Podman, LXC, Kubernetes) and enables auto-install by default when running in a container. This means Node.js tools work out of the box in Docker without any configuration.
You can override this behavior explicitly via configuration or the --auto-install CLI
flag:
# .lintro-config.yaml — disable auto-install even inside a container
execution:
auto_install_deps: false
See the Docker Usage Guide for more details on container behavior.
Tool-Specific Options
# Tool-specific options (key=value; lists use |)
lintro check --tool-options "ruff:line_length=88,prettier:print_width=80"
# Example with lists and booleans
lintro check --tool-options "ruff:select=E|F|W,ruff:preview=True"
# Exclude patterns
lintro check --exclude "*.pyc,venv,node_modules"
Environment Variables
Lintro reads the following environment variables at runtime. Most configuration is done
through .lintro-config.yaml and CLI flags; these variables cover a few runtime
overrides.
# Override the directory where run logs/artifacts are written (default: .lintro)
export LINTRO_LOG_DIR=/tmp/lintro-runs
# Timeout (seconds) for tool version checks (default: 30)
export LINTRO_VERSION_TIMEOUT=60
# Force Docker install-context detection (set to 1)
export LINTRO_DOCKER=1
# Opt in to loading external (third-party) plugins. Disabled by default.
export LINTRO_ENABLE_EXTERNAL_PLUGINS=1
# Force-clear discovery/config caches on each invoke. Accepted truthy values
# after trim + case-insensitive match: 1, true, yes, on.
export LINTRO_NO_CACHE=1
# AI config overlays (flag > env > .lintro-config.yaml > default). See
# docs/ai-features.md "Invocation overrides".
export LINTRO_AI_PROVIDER=cursor
export LINTRO_AI_MODEL=cursor-grok-4.6-high
export LINTRO_AI_TRANSPORT=cli
export LINTRO_AI_ENABLED=1
export LINTRO_AI_REVIEW=1
export LINTRO_AI_MAX_COST_USD=uncapped # sentinel; a positive number is a USD cap; overlay 0 is an error
export LINTRO_AI_PROVIDERS__CURSOR__TRUST_WORKSPACE=false # one ai.providers.<name>.<field> setting
| Variable | Description | Default |
|---|---|---|
LINTRO_LOG_DIR | Base directory for run logs and artifacts | .lintro |
LINTRO_VERSION_TIMEOUT | Timeout in seconds for tool version checks (must be >= 1) | 30 |
LINTRO_DOCKER | Force Docker install-context detection when set to 1 | - |
LINTRO_CONFIG | Shown in the lintro environment report; informational only | - |
LINTRO_ENABLE_EXTERNAL_PLUGINS | Opt in to loading external (third-party) plugins (1/0) | 0 |
LINTRO_NO_CACHE | Force-clear caches each invoke (1/true/yes/on) | - |
LINTRO_AI_PROVIDER | Override ai.provider (anthropic / cursor / openai) | - |
LINTRO_AI_MODEL | Override ai.model | - |
LINTRO_AI_TRANSPORT | Override ai.transport (api / cli) | - |
LINTRO_AI_ENABLED | Override ai.enabled (1/0/true/false) | - |
LINTRO_AI_REVIEW | Override ai.review (1/0/true/false) | - |
LINTRO_AI_MAX_COST_USD | Override ai.max_cost_usd (USD cap or uncapped; 0 is rejected — YAML 0 is a $0 cap, so never copy the value between the two surfaces) | - |
LINTRO_AI_PROVIDERS__<PROVIDER>__<FIELD> | Override one ai.providers.<provider>.<field> setting, e.g. LINTRO_AI_PROVIDERS__CURSOR__TRUST_WORKSPACE. Provider and field are upper-cased and joined by a double underscore; an unknown provider or field is an error, not a silent no-op | - |
LINTRO_NO_CACHE is truthy after trim and case-insensitive match against 1, true,
yes, or on.
Note: There is no environment variable for tool timeouts, verbosity, exclude patterns, output format, or auto-install. Use CLI flags (
--exclude,--output-format,--auto-install) or.lintro-config.yamlfor those settings. Auto-install is resolved from the--auto-installflag, thenexecution.auto_install_deps, then container auto-detection — not from an environment variable.
External Plugins (Trust Model)
Lintro can load third-party tool plugins published as Python packages that expose a
lintro.plugins entry point. Because loading a plugin imports and executes its code,
external plugins are disabled by default — a default installation never runs
third-party plugin code at startup. This is a security boundary: any package installed
in the same environment could otherwise execute arbitrary code every time Lintro runs.
See SECURITY.md for the full threat model.
Enable external plugins only after you have reviewed and trust them, using either mechanism:
# .lintro-config.yaml
plugins:
# Opt in and restrict loading to an explicit allowlist (recommended).
# Names match the entry-point name or the distribution (package) name.
trusted:
- my-org-tool
- another-plugin
# Optional: enable loading of ALL discovered plugins (no allowlist).
# Prefer 'trusted' over this blanket toggle.
enabled: false
Equivalent pyproject.toml:
[tool.lintro.plugins]
trusted = ["my-org-tool", "another-plugin"]
enabled = false
Or, for a one-off/CI run, the environment variable:
export LINTRO_ENABLE_EXTERNAL_PLUGINS=1
Resolution rules:
- Loading is enabled when
LINTRO_ENABLE_EXTERNAL_PLUGINSis truthy or thepluginsconfig opts in (atrustedallowlist is itself an opt-in, as isenabled: true). - When a
trustedallowlist is present, only plugins whose entry-point name or distribution name is listed are loaded; all others are skipped and logged, regardless of how loading was enabled. - With no allowlist configured, enabling loads all discovered
lintro.pluginsentry points — use this only in fully trusted environments.
Pre-Execution Summary
Before running tools, Lintro displays a configuration summary table showing the effective settings for the current run:
- Environment: Local, Container, or CI
- Auto-install: Whether auto-install is enabled (and the source — CLI flag, config, or container detection)
- Tools: Which tools will run
- Skipped tools: Which tools were skipped and why
This summary is shown for all output formats except JSON (--output-format json).
Skipped Tools
When tools are skipped, Lintro reports them in the summary table with a SKIP status
and a note explaining the reason. Common skip reasons:
| Reason | Description |
|---|---|
node_modules not found | Node.js deps missing and auto-install is disabled |
disabled in config | Tool disabled via tools.<name>.enabled: false |
not in enabled_tools | Tool not in execution.enabled_tools allowlist (default/--tools all only; named --tools bypasses) |
deferred to <tool> | Framework tool preferred (e.g., tsc to vue-tsc) |
| Version check messages | Tool version below minimum required |
Skipped tools do not affect exit codes — only tools that run and find issues contribute to a non-zero exit.
Disabling a tool from pyproject.toml
The tools: section of .lintro-config.yaml has two equivalent pyproject.toml
spellings — a flat per-tool table, or a nested tool/tools table:
[tool.lintro.trufflehog]
enabled = false
# Equivalent, mirroring the YAML `tools:` section
[tool.lintro.tool.trufflehog]
enabled = false
[tool.lintro.execution] and [tool.lintro.enforce] nested tables are equivalent to
the YAML execution: / enforce: sections. Flat keys such as fail_fast under
[tool.lintro] also work.
pyproject.toml is a fallback: when a .lintro-config.yaml exists, it is the only
configuration source and these tables are not consulted.
Project Setup with lintro init
Run lintro init to detect project languages, select an install profile, and generate a
.lintro-config.yaml tailored to your stack:
lintro init # auto-detect languages and write config
lintro init --minimal # fewer defaults
lintro init --profile python # use a specific profile
lintro init --force # overwrite existing config
If a config file already exists, lintro init merges new tool entries without
clobbering user-managed sections. Use --force to replace the file entirely.
After init, run lintro install --profile recommended and lintro doctor to install
and verify tools.
Doctor: Config-Aware Health Checks
lintro doctor respects execution.enabled_tools and per-tool tools.<name>.enabled
settings. By default, disabled tools are shown separately and not counted as failures:
lintro doctor # check enabled tools only
lintro doctor --all # check every manifest tool
lintro doctor --tools ruff,mypy # explicit tools override config filtering
lintro doctor --json # machine-readable output for CI
The --json output includes per-tool fields: installed, recommended, min_version,
status (OK, MISSING, OUTDATED, INCOMPATIBLE, DISABLED, UNKNOWN), install_hint, and
upgrade_hint. upgrade_hint is the install-strategy command (pin conflicts, uv vs
pip, node package manager). Doctor, lintro versions --json, and MCP lintro_versions
always include advisory (null when the tool is current). When present, that object
has the detected update channel and a path-heuristic update_command. Execute
upgrade_hint to change installs; update_command is diagnostic and can disagree with
upgrade_hint when the binary path and the manifest strategy name different managers.
binary_path is the tool binary resolved past cargo/bash wrappers. A standalone or
unknown path does not inherit a pip/npm/cargo update_command from manifest
install.type.
The human lintro versions table labels a tool OUTDATED when it meets the minimum
but trails the recommended pin (below_recommended). JSON version_check_passed stays
true in that case; only the table status string changed from PASS. The process still
exits 0.
Node.js Package Manager Policy {#node-package-manager-policy}
lintro install used to pick bun whenever bun happened to be on PATH, and to install
globally regardless of what the project said. In an npm-first repository that
created two authorities: your own commands and your editor used the project’s local
dependency, while lintro installed and upgraded a global one (#2005).
Which manager, in priority order:
- Explicit choice —
lintro install --node-package-manager npm. packageManagermetadata — the Corepack field inpackage.json("packageManager": "pnpm@9.1.0").- Lockfile evidence —
bun.lock/bun.lockb,pnpm-lock.yaml,yarn.lock,package-lock.json,npm-shrinkwrap.json. - Available manager — bun if installed, otherwise npm, then pnpm, then yarn. bun and npm are lintro’s own preference; pnpm and yarn are included so a machine that only has those still gets a command it can run. This is the only step where lintro’s own preference decides anything.
Availability does not veto the first three. If your project is npm-locked but only
bun is installed, lintro still tells you to run npm install -D … rather than quietly
writing a bun.lock into your repository.
Where it installs. Inside a Node project (anything with a package.json at or above
the working directory), lintro adds a dev dependency — npm install -D <pkg>@<ver>
— because the project owns its tool versions and a lockfile-pinned dependency is what
Node.js Tool Resolution will run. Global installs are
reserved for --global or for an environment with no project manifest at all (a bare CI
runner, a container image, your $HOME). The upward search for a manifest stops at the
first directory containing a .git entry, and never treats $HOME itself as a project
root unless that is where you ran the command — a stray ~/package.json must never
collect your tools.
Project pins are never replaced implicitly. If package.json declares a version
that differs from lintro’s recommendation, --upgrade reports the difference and asks
for an explicit decision instead of rewriting your manifest:
$ lintro install prettier --upgrade
Node package manager: npm (from lockfile) → project dev dependency
…
prettier Upgrade prettier explicitly: this project pins 3.1.0 in package.json but
lintro recommends 3.9.4. Run `npm install -D prettier@3.9.4` to adopt
lintro's version, or keep the project pin.
The comparison is deliberately literal, not a semver range solve: a spec that names the
recommended version exactly — 3.9.4, ^3.9.4, ~3.9.4, =3.9.4, v3.9.4 — is not a
conflict and upgrades normally. On a first install of a declared-but-missing package,
those same spellings emit the manager’s install-all command (npm install,
bun install, …) so the lockfile pin is restored, rather than a versioned add that
would rewrite the range. Anything else, including a wider range such as ^3.9.0 that a
resolver would satisfy with 3.9.4, is reported so you decide. Erring toward asking is
deliberate: the cost of a needless question is far below the cost of silently rewriting
someone’s package.json.
Planning matches execution. The version probe for npm-installed tools resolves
through the same chain a check uses, so lintro install reports on the binary
lintro check will actually run rather than on whatever a bare name finds on PATH.
| Flag | Effect |
|---|---|
--node-package-manager {bun,npm,pnpm,yarn} | Force the manager, overriding packageManager and lockfiles. |
--global | Install npm-managed tools globally instead of as project dev dependencies. |
Install Lock / Export
Use lintro install --write-lock to capture the resolved install plan:
lintro install --profile recommended --write-lock
This writes .lintro-install.lock.json containing every tool in the plan with its
version, install hint, and status (to_install, to_upgrade, ok, outdated,
manual, skipped), plus the selected profile and detected languages. Share this file
with teammates or CI to reproduce the same tool set.
Tool Configuration
Lintro respects each tool’s native configuration files, allowing you to leverage existing setups.
Enforce Settings (Cross-Cutting Concerns)
The enforce tier contains settings that MUST be consistent across tools. These are
injected directly as CLI flags to each tool, overriding their native configs.
enforce:
line_length: 88 # Injected as --line-length (ruff, black) or --print-width (prettier)
target_python: py313 # Injected as --target-version (ruff, black)
How CLI injection works:
| Tool | CLI Flag for line_length | CLI Flag for target_python |
|---|---|---|
| Ruff | --line-length 88 | --target-version py313 |
| Black | --line-length 88 | --target-version py313 |
| Prettier | --print-width 88 | N/A |
Tools without CLI support:
Some tools (Yamllint, Markdownlint) don’t have CLI flags for line length. For these:
- Use the
defaultstier to provide fallback config - Or configure their native config files manually
Defaults Tier (Fallback Config)
The defaults tier provides fallback configuration for tools that have no native config
file. This is useful for ensuring consistent settings without creating multiple config
files.
defaults:
prettier:
semi: true
singleQuote: true
tabWidth: 2
trailingComma: es5
yamllint:
extends: default
rules:
line-length:
max: 88
markdownlint:
MD013:
line_length: 88
code_blocks: false
tables: false
When defaults are applied:
- Lintro checks if the tool has a native config file (e.g.,
.prettierrc) - If NO native config exists, Lintro generates a temp file from
defaults - If native config EXISTS,
defaultsare ignored (native config is used)
Tool Ordering Configuration
Execution order is not authored. Since #1742 it is derived from what each tool
declares it touches and what it does to it, so it is complete, verifiable and the same
everywhere lintro reports it. The scalar tool_order, tool_order_custom,
tool_priorities and definition.priority settings are deleted; so is the
[tool.lintro.post_checks] table, whose only real job — running black after ruff — now
falls out of the model.
There is exactly one configurable input, and it is a tie-break rather than an order:
execution.precedence names which of two tools that can write the same file has
authority over the other. Everything else about the order still follows from the claims
— see Write precedence.
Upgrading. Leftover tool_order, tool_order_custom, tool_priorities or
[tool.lintro.post_checks] keys warn as unknown and are ignored; a run that carried
them keeps working and needs no edit to succeed. The one hard break is
lintro list-tools --show-conflicts, which is removed and now fails as an unknown
option — use lintro check --explain-order to see what orders a run instead.
Derivation rules for phase edges — the per-pattern FIX → FORMAT → CHECK
ordering. They are not the whole graph: a second class of edge, derived from the files
each mutating tool would actually be handed, is added on top of them and is described
under Write precedence. Where the two disagree
the write-conflict edge is the one that decides, so read the “no edge” rows below as “no
phase edge”.
| Rule | Behaviour |
|---|---|
| Phase per pattern | A tool occupies the earliest phase it holds there: FIX → FORMAT → CHECK |
| One invocation | A tool that both fixes and checks a pattern sits in FIX; its diagnostics come out with it |
| Edges | Every earlier-phase tool precedes every later-phase tool for that pattern |
| Ties | Equal phases derive no phase edge; two writers that share a file still get a conflict edge |
| Broad claims | Only * subsumes: *.py never joins the test_*.py group, though two writers of one test_a.py still conflict |
| Project-scoped | A claim with no patterns (osv-scanner) derives no phase edge; a patternless writer conflicts with every writer under its roots |
| Cycles | Reported before ordering, naming the tools and the patterns whose edges closed them |
The pairings this recovers include ruff → black on Python, oxlint → oxfmt on JS/TS,
stylelint → prettier on CSS, prettier → html-validate on HTML and
shfmt → shellcheck on shell.
Parallel runs use the same graph — both classes of edge — so two tools share a batch only when no derived edge separates them, so a mutator and a tool that must observe its writes never run concurrently.
Inspecting the order
lintro check --explain-order # the order this run would use, then exit
lintro check --tools ruff,black --explain-order
lintro format --explain-order
lintro doctor # compact summary section
lintro config # the same order, as a table
--explain-order prints the order and exits without running a single tool. Tool
selection is resolved exactly as the real run would resolve it, and the order comes from
the same scheduler the run uses, so what you see is what would have executed.
Sample output:
Execution order (derived from tool claims)
This is the order that runs. It is derived from what each tool claims to touch, not configured.
1. ruff
(unconstrained; alphabetical tiebreak)
2. black
after ruff — *.py: ruff(fix) -> black(format)
after ruff — *.pyi: ruff(fix) -> black(format)
Cycles (0): the derived graph is a DAG.
Format authority on Python
*.py is the one pattern in the 42-tool set with two mutating claimants. Black holds
{FORMAT} and ruff holds {FIX, FORMAT}, so black owns the format phase (fewest
mutating capabilities wins) and ruff is demoted to its fix capability: when black is in
the run, ruff’s format / format_check stages are switched off unless you ask for
them explicitly through --tool-options or [tool.lintro.ruff].
Mutate-then-verify (lintro format)
Since #1743 lintro format runs in two phases:
- Mutation. Every mutating capability (
FIX,FORMAT) runs in derived DAG order. - Verify. One pass runs the
CHECKcapability of the same tools. For every tool that declaresCHECK, that single pass is the run’s authoritative residual count.
A tool’s own post-fix opinion is no longer the residual — it is replaced, not added, so no issue is counted twice. The figure the run displays is net resolved: issues detected before the mutation phase minus the residual measured after it. It is not called “fixed” because no tool reported it — it is the difference between two measurements, and a finding one tool fixed and another reintroduced nets out of it. The word “fixed” is kept only where a tool reports its own fix count.
The machine-readable keys for this figure, and for the third state below:
| Surface | Key | Meaning |
|---|---|---|
| JSON, per tool | net_resolved | Before minus after; null when the residual is unknown |
| JSON, per tool | remaining | Measured residual; null when the residual is unknown |
| JSON, per tool | residual_unknown | true when no after-count was taken |
| JSON, per tool | residual_unknown_reason | Why — the check crashed, timed out or was skipped |
| JSON, run summary | summary.total_net_resolved | Sum over the tools that were measured |
| JSON, run summary | summary.total_remaining | Sum over the tools that were measured |
| JSON, run summary | summary.residual_unknown_tools | Tools the two totals above do not cover |
| JSONL stream, per tool | net_resolved_count | Per-result net resolved; absent when the residual is unknown |
The previous spellings still appear with the same values and are deprecated, to be
removed in a later release: fixed per tool (read net_resolved),
summary.total_fixed (read summary.total_net_resolved), fixed_issues_count in the
JSONL stream (read net_resolved_count) and fixed_count in the MCP tool summary (read
net_resolved).
Which tools the pass covers. Every mutating capability declares CHECK, so the
central verify pass covers every tool that can rewrite a file, the four formatters that
used to be FORMAT-only (prettier, oxfmt, rustfmt and shfmt) included since #2607. It
is the only source of residuals: no tool keeps a private post-mutation recheck, and a
tool’s own post-fix opinion is discarded, not added. Declaring CHECK does not change
which tools lintro check runs — check-run selection has always included every
registered tool; the capability is what the fmt run’s verify pass reads when it decides
who reports residuals.
This is the only place cross-tool interference is visible. If ruff fixes a file and prettier then reformats it, ruff’s own post-fix lint already ran; a run-level pass sees the result of every mutating tool that ran before it.
Scope. Verifying every file again would double the cost of a format run, so the pass
is narrowed by fingerprint (mtime + size, from lintro/utils/file_cache.py; the pass
itself is lintro/tools/core/verify_pass.py): every file a mutating capability could be
handed is stat’ed before the mutation phase and re-stat’ed after, and only the files
whose fingerprint moved are verified. A file nobody rewrote keeps the issues it had
before the run, so narrowing never loses a residual.
mtime over-approximates: a formatter rewriting a file to byte-identical content still bumps mtime, so a file may be re-verified needlessly. That is a wasted check, not a wrong answer. Size alone is near-useless (a quote-style rewrite is the same length) and serves only as a cheap tiebreak.
The floor. When fingerprints cannot be trusted — a stat that fails, or a
filesystem with whole-second mtime granularity, where a rewrite inside the same second
is invisible — the pass falls back to every file handed to a mutating capability.
That is the documented floor, not a separate code path: the same verify pass runs over a
wider set. The run reports which it used:
Verify pass: re-checking 12 changed file(s)
Verify pass: re-checking 340 file(s) (coarse mtime resolution)
Verify pass: re-checking 340 file(s) (some files could not be fingerprinted)
lintro check is unaffected and stays read-only: it takes no snapshot and runs no
verify pass. So does lintro format --dry-run, which is a check-mode preview.
When the verify CHECK cannot answer — it was skipped by a version gate, it raised,
or it timed out — the residual is unknown, a third state beside “clean” and “N
remaining”. A timeout counts here even when the tool returned findings: a multi-root
checker such as golangci-lint aggregates one result across module roots, so missing the
deadline on one root leaves the others unchecked. An unknown residual fails the run, and
it is never rendered as a measured after-count: the summary prints unknown in the net
resolved and remaining columns with the reason beside it, and the JSON report carries
"remaining": null, "net_resolved": null (and "fixed": null) plus
"residual_unknown": true and "residual_unknown_reason". The run summary names every
such tool in summary.residual_unknown_tools, because summary.total_net_resolved and
summary.total_remaining cover only the tools that were measured — a zero there is
never a claim about a tool in this state. The TOTALS table says the same thing with a
Residual Unknown (tools) row. The tool’s pre-fix findings are still listed, because
those were measured.
Mutation runs in parallel, in safe batches. Two mutating capabilities in flight over
the same file would race on its bytes, so the scheduler keeps them out of the same batch
— see Write precedence. Within a batch the
full parallel fan-out applies under format exactly as it does under check.
Write precedence (overlapping mutators)
Two tools that can rewrite the same file must never run at the same time: each reads,
rewrites and writes the whole file, so the second write silently discards the first
tool’s edit. The derived scheduler closes that by deriving a second class of edge
alongside the per-pattern FIX → FORMAT → CHECK ordering.
Overlap is decided by files, not by globs. Every mutating tool’s run-scoped
candidate list is resolved the way the verify pass resolves it — the run’s paths, its
excludes and --diff all apply — and canonicalised with realpath. Two writers
conflict when those sets intersect. So Cargo.toml (clippy) relates to *.toml
(taplo), and *.py relates to test_*.py, neither of which a string comparison of the
patterns would ever find.
--incremental is the deliberate exception: it narrows what each tool is handed, but
conflict planning ignores it and compares the unnarrowed sets. Narrowing can only
remove edges, and the evidence it would rest on is a per-tool mtime cache — the same
kind of evidence the verify pass keeps a documented floor for. An overlap missed because
a cache called a file unchanged is a lost write; an overlap found for a file neither
tool ends up touching costs one batch.
Three cases cannot be answered by a file list, and all three split the batch rather than risk a lost write:
- a project-scoped writer — one that declares no patterns, or whose definition is
partitionable=False(clippy, golangci-lint) — expands from the handed paths to a whole project root, so it conflicts with every writer under that root; - an unknown tool, meaning a name the registry cannot resolve, gets a batch of its own (a registered tool that deliberately declares no claims, such as commitlint, stays unconstrained as before);
- a caller that names no scan paths (
lintro config,lintro list-tools) has no candidate lists, so overlap falls back to asking whether two patterns could match one file.
Read-only capabilities never conflict. CHECK reads, so lintro check,
lintro format --dry-run and the verify pass batch exactly as they did before this rule
existed.
Who writes last. For a conflicting pair the winner is the authority: it runs last,
so its write is the one that survives, and it keeps FORMAT. The winner is chosen by,
in order:
- a configured override,
execution.precedence; FIXbeforeFORMAT— a fix rewrites structure and leaves layout dirty;- the tool with fewer mutating capabilities runs last, so a dedicated formatter outranks a multi-capability tool (this is why black, not ruff, formats Python);
- the alphabetically last tool id runs last.
One format owner per overlapping scope. When several conflicting tools declare
FORMAT, the scheduler names one of them the owner and records every other tool’s
FORMAT as demoted for that scope. The demoted tool’s CHECK stays enabled, so it
keeps reporting. The decision is re-resolved on every run, not frozen at lintro init,
and it is printed by lintro check --explain-order, lintro doctor and lintro init:
Format ownership (1):
*.py: black owns FORMAT; ruff(format) demoted (FIX runs before FORMAT; override with execution.precedence)
What the record does today is set the write order: the owner runs last, so where two
formatters both rewrite a file its layout is the one left on disk. The one place a
demoted FORMAT is additionally switched off at the tool level is ruff when black is
selected — the Ruff vs Black policy below, which
predates this rule and still lives in the executor’s tool configuration rather than
reading the record. Wiring every formatter’s stages to the derived record is a
follow-up; until it lands, treat the reported owner as the authority on order and
reporting, and the ruff/black switch as unchanged.
Overriding it. execution.precedence takes [winner, loser] pairs. The winner has
authority on every scope the two share — it is ordered last, and it is the tool the
reports name as the owner:
execution:
precedence:
- [ruff, black] # order ruff last, and report ruff as the Python FORMAT owner
Note the limit that follows from the paragraph above: this reverses the order and the
reported owner, but it does not by itself re-enable ruff’s formatter — that switch still
follows the ruff/black policy. Set ruff’s format (and format_check) tool options
explicitly if you want ruff to do the formatting.
Contradictory pairs are a hard error, never a silent fallback. That covers a pair and its reverse:
Configured tool precedence is contradictory: both [ruff, black] and [black, ruff] appear
in execution.precedence. Keep one of the two pairs so the tools form an order rather
than a loop.
and a longer loop closed by several pairs:
Configured tool precedence is contradictory: one_fixer <-> two_fixer on *.py. Remove or
reverse one of the pairs in execution.precedence so the tools form an order rather than
a loop.
Performance shape. A broad-pattern mutator is a serialisation point: typos claims
*, so it overlaps every other writer and is a batch of one until it narrows its claim.
Pattern-disjoint groups stay parallel — ruff (*.py) and taplo (*.toml) still share a
batch, and both of those are writers, which is what makes the example worth anything.
Real projects mix file types, so two or three batches is the common case, and splitting
when in doubt costs one batch while not splitting costs a lost edit.
Ruff vs Black Policy (Python)
Lintro enforces Ruff-first linting and Black-first formatting when Black is configured as a post-check.
- Ruff: primary linter (keep strict rules like
COM812trailing commas andE501line length enabled for checks) - Black: primary formatter (applies formatting during post-checks; performs safe line breaking where Ruff’s auto-format may be limited)
Runtime behavior with Black as post-check:
-
lintro format
- Ruff fixes lint issues only (Ruff
format=False) unless explicitly overridden - Black performs formatting in the post-check phase
- Ruff fixes lint issues only (Ruff
-
lintro check
- Ruff runs lint checks (Ruff
format_check=False) unless explicitly overridden - Black runs
--checkas a post-check to enforce formatting
- Ruff runs lint checks (Ruff
Overrides when needed:
# Force Ruff to format during fmt
lintro format --tool-options ruff:format=True
# Force Ruff to include format check during check
lintro check --tool-options ruff:format_check=True
Rationale:
- Avoids double-formatting churn (Ruff format followed by Black format) while preserving
Ruff’s stricter lint rules (e.g.,
COM812,E501). - Black’s safe wrapping is preferred for long lines; Ruff continues to enforce lint limits during checks.
Node.js Tool Resolution {#nodejs-tool-resolution}
Every Node.js tool resolves the same way. There is one chain, implemented once in
NodeJSBuilder (lintro/tools/core/command_builders.py), and it applies to
astro check, commitlint, html-validate, markdownlint-cli2, oxfmt, oxlint,
prettier, spectral, stylelint, svelte-check, tsc and vue-tsc alike:
node_modules/.bin/<binary>, searched upward from the directory being checked until the nearestpackage.jsonor.git(whichever is hit first). A nested package with its ownpackage.jsonstops there; a decoynode_modulesabove that boundary is ignored. This is the preferred answer: it is lockfile-pinned, offline, and it is the same binary your editor and your ownnpm runscripts use.- the
PATH-resolved absolute path of<binary>— a global install (bun add -g,npm install -g) or a Homebrew formula. bunx <package>@<pinned>/npx --yes --package <package>@<pinned> <binary>— a registry fetch at the version Lintro pins in its manifest.bunxis tried first, thennpx.bunxkeeps the short form when the executable name matches the package name (bunx prettier@<pinned>); it uses--packagewhen they differ (bunx --package typescript@<pinned> tsc).npxalways uses--yes --packageso it cannot hang waiting for a TTY prompt.- bare
<binary>— last resort, fails if nothing is installed.
@latest is never resolved at runtime, for any tool. Branch 3 emits a one-time warning
because it needs network access to the npm registry and imposes the pinned package’s own
engines floor on your runtime; a failure on that branch is reported with install
guidance rather than the tool’s raw error.
Which install should I use? A project devDependency (bun add -D <pkg> /
npm install -D <pkg>) is the best answer for every Node tool — it wins the chain and
pins the version through your lockfile. A global or Homebrew install is a valid fallback
and is used whenever no project-local install is present.
Changed after v0.115.0 (#1811). The chain above used to apply only to
html-validate. Other Node tools either went straight tobunx <binary>/npx <binary>(never consultingPATH, and resolving@latest) or preferredPATHahead of any project-local install. Two consequences worth checking after upgrading:
- A project-local install now wins. If a project pins an older tool version as a devDependency while you relied on a newer global one, Lintro now runs the local pin — the same version your editor runs. Remove the devDependency, or upgrade it, if you wanted the global.
prettieris no longerPATH-only. It previously had no Node builder at all and was resolved as a bareprettiername againstPATH; a project’s lockfile-pinned prettier was never used. It now follows the same chain as everything else.Also fixed on the way:
commitlintandmarkdownlint-cli2had nonpxbranch, so on a machine with npm but no bun a devDependency was unreachable and the tool reported a skip. Both now resolve like every other Node tool.
Because the version check (verify_tool_version) resolves through this same chain, the
binary Lintro version-gates is now the binary Lintro runs, for every Node tool rather
than just for html-validate.
Python Tools
Ruff Configuration
File: pyproject.toml
[tool.ruff]
# Basic configuration
line-length = 88
target-version = "py313"
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".mypy_cache",
".ruff_cache",
".venv",
"__pypackages__",
"migrations",
]
# Rule selection
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"N", # pep8-naming
"D", # pydocstyle
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
]
ignore = [
"D100", # Missing docstring in public module
"D104", # Missing docstring in public package
]
# Per-file ignores
[tool.ruff.per-file-ignores]
"tests/**/*.py" = ["D100", "D103"]
"__init__.py" = ["F401"]
# Import sorting
[tool.ruff.isort]
known-first-party = ["lintro"]
force-single-line = true
# Docstring configuration
[tool.ruff.pydocstyle]
convention = "google"
Alternative: setup.cfg
[tool:ruff]
line-length = 88
select = E,W,F,I,N,D
exclude = .git,__pycache__,.venv
Mypy Configuration
- Default run mode:
--strictwith--ignore-missing-importsenabled to avoid third-party stub noise (applied unless you override viaset_options()or--tool-options). - Config discovery: mypy auto-discovers
pyproject.toml [tool.mypy],mypy.ini, orsetup.cfg [mypy]and the discovered config is passed via--config-file. When a native config providesexclude, Lintro does not add its default test/test_samples excludes; otherwise, it applies the defaults plus.lintro-ignore. - Recommended overrides when needed:
# Disable strict mode temporarily
lintro check --tools mypy --tool-options mypy:strict=False
# Surface missing-import errors (no ignore)
lintro check --tools mypy --tool-options mypy:ignore_missing_imports=False
# Pin target Python version
lintro check --tools mypy --tool-options mypy:python_version=3.13
Bandit Configuration
File: pyproject.toml
[tool.bandit]
exclude_dirs = ["tests", "venv", ".git"]
tests = ["B101,B102,B103"] # Specific test IDs to run
skips = ["B101"] # Test IDs to skip
confidence = "MEDIUM" # Minimum confidence level
severity = "LOW" # Minimum severity level
[tool.bandit.assert_used]
exclude = ["test_*.py"] # Files to exclude from assert_used test
File: .bandit
[bandit]
exclude = tests,venv,.git
tests = B101,B102,B103
skips = B101
confidence = MEDIUM
severity = LOW
[[tool.bandit.assert_used]]
exclude = test_*.py
Available Options:
tests: Comma-separated list of test IDs to runskips: Comma-separated list of test IDs to skipexclude: Comma-separated list of paths to excludeexclude_dirs: List of directories to exclude (pyproject.toml only)severity: Minimum severity level (LOW,MEDIUM,HIGH)confidence: Minimum confidence level (LOW,MEDIUM,HIGH)baseline: Path to baseline report for comparison
Semgrep Configuration
Semgrep is a fast, open-source static analysis tool for security scanning and code quality enforcement across 30+ languages.
File: .semgrep.yaml or .semgrep.yml
rules:
- id: custom-security-rule
pattern: eval(...)
message: 'Avoid using eval() - potential code injection'
languages: [python]
severity: ERROR
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
config | string | Rule config: auto, p/python, p/javascript, or path |
exclude | list | Patterns to exclude from scanning |
include | list | Patterns to include in scanning |
severity | string | Minimum severity: INFO, WARNING, ERROR |
timeout_threshold | int | Per-file timeout in seconds |
jobs | int | Number of parallel jobs |
Example Usage:
# Use Python security rules
lintro check --tools semgrep --tool-options "semgrep:config=p/python"
# Filter by severity
lintro check --tools semgrep --tool-options "semgrep:severity=ERROR"
# Exclude test files
lintro check --tools semgrep --tool-options "semgrep:exclude=tests/*|vendor/*"
Cppcheck Configuration
Cppcheck is a static analysis tool for C/C++ that detects undefined behavior, memory-safety defects, and other bugs. It is check-only (no auto-fix) and runs standalone on files without any build/project context.
Installation:
- macOS:
brew install cppcheck - Debian/Ubuntu:
apt-get install cppcheck
Lintro runs Cppcheck over the files it discovers and drives it entirely through
command-line options, so configuration goes through --tool-options. Cppcheck’s own
project modes (--project=compile_commands.json, GUI project files) and suppression
files are not wired into the Lintro integration.
Only source files (.c, .cpp, .cc, .cxx, .c++) are passed to Cppcheck. Headers
handed to Cppcheck directly are analyzed as standalone translation units and misfire
without the source that defines their macros and uses their declarations, so — as
upstream’s manual recommends — Lintro lets Cppcheck reach headers through the sources
that #include them. unusedFunction is likewise unsupported and rejected with a
ValueError, as is all, which implies it: they need whole-program visibility, while
Lintro invokes Cppcheck on the file list discovered for the run — a path argument, a
--diff scope, or the whole tree — so functions would be reported unused merely because
their callers were outside that list.
Lintro requires Cppcheck 2.13.0 or newer. Cppcheck ships no portable single binary,
so the Docker image installs Debian’s package (currently 2.17.1 on trixie) and
install-tools.sh uses apt or Homebrew. Distribution packages older than 2.13.0 are
rejected by the version check; install from Homebrew or upstream in that case.
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
enable | string | list | Check categories forwarded to --enable=. Commas separate --tool-options entries, so several values are written pipe-separated (cppcheck:enable=warning|style). Default warning,style,performance,portability (error checks always run). unusedFunction and all are rejected. |
inconclusive | bool | Report findings cppcheck cannot fully confirm. |
std | string | Language standard (e.g. c11, c++17). |
inline_suppr | bool | Honor inline // cppcheck-suppress comments. |
suppress | string | list | Suppression specifications forwarded to --suppress=, one flag per value. Several values are written pipe-separated (cppcheck:suppress=missingInclude|unusedStructMember). |
Example Usage:
# Run with the default check set
lintro check src/ --tools cppcheck
# Enable only warnings, and assume C11. Options are comma-separated; the pipe
# is the list separator *within* one value.
lintro check src/ --tools cppcheck \
--tool-options "cppcheck:enable=warning,cppcheck:std=c11"
# Enable several categories (pipe-delimited list inside one option)
lintro check src/ --tools cppcheck --tool-options "cppcheck:enable=warning|style"
# Include inconclusive findings and suppress missing-include noise
lintro check src/ --tools cppcheck \
--tool-options "cppcheck:inconclusive=true,cppcheck:suppress=missingInclude"
Cppcheck’s structured output is parsed from its native XML report (schema version 2).
SARIF output is available in recent versions but is lossy for cppcheck (it collapses
style/performance/portability into a single warning level), so the native XML
parser is used. See Cppcheck Analysis.
Gitleaks Configuration
File: .gitleaks.toml
# Custom rule example
[[rules]]
id = "custom-api-key"
description = "Custom API Key Pattern"
regex = '''custom_api_key_[a-zA-Z0-9]{32}'''
tags = ["key", "custom"]
# Allowlist to ignore false positives
[allowlist]
paths = [
'''test_samples/''',
'''\.git/''',
]
regexes = [
'''EXAMPLE''',
'''test_''',
]
Available Options:
| Option | Type | Description |
|---|---|---|
no_git | boolean | Scan without git history (files only) |
config | string | Path to custom gitleaks config file |
baseline_path | string | Path to baseline file (ignore known secrets) |
redact | boolean | Redact secrets in output (default: true) |
max_target_megabytes | integer | Skip files larger than this size in MB |
Usage Examples:
# Basic scan with default config
lintro check --tools gitleaks
# Scan with git history (not just files)
lintro check --tools gitleaks --tool-options gitleaks:no_git=False
# Use custom config file
lintro check --tools gitleaks --tool-options gitleaks:config=.gitleaks.toml
# Use baseline to ignore known secrets
lintro check --tools gitleaks --tool-options gitleaks:baseline_path=gitleaks-baseline.json
# Limit file size to scan
lintro check --tools gitleaks --tool-options gitleaks:max_target_megabytes=10
TruffleHog Configuration
TruffleHog is a secrets scanner with 800+ provider-specific detectors and optional live
credential verification. Lintro runs it in filesystem mode. Verification is disabled
by default (--no-verification) so default scans make no outbound network calls;
verification can be re-enabled per run, in which case TruffleHog may contact third-party
providers to test candidate credentials (accept that trade-off before enabling it).
TruffleHog is configured via CLI options (there is no default config file).
Install: brew install trufflehog or
GitHub Releases
Available Options:
| Option | Type | Description |
|---|---|---|
no_verification | boolean | Disable live credential verification (default: true) |
results | string | Filter result type — single value (see note below) |
config | string | Path to a custom detector configuration file |
exclude_paths | string | Path to a file of newline-separated exclude regexes |
concurrency | integer | Number of concurrent workers |
Note:
--tool-optionsuses commas to separate options, so a comma-separatedresultsvalue (e.g.verified,unverified) cannot be passed on the CLI. Use a single value such asresults=unverified, or leave it unset to report every result type.
Usage Examples:
# Basic scan (verification disabled by default)
lintro check --tools trufflehog
# Raise worker concurrency
lintro check --tools trufflehog --tool-options trufflehog:concurrency=8
# Only report unverified results (single value; commas are not CLI-safe here)
lintro check --tools trufflehog --tool-options trufflehog:results=unverified
# Explicitly enable live verification (makes network calls — off by default)
lintro check --tools trufflehog --tool-options trufflehog:no_verification=False
OSV-Scanner Configuration
OSV-Scanner is Google’s vulnerability scanner using the Open-Source Vulnerabilities (OSV) database. It scans lockfiles for known vulnerabilities across multiple ecosystems including PyPI, npm, Go, Rust, Ruby, PHP, .NET, and Java.
Install: go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latest or
download from GitHub Releases.
File: .osv-scanner.toml
# Ignore specific vulnerabilities (ignoreUntil is required for lintro classification)
[[IgnoredVulns]]
id = "GHSA-xxxx-xxxx-xxxx"
ignoreUntil = 2026-12-31
reason = "Not applicable to this project"
# Override package scanning behavior
[[PackageOverrides]]
name = "example-package"
ecosystem = "PyPI"
ignore = true
reason = "False positive"
Available Options:
| Option | Type | Description |
|---|---|---|
timeout | integer | Scan timeout in seconds (default: 120) |
check_suppressions | boolean | Run probe scan to detect stale suppressions (default: true) |
When check_suppressions is enabled, lintro runs a second osv-scanner scan without
suppressions to classify each .osv-scanner.toml entry as Active (vulnerability
still present), Stale (vulnerability resolved upstream — safe to remove), or
Expired (past the ignoreUntil date). Results appear in the summary table Notes
column and in JSON output under metadata.suppressions.
Usage Examples:
# Scan for vulnerabilities in lockfiles
lintro check --tools osv_scanner
# With custom timeout for slow networks
lintro check --tools osv_scanner --tool-options "osv_scanner:timeout=300"
# Skip suppression staleness check
lintro check --tools osv_scanner --tool-options "osv_scanner:check_suppressions=false"
# Ignore vendored or nested checkouts (equivalent to a .lintro-ignore entry)
lintro check --tools osv_scanner --exclude ".claude"
Excluding lockfiles: osv-scanner performs its own recursive lockfile discovery, so
lintro applies the resolved exclusion set — .lintro-ignore entries plus --exclude
patterns — to the lockfile paths it reports. Both mechanisms use the same gitignore
semantics as file discovery and yield identical results. Directory patterns (.claude,
vendor/) drop every finding from that tree — useful when nested checkouts multiply the
same finding — and file patterns (bun.lock, *.lock) suppress individual lockfiles.
pip-audit Configuration
pip-audit is the Python Packaging Authority (PyPA) scanner for Python dependencies with
known vulnerabilities. It queries the PyPI Advisory Database and OSV, complementing
bandit (which scans source code) by scanning the dependency surface. It audits
requirements*.txt files (via -r) and Python projects declared in pyproject.toml /
setup.py.
Install: pip install pip-audit, uv add pip-audit, or brew install pip-audit.
File: none. pip-audit has no native config file; suppressions are passed on the command line by upstream and are not exposed via lintro.
Available Options:
| Option | Type | Description |
|---|---|---|
timeout | integer | Scan timeout in seconds (default: 120) |
Notes:
- pip-audit’s JSON output carries no severity field, so lintro reports severity as
UNKNOWN. - Advisory IDs (PYSEC/GHSA/CVE) link to the corresponding osv.dev page.
- Requirements discovery is recursive: nested files such as
requirements/base.txtorservices/api/requirements.txtare picked up, while vendored/generated trees (node_modules,.venv,venv,vendor,.git,__pycache__) are skipped. To audit a file outside this scope, pass it explicitly on the command line.
Usage Examples:
# Scan requirements and project manifests for vulnerable dependencies
lintro check --tools pip_audit
# With a longer timeout for slow networks
lintro check --tools pip_audit --tool-options "pip_audit:timeout=300"
pydoclint Configuration
File: pyproject.toml
[tool.pydoclint]
style = "google"
arg-type-hints-in-docstring = false
arg-type-hints-in-signature = true
check-return-types = false
check-arg-order = true
skip-checking-short-docstrings = true
Available Options:
style:google,numpy,sphinxarg-type-hints-in-docstring: Require types in docstring (default: true)arg-type-hints-in-signature: Require type annotations (default: true)check-return-types: Validate return types match (default: true)check-arg-order: Verify argument order matches signatureskip-checking-short-docstrings: Skip single-line docstrings
import-linter Configuration {#import-linter}
Tool: import-linter (binary
lint-imports) — checks architectural import contracts for a Python package.
Install: uv pip install 'lintro[full]' or uv pip install import-linter
File: pyproject.toml (also reads .importlinter and setup.cfg)
[tool.importlinter]
root_package = "mypkg"
[[tool.importlinter.contracts]]
name = "Layered architecture"
type = "layers"
layers = ["mypkg.api", "mypkg.services", "mypkg.storage"]
[[tool.importlinter.contracts]]
name = "CLI must not be imported by the core"
type = "forbidden"
source_modules = ["mypkg.core"]
forbidden_modules = ["mypkg.cli"]
Behaviour in Lintro:
- Project-scoped: the tool runs once per invocation against the whole import graph, regardless of how many files were passed.
- Configuration is discovered by walking upward from the given paths; the tool then runs from the config file’s directory so the root package is importable.
- A project with no import-linter configuration reports a clean result rather than an error, so the tool is safe to leave enabled.
- Check-only.
lintro formatnever runs import-linter.
Available --tool-options:
timeout: Seconds to allow the graph build and contract check (default: 60)
Usage:
lintro check .
lintro check . --tools import-linter
Contract syntax and every contract type are documented upstream at https://import-linter.readthedocs.io/en/stable/contract_types/.
pylint Configuration {#pylint}
Tool: pylint — Python static analyser.
Lintro wires it in mainly for duplicate-code (R0801), the copy-paste detector no
other bundled tool provides.
Install: uv pip install 'lintro[full]' or uv pip install pylint
File: pylint reads the first of these that declares pylint configuration, in this
precedence order: pylintrc, pylintrc.toml, .pylintrc, .pylintrc.toml,
pyproject.toml, setup.cfg, tox.ini. Lintro discovers the same file and passes it
as --rcfile.
[tool.pylint.main]
disable = ["all"]
enable = ["duplicate-code"]
jobs = 0
[tool.pylint.similarities]
min-similarity-lines = 12
ignore-comments = true
ignore-docstrings = true
ignore-imports = true
Behaviour in Lintro:
- Project-scoped: every discovered file is passed to a single
pylintinvocation. Cross-module checkers only see clones that appear in one run, so per-file execution would silently miss everyR0801. - Configuration is discovered by walking upward from the given paths and is passed as
--rcfile. With no pylint configuration anywhere above the paths, pylint’s built-in defaults apply — which overlap heavily with ruff, so configure it explicitly. - The
R0801message body (the file list plus the duplicated source block) is preserved verbatim; it is the only description of what is duplicated. - pylint’s message category becomes the Lintro severity:
fatal/errorare ERROR,warningandrefactor(which includesduplicate-code) are WARNING, andconvention/infoare INFO. - Check-only.
lintro formatnever runs pylint.
Available --tool-options:
disable: Message or category to disable, forwarded to--disable=. Commas separate--tool-optionsentries, so several values are written pipe-separated (pylint:disable=C0114|R0801)enable: Message or category to enable, forwarded to--enable=. Several values are written pipe-separated (pylint:enable=duplicate-code|C0114)timeout: Seconds to allow the run (default: 900; a whole-repo pylint run is slow)
Usage:
lintro check .
lintro check . --tools pylint
lintro check . --tools pylint --tool-options "pylint:disable=all,pylint:enable=duplicate-code"
# Several message ids in one option: pipe-separated, because commas split entries
lintro check . --tools pylint --tool-options "pylint:enable=duplicate-code|C0114"
Every message id and symbol is documented upstream at https://pylint.readthedocs.io/en/stable/user_guide/messages/messages_overview.html.
Frontend Tools
Prettier Configuration
Prettier handles formatting for CSS, HTML, JSON, YAML, Markdown, and GraphQL files.
Note: JavaScript and TypeScript files are handled by oxfmt for better performance (30x faster). See Oxfmt Configuration for JS/TS formatting options.
File: .prettierrc
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 80,
"endOfLine": "lf"
}
File: prettier.config.js
module.exports = {
tabWidth: 2,
semi: true,
singleQuote: true,
trailingComma: 'es5',
bracketSpacing: true,
arrowParens: 'avoid',
printWidth: 80,
// Override for specific file types
overrides: [
{
files: '*.json',
options: {
tabWidth: 4,
},
},
{
files: '*.md',
options: {
printWidth: 120,
proseWrap: 'always',
},
},
],
};
File: package.json
{
"prettier": {
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
}
Ignore Files: .prettierignore
node_modules/
dist/
build/
coverage/
*.min.js
*.min.css
TypeScript Tools
TypeScript Compiler (tsc) Configuration
The TypeScript Compiler provides static type checking for TypeScript projects. Lintro
wraps tsc --noEmit to check types without generating output files.
Installation:
# Homebrew (macOS/Linux)
brew install typescript
# npm
npm install -D typescript
# bun
bun add -D typescript
A project devDependency is preferred: a type checker must match the project’s own
TypeScript version, because a different compiler reports different diagnostics. A global
or Homebrew tsc is used when the project has no local install. See
Node.js Tool Resolution.
File: tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
project | string | Path to tsconfig.json (—project) |
strict | boolean | Enable all strict type checking options |
skip_lib_check | boolean | Skip type checking of declaration files (default: true) |
timeout | integer | Execution timeout in seconds (default: 60) |
Usage Examples:
# Basic type check (uses tsconfig.json if present)
lintro check src/ --tools tsc
# Enable strict mode
lintro check src/ --tools tsc --tool-options "tsc:strict=True"
# Use specific config file
lintro check src/ --tools tsc --tool-options "tsc:project=tsconfig.build.json"
# Disable library check for faster execution
lintro check src/ --tools tsc --tool-options "tsc:skip_lib_check=True"
# Combine with other JS/TS tools
lintro check src/ --tools tsc,oxlint,oxfmt
Oxlint Configuration
Oxlint is a fast JavaScript/TypeScript linter (50-100x faster than ESLint) with 661+ built-in rules from ESLint, TypeScript, React, JSX-a11y, Unicorn, and more.
Native Config Detection:
Lintro detects these oxlint config files:
.oxlintrc.jsonoxlint.json
When a native config exists, Lintro uses it automatically and skips generating defaults.
Installation:
# bun (recommended)
bun add -D oxlint
# npm
npm install -D oxlint
Lintro prefers the project’s own node_modules/.bin/oxlint, then a binary on PATH,
then a version-pinned bunx/npx fetch, so a global or Homebrew install works too. See
Node.js Tool Resolution.
File: .oxlintrc.json
{
"rules": {
"no-debugger": "error",
"no-console": "warn",
"eqeqeq": "error"
},
"plugins": ["react", "unicorn"],
"ignorePatterns": ["dist/**", "node_modules/**"]
}
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
config | string | Path to config file (—config) |
tsconfig | string | Path to tsconfig.json (—tsconfig) |
allow | list[str] | Rules to allow (turn off) |
deny | list[str] | Rules to deny (report as errors) |
warn | list[str] | Rules to warn on (report as warnings) |
quiet | boolean | Suppress warnings, only report errors (—quiet) |
timeout | integer | Execution timeout in seconds (default: 30) |
Usage Examples:
# Basic check
lintro check --tools oxlint
# Auto-fix issues
lintro format --tools oxlint
# Suppress warnings (errors only)
lintro check --tools oxlint --tool-options "oxlint:quiet=True"
# Deny specific rules (report as errors)
lintro check --tools oxlint --tool-options "oxlint:deny=no-debugger|no-console"
# Allow specific rules (ignore them)
lintro check --tools oxlint --tool-options "oxlint:allow=no-unused-vars"
# Use custom config file
lintro check --tools oxlint --tool-options "oxlint:config=.oxlintrc.custom.json"
# Specify tsconfig for TypeScript projects
lintro check --tools oxlint --tool-options "oxlint:tsconfig=tsconfig.app.json"
Stylelint Configuration
Stylelint is a mighty, configurable linter and fixer for CSS, SCSS, Sass, and Less
stylesheets, with 100+ built-in rules and --fix support.
Stylelint requires a configuration to run. When no config is resolvable, Lintro skips stylelint as a non-error (rather than surfacing stylelint’s hard
ConfigurationError).
Native Config Detection:
Stylelint resolves configuration per file (walking upward). Lintro supports:
.stylelintrc,.stylelintrc.json,.stylelintrc.yaml,.stylelintrc.yml.stylelintrc.js,.stylelintrc.cjs,.stylelintrc.mjsstylelint.config.js,stylelint.config.cjs,stylelint.config.mjs- a
stylelintkey inpackage.json
.stylelintignore is honored by the underlying tool.
Installation:
# bun (recommended, with a shareable config)
bun add -D stylelint stylelint-config-standard
# npm
npm install -D stylelint stylelint-config-standard
Lintro prefers the project’s own node_modules/.bin/stylelint, then a binary on PATH,
then a version-pinned bunx/npx fetch, so a global install works too. See
Node.js Tool Resolution.
File: .stylelintrc.json
{
"extends": ["stylelint-config-standard"],
"rules": {
"color-hex-length": "short",
"block-no-empty": true,
"declaration-block-no-duplicate-properties": true
}
}
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
config | string | Path to a stylelint config file (--config) |
verbose_fix_output | boolean | Include raw stylelint output in fix() |
timeout | integer | Execution timeout in seconds (default: 30) |
Usage Examples:
# Basic check
lintro check styles/ --tools stylelint
# Auto-fix issues
lintro format styles/ --tools stylelint
# Use a specific config file
lintro check styles/ --tools stylelint --tool-options "stylelint:config=.stylelintrc.json"
# Increase timeout for large stylesheets
lintro check styles/ --tools stylelint --tool-options "stylelint:timeout=60"
Oxfmt Configuration
Oxfmt is a fast JavaScript/TypeScript formatter (30x faster than Prettier) that provides Prettier-compatible formatting with minimal configuration.
Native Config Detection:
Lintro detects these oxfmt config files:
.oxfmtrc.json.oxfmtrc.jsonc(supports comments)
When a native config exists, Lintro uses it automatically and skips generating defaults.
Installation:
# bun (recommended)
bun add -D oxfmt
# npm
npm install -D oxfmt
Lintro prefers the project’s own node_modules/.bin/oxfmt, then a binary on PATH,
then a version-pinned bunx/npx fetch, so a global or Homebrew install works too. See
Node.js Tool Resolution.
File: .oxfmtrc.json or .oxfmtrc.jsonc
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
}
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
config | string | Path to config file (—config) |
ignore_path | string | Path to ignore file (—ignore-path) |
timeout | integer | Execution timeout in seconds (default: 30) |
Note: Formatting options (
printWidth,tabWidth,useTabs,semi,singleQuote) are only supported via config file (.oxfmtrc.json), not CLI flags.
Usage Examples:
# Basic check
lintro check --tools oxfmt
# Format files
lintro format --tools oxfmt
# Use custom config file
lintro format --tools oxfmt --tool-options "oxfmt:config=.oxfmtrc.custom.json"
# Use custom ignore file
lintro format --tools oxfmt --tool-options "oxfmt:ignore_path=.oxfmtignore"
# Increase timeout
lintro format --tools oxfmt --tool-options "oxfmt:timeout=60"
API Description Tools
Spectral Configuration
Spectral is a linter for OpenAPI (2.0/3.0/3.1), AsyncAPI, and JSON Schema documents. It
is check-only (no autofixer) and requires a ruleset. Lintro discovers a supported
ruleset upward from the target (or uses the ruleset option) and skips as a non-error
when none is found. With a ruleset, Spectral checks every matching *.yaml, *.yml,
and *.json file. Lintro always supplies Spectral’s internal --ignore-unknown-format
flag so non-API documents do not produce format warnings; the flag is not a
--tool-options setting.
Spectral is intentionally absent from the language map: enabling it for every YAML or
JSON project would be too broad. A native .spectral.* file at a scan root selects it
on a no-config first run; after lintro init, add spectral to
execution.enabled_tools. Users can also name it with --tools spectral; once
selected, Spectral still skips unless discovery or the ruleset option finds a ruleset.
Native Config Detection:
Lintro detects (and Spectral requires) one of these ruleset files:
.spectral.yaml.spectral.yml.spectral.json.spectral.js
Installation:
# bun (recommended)
bun add -D @stoplight/spectral-cli
# npm
npm install -D @stoplight/spectral-cli
Lintro prefers the project’s own node_modules/.bin/spectral, then a binary on PATH,
then a version-pinned bunx/npx fetch. See
Node.js Tool Resolution.
File: .spectral.yaml
extends: ['spectral:oas'] # or ["spectral:asyncapi"]
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
ruleset | string | Explicit path to a ruleset (overrides discovery) |
timeout | integer | Execution timeout in seconds (default: 30) |
Usage Examples:
# Lint an OpenAPI document (rulesets are discovered upward from the target)
lintro check --tools spectral openapi.yaml
# Use an explicit ruleset
lintro check --tools spectral --tool-options "spectral:ruleset=.spectral.custom.yaml"
Web Framework Tools
Astro Check Configuration
Astro Check is Astro’s built-in type checking command that provides TypeScript
diagnostics for .astro files including frontmatter scripts, component props, and
template expressions.
Installation:
# bun (recommended)
bun add -D @astrojs/check
# npm
npm install -D @astrojs/check
astro check needs @astrojs/check, and Lintro runs Astro with CI=1 so its
interactive “install @astrojs/check?” prompt cannot complete. Add that package as a
devDependency; astro is already a production dependency in an Astro project, and
installing it with -D would move it out of dependencies. A project-local install of
@astrojs/check is strongly preferred here; see
Node.js Tool Resolution.
Native Config: astro.config.mjs, astro.config.ts, or astro.config.js
Astro check uses your project’s astro.config and tsconfig.json for configuration. No
additional configuration is needed for Lintro.
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
root | string | Root directory for the Astro project |
timeout | integer | Execution timeout in seconds (default: 120) |
Usage Examples:
# Check Astro project
lintro check src/ --tools astro-check
# Check with specific root directory
lintro check . --tools astro-check --tool-options "astro-check:root=./packages/web"
# Auto-install dependencies before checking
lintro check src/ --tools astro-check --auto-install
Svelte Check Configuration
Svelte Check is the official type checker and linter for Svelte components. It provides
TypeScript type checking, unused CSS detection, and accessibility hints for .svelte
files.
Installation:
# bun (recommended)
bun add -D svelte-check
# npm
npm install -D svelte-check
Native Config: svelte.config.js or svelte.config.ts
Svelte Check uses your project’s svelte.config and tsconfig.json for configuration.
No additional configuration is needed for Lintro.
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
threshold | string | Minimum severity to report: error or warning (default: warning) |
tsconfig | string | Path to tsconfig.json file |
timeout | integer | Execution timeout in seconds (default: 120) |
Usage Examples:
# Check Svelte project
lintro check src/ --tools svelte-check
# Check with warning threshold
lintro check src/ --tools svelte-check --tool-options "svelte-check:threshold=warning"
# Check with specific tsconfig
lintro check . --tools svelte-check --tool-options "svelte-check:tsconfig=./tsconfig.app.json"
# Auto-install dependencies before checking
lintro check src/ --tools svelte-check --auto-install
Vue-tsc Configuration
Vue-tsc is the TypeScript type checker for Vue Single File Components (SFCs). It extends
tsc with Vue-specific type checking capabilities for .vue files.
Installation:
# bun (recommended)
bun add -D vue-tsc
# npm
npm install -D vue-tsc
Native Config: tsconfig.json or tsconfig.app.json
Vue-tsc uses your project’s tsconfig.json for configuration. No additional
configuration is needed for Lintro.
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
project | string | Path to tsconfig.json file |
strict | boolean | Enable strict type checking mode |
skip_lib_check | boolean | Skip checking declaration files (default: true) |
use_project_files | boolean | Use tsconfig’s include patterns (default: false) |
timeout | integer | Execution timeout in seconds (default: 120) |
Usage Examples:
# Check Vue project
lintro check src/ --tools vue-tsc
# Check with specific tsconfig
lintro check . --tools vue-tsc --tool-options "vue-tsc:project=tsconfig.app.json"
# Enable strict mode
lintro check src/ --tools vue-tsc --tool-options "vue-tsc:strict=true"
# Auto-install dependencies before checking
lintro check src/ --tools vue-tsc --auto-install
html-validate Configuration
html-validate is an offline HTML validator that checks documents for standards
compliance, best practices, and accessibility (WCAG) issues. It is check-only — the tool
ships no autofixer. By default it inspects *.html, *.htm, *.vue, and *.svelte
files.
Installation:
# bun (recommended)
bun add -D html-validate
# npm
npm install -D html-validate
A project-local devDependency is the supported configuration. It is the first and
most reliable branch of Lintro’s executable resolution, it is lockfile-pinned, and it
needs no registry access at check time. A global install (-g) does not populate
node_modules/.bin, so it lands on a later, weaker branch.
html-validate uses the shared Node.js chain — as of a release after v0.115.0 (#1811) so does every other Node.js tool; see Node.js Tool Resolution.
Executable resolution order:
node_modules/.bin/html-validate, searched upward from the directory being checked until the nearestpackage.jsonor.git.- the
PATH-resolved absolute path ofhtml-validate(e.g. a global install). bunx html-validate@<pinned>— registry fallback, only ifbunxis available.npx --yes --package html-validate@<pinned> html-validate— registry fallback, only ifnpxis available.- bare
html-validate(fails if nothing is installed).
<pinned> is the version Lintro pins in its manifest; @latest is never resolved at
runtime. Branches 3 and 4 emit a one-time warning because they require network access to
the npm registry, and a failure on either path is reported with install guidance rather
than html-validate’s raw error.
Node runtime requirement: the pinned html-validate (currently 11.11.0) declares
engines: { "node": "^22.22.0 || >= 24.8.0" }. Any consumer that installs it, or that
reaches the bunx/npx fallback, needs a Node runtime satisfying that range — Node 20,
21, and 22.0–22.21 are not supported. Size your CI matrix accordingly.
Native Config: .htmlvalidate.json, .htmlvalidate.js, .htmlvalidate.cjs, or
.htmlvalidate.mjs
html-validate reads its rule configuration from the project’s native config file when present. No additional configuration is required for Lintro.
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
timeout | integer | Execution timeout in seconds (default: 120) |
Usage Examples:
# Check HTML files
lintro check src/ --tools html-validate
# Check the whole project
lintro check . --tools html-validate
# Auto-install dependencies before checking
lintro check src/ --tools html-validate --auto-install
SQL Tools
SQLFluff Configuration
File: .sqlfluff
[sqlfluff]
dialect = ansi
templater = jinja
exclude_rules = L016,L031
[sqlfluff:indentation]
indent_unit = space
tab_space_size = 4
[sqlfluff:layout:type:comma]
line_position = trailing
[sqlfluff:rules:capitalisation.keywords]
capitalisation_policy = upper
[sqlfluff:rules:capitalisation.identifiers]
extended_capitalisation_policy = lower
File: pyproject.toml
[tool.sqlfluff.core]
dialect = "ansi"
templater = "jinja"
exclude_rules = ["L016", "L031"]
[tool.sqlfluff.indentation]
indent_unit = "space"
tab_space_size = 4
[tool.sqlfluff.rules.capitalisation.keywords]
capitalisation_policy = "upper"
[tool.sqlfluff.rules.capitalisation.identifiers]
extended_capitalisation_policy = "lower"
Available Options:
| Option | Type | Description |
|---|---|---|
dialect | string | SQL dialect (ansi, bigquery, postgres, mysql, etc.) |
exclude_rules | list | List of rules to exclude from checking |
rules | list | List of specific rules to include |
templater | string | Templater to use (raw, jinja, python, placeholder) |
Supported Dialects:
ansi- ANSI SQL standardbigquery- Google BigQueryclickhouse- ClickHousedatabricks- Databricks SQLdb2- IBM Db2exasol- Exasolhive- Apache Hivemysql- MySQLoracle- Oracle Databasepostgres- PostgreSQLredshift- Amazon Redshiftsnowflake- Snowflakesoql- Salesforce SOQLsparksql- Apache Spark SQLsqlite- SQLiteteradata- Teradatatsql- T-SQL (Microsoft SQL Server)
Usage Examples:
# Basic SQL check
lintro check --tools sqlfluff
# Format SQL files
lintro format --tools sqlfluff
# Check with specific dialect
lintro check --tools sqlfluff --tool-options sqlfluff:dialect=postgres
# Exclude specific rules
lintro check --tools sqlfluff --tool-options sqlfluff:exclude_rules=L010,L014
# Use jinja templater
lintro check --tools sqlfluff --tool-options sqlfluff:templater=jinja
YAML Tools
Yamllint Configuration
File: .yamllint
extends: default
rules:
# Line length
line-length:
max: 120
level: warning
# Indentation
indentation:
spaces: 2
indent-sequences: true
check-multi-line-strings: false
# Comments
comments:
min-spaces-from-content: 2
# Document start
document-start:
present: false
# Truthy values
truthy:
allowed-values: ['true', 'false']
check-keys: true
File: pyproject.toml
[tool.yamllint]
extends = "default"
[tool.yamllint.rules.line-length]
max = 120
[tool.yamllint.rules.indentation]
spaces = 2
Markdown Tools
Markdownlint-cli2 Configuration {#markdownlint-cli2-configuration}
Markdownlint-cli2 supports configuration via JSON, JSONC, YAML, or TOML files. Lintro defers to markdownlint-cli2’s native configuration discovery, which searches upward from the file being checked.
File: .markdownlint.json
{
"default": true,
"MD013": {
"line_length": 120
},
"MD041": false
}
File: .markdownlint.yaml
default: true
MD013:
line_length: 120
MD041: false
File: .markdownlint-cli2.jsonc
{
"config": {
"default": true,
"MD013": { "line_length": 120 },
},
}
Available Options:
- Configuration files are discovered automatically by markdownlint-cli2
- Rules can be enabled/disabled via configuration files
- Lintro respects markdownlint-cli2’s native configuration discovery
- Future versions may expose additional options via
[tool.lintro.markdownlint-cli2]inpyproject.toml
Prose / Documentation Tools
Vale Configuration {#vale-configuration}
Vale is a syntax-aware prose linter. Lintro defers to Vale’s native
configuration discovery, which walks upward from each linted file to find a .vale.ini.
Vale requires a configuration to run. When none is resolvable, Lintro skips vale as a non-error (rather than surfacing vale’s
E100runtime error), keeping mixed-language runs clean.
Native config detection: .vale.ini, _vale.ini, vale.ini.
File: .vale.ini
MinAlertLevel = suggestion
[*.md]
BasedOnStyles = Vale
Installation:
brew install vale
# or download from https://github.com/errata-ai/vale/releases
Available --tool-options:
| Option | Type | Description |
|---|---|---|
config | string | Path to a Vale config file (maps to --config) |
min_alert_level | string | Minimum alert level: suggestion, warning, or error |
timeout | int | Per-run timeout in seconds (default 30) |
Usage:
lintro check docs/ --tools vale
lintro check docs/ --tools vale --tool-options vale:min_alert_level=warning
lintro check docs/ --tools vale --tool-options vale:config=.vale.ini
Typos Configuration
typos is a fast, low-false-positive spell checker for source code and documentation. It checks all text files and can auto-correct misspellings.
When typos runs. typos is language-agnostic, so it has no entry in the manifest’s
language_map. That makes selection depend on which path a project takes:
- No Lintro config (first run). Tool selection comes from language detection, which does not pull typos in on its own. It joins through the “unmapped tool with a native config” path — i.e. as soon as a
typos.toml,.typos.tomlor_typos.tomlexists at a scan root. crate-ci/typos also reads[tool.typos]inpyproject.tomland[package.metadata.typos]/[workspace.metadata.typos]inCargo.toml; those files are not in Lintro’snative_configs, so a typical Python or Rust tree does not auto-select the plugin. The binary still honors those tables once typos is selected. An empty[tool.lintro]table is not a config, so this path still applies.- With a resolved Lintro config, or
--tools all. Language scoping is bypassed. A resolved config is.lintro-config.yaml(or.yml), a non-empty[tool.lintro]table inpyproject.toml, or an in-memorytools:section. Typos then runs when the binary is onPATHand it is not filtered out byexecution.enabled_toolsortools.typos.enabled: false.lintro init’s recommended profile writes a language-basedenabled_toolsallowlist that does not include typos, so those projects do not start spell-checking on upgrade. An existing unscopedlintro check(enabled_tools: []or omitted) will.
lintro check --tools typosselects it explicitly in either case.Two things are worth knowing before that first run:
Turning it off. Do not create a Lintro config solely to disable typos on a no-config first run: that file is a resolved config, so language scoping is skipped and an empty
enabled_toolsallowlist runs the full unscoped registry, including typos. On a first run, omittypos.toml/.typos.toml/_typos.tomlinstead. On a project that already has a Lintro config, disable the tool:tools: typos: enabled: falseOr leave it out of
execution.enabled_toolsif you use an allowlist (lintro init --profile recommendedalready does).
--toolsis an allowlist rather than an opt-out:lintro check --tools ruffruns ruff only, dropping every other tool as well, so reach for it to narrow a single run, not to exclude one tool.Project vocabulary. The first run usually reports a handful of deliberate spellings — product names, abbreviations, deliberate misspellings inside test fixtures. Add them to
.typos.tomlrather than suppressing the tool: real words go in[default.extend-words], and anything that is only correct in one context is better handled by a context-anchored[default] extend-ignore-repattern so the same word is still caught elsewhere.lintro format --tools typosapplies the corrections typos is confident about, which clears most of the rest.
File: typos.toml, .typos.toml, or _typos.toml
# Accept project-specific vocabulary that the default dictionary flags
[default.extend-words]
unparseable = "unparseable"
# Skip files/directories that deliberately contain non-English or fixture text
[files]
extend-exclude = ["tests/fixtures/"]
Available Options:
| Option | Type | Description |
|---|---|---|
timeout | integer | Per-invocation timeout in seconds (default: 30) |
typos’ word list and file scope are configured through its native typos.toml file
rather than --tool-options. Lintro runs typos with --force-exclude so that
[files] extend-exclude still applies to the explicit file list Lintro passes. Lintro
also pre-filters the file list with a NUL-byte sniff over each file’s first 8 KiB, which
keeps lintro format away from the common binary formats (images, archives, compiled
objects). That heuristic is not exhaustive — a binary format with no NUL byte in its
header can still reach --write-changes — so add the extensions you care about to
[files] extend-exclude when a project stores unusual binary assets.
Usage Examples:
# Check spelling across the project
lintro check --tools typos
# Auto-correct misspellings in place
lintro format --tools typos
Rust Tools
Clippy Configuration
Clippy is Rust’s official linter and is configured through Cargo.toml or a separate clippy.toml file. Lintro automatically discovers and runs clippy on Rust projects by finding Cargo.toml files.
File: Cargo.toml
[package]
name = "my-rust-project"
version = "0.1.0"
[lints.clippy]
# Enable all lints
pedantic = "warn"
# Or be more restrictive
# pedantic = { level = "warn", priority = -1 }
# Disable specific lints
too_many_arguments = "allow"
type_complexity = "allow"
# Configure lint levels
needless_return = "warn"
unused_variables = "error"
File: clippy.toml (alternative)
# Clippy-specific configuration
too-many-arguments-threshold = 10
type-complexity-threshold = 100
cognitive-complexity-threshold = 15
# Disable specific lints
disallowed-names = []
Available Options:
- pedantic: Enable all lints that are typically only enabled in CI
- nursery: Enable newer, more experimental lints
- restriction: Enable very strict lints that may be overly restrictive for some projects
- cargo: Enable lints that check Cargo.toml files
Lintro usage:
# Check Rust code with Clippy
lintro check --tools clippy
# Auto-fix Clippy issues where possible
lintro format --tools clippy
# Check specific Rust directories
lintro check src/ --tools clippy
golangci-lint Configuration
golangci-lint is the de-facto Go meta-linter, running 100+ sub-linters in parallel.
Lintro targets golangci-lint v2 and automatically discovers Go modules by finding
go.mod files; non-Go projects are skipped. It requires the Go toolchain to be
installed. Linter selection and rule tuning are configured through the project’s native
config file.
Installation:
brew install golangci-lint
# or see https://golangci-lint.run/welcome/install/
File: .golangci.yml (also .golangci.yaml, .golangci.toml, .golangci.json)
version: '2'
linters:
enable:
- errcheck
- staticcheck
- ineffassign
- govet
Available Options:
| Option | Type | Description |
|---|---|---|
timeout | integer | Execution timeout in seconds (120) |
Linter enable/disable and per-linter settings live in the native .golangci.* config
file rather than as lintro --tool-options.
Lintro usage:
# Check Go code with golangci-lint
lintro check --tools golangci_lint
# Auto-fix issues where the underlying linters support it
lintro format --tools golangci_lint
# Increase the timeout for a large module
lintro check --tools golangci_lint --tool-options golangci_lint:timeout=300
Cargo-deny Configuration
Cargo-deny checks Rust dependencies for license compliance, security advisories, banned
crates, and duplicate dependencies. It uses deny.toml for configuration and runs via
cargo deny check.
Installation:
# cargo install
cargo install cargo-deny
# Or via cargo-binstall for faster installation
cargo binstall cargo-deny
File: deny.toml
[advisories]
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"]
[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]
[bans]
multiple-versions = "warn"
wildcards = "allow"
[sources]
unknown-registry = "warn"
unknown-git = "warn"
Available Options via --tool-options:
| Option | Type | Description |
|---|---|---|
timeout | integer | Execution timeout in seconds (default: 60) |
Lintro usage:
# Check Rust project with cargo-deny
lintro check --tools cargo_deny
# Check specific Rust directory
lintro check my-crate/ --tools cargo_deny
# Set a longer timeout for large workspaces
lintro check . --tools cargo_deny --tool-options "cargo_deny:timeout=120"
Shell Tools
ShellCheck Configuration
ShellCheck is a static analysis tool for shell scripts. It identifies bugs, syntax issues, and suggests improvements for bash/sh/dash/ksh/zsh scripts. Unlike formatters, ShellCheck focuses on finding potential bugs and problematic patterns.
Installation:
# macOS
brew install shellcheck
# Debian/Ubuntu
apt-get install shellcheck
# Fedora
dnf install ShellCheck
File: .shellcheckrc
# Exclude specific codes
disable=SC2086,SC2046
# Set default shell dialect
shell=bash
# Set minimum severity level
severity=warning
Lintro options via --tool-options:
# Set minimum severity level (error, warning, info, style)
lintro check --tools shellcheck --tool-options "shellcheck:severity=warning"
# Force shell dialect (bash, sh, dash, ksh, zsh)
lintro check --tools shellcheck --tool-options "shellcheck:shell=bash"
# Exclude specific codes
lintro check --tools shellcheck --tool-options "shellcheck:exclude=SC2086|SC2046"
# Follow repo-local sourced files (resolves SC1091 for SCRIPT_DIR sourcing)
lintro check --tools shellcheck \
--tool-options "shellcheck:external_sources=True,shellcheck:source_paths=SCRIPTDIR"
Available Options:
| Option | Type | Description |
|---|---|---|
severity | str | Minimum severity: error, warning, info, style |
exclude | list[str] | List of codes to exclude (e.g., SC2086, SC2046) |
shell | str | Force shell dialect: bash, sh, dash, ksh, zsh |
external_sources | bool | Follow sourced files external to the script (-x). Default False |
source_paths | list[str] | Search paths for sourced files (--source-path=...); supports the SCRIPTDIR token. Setting this implies external_sources (-x is enabled automatically) |
Source-following (SC1091):
Scripts that source repo-local helpers with the runtime-safe pattern below emit SC1091
(“Not following …”) by default, because ShellCheck cannot statically resolve the
dynamic path:
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/common.sh"
Opt in to source-following (off by default, so existing projects are unaffected) by
pointing source_paths at the directories ShellCheck should search. ShellCheck’s
literal SCRIPTDIR token resolves relative to each script’s own directory, which
matches the pattern above without hard-coding repo paths. Because ShellCheck ignores
--source-path unless -x is active, setting source_paths automatically enables
external_sources, so you do not need to set both — configuring paths is enough. (You
may still set external_sources = true on its own to follow sources found on the
default search path.)
pyproject.toml:
[tool.lintro.shellcheck]
# source_paths implies external_sources; -x is added automatically.
source_paths = ["SCRIPTDIR", "scripts/lib"]
With this enabled, the SC1091 above is resolved without per-line
# shellcheck disable=SC1091 or # shellcheck source=... suppressions.
Common ShellCheck Codes:
| Code | Description |
|---|---|
| SC2086 | Double quote to prevent globbing and word splitting |
| SC2046 | Quote this to prevent word splitting |
| SC2002 | Useless use of cat |
| SC2006 | Use $(…) notation instead of backticks |
| SC2034 | Variable appears unused |
| SC2155 | Declare and assign separately |
Inline ignoring:
#!/bin/bash
# shellcheck disable=SC2086
echo $unquoted_variable
# Or use a directive that applies to the whole file at the top:
# shellcheck disable=SC2086,SC2046
Lintro usage:
# Check shell scripts with ShellCheck
lintro check --tools shellcheck
# Check with warning level (ignores info and style)
lintro check --tools shellcheck --tool-options "shellcheck:severity=warning"
# Check specific shell directories
lintro check scripts/ --tools shellcheck
Shfmt Configuration
Shfmt is a shell script formatter that supports POSIX, Bash, mksh, and bats shells. It formats shell scripts to ensure consistent style and can detect formatting issues in diff mode.
Installation:
# macOS
brew install shfmt
# Linux (via Go)
go install mvdan.cc/sh/v3/cmd/shfmt@latest
# Via npm/bun
bun add -g shfmt
File: .editorconfig (shfmt respects EditorConfig)
[*.sh]
indent_style = tab
indent_size = 4
shell_variant = bash
binary_next_line = true
switch_case_indent = true
space_redirects = false
Lintro options via --tool-options:
# Set indentation to 4 spaces (0 for tabs)
lintro check --tools shfmt --tool-options "shfmt:indent=4"
# Enable binary operators at start of line
lintro check --tools shfmt --tool-options "shfmt:binary_next_line=True"
# Indent switch cases
lintro check --tools shfmt --tool-options "shfmt:switch_case_indent=True"
# Add space after redirect operators
lintro check --tools shfmt --tool-options "shfmt:space_redirects=True"
# Set language dialect (bash, posix, mksh, bats)
lintro check --tools shfmt --tool-options "shfmt:language_dialect=bash"
# Enable code simplification
lintro check --tools shfmt --tool-options "shfmt:simplify=True"
Available Options:
| Option | Type | Description |
|---|---|---|
indent | int | Indentation size. 0 for tabs, >0 for spaces |
binary_next_line | bool | Binary ops like && and | may start a line |
switch_case_indent | bool | Indent switch cases |
space_redirects | bool | Redirect operators followed by space |
language_dialect | str | Shell dialect: bash, posix, mksh, bats |
simplify | bool | Simplify code where possible |
Lintro usage:
# Check shell scripts with shfmt
lintro check --tools shfmt
# Auto-format shell scripts
lintro format --tools shfmt
# Check specific shell directories
lintro check scripts/ --tools shfmt
Dotenv Tools
dotenv-linter Configuration
dotenv-linter is a fast, Rust-based linter and fixer
for .env files. It detects duplicate keys, lowercase keys, incorrect delimiters,
unordered keys, and stray whitespace, and can auto-fix most of them.
Installation:
# macOS
brew install dotenv-linter
# Cargo
cargo install dotenv-linter
# Binary releases
# https://github.com/dotenv-linter/dotenv-linter/releases
Native config: dotenv-linter has no config file; behavior is controlled entirely via
CLI flags (surfaced through --tool-options).
Lintro options via --tool-options:
# Recursively scan directories for .env files
lintro check --tools dotenv_linter --tool-options "dotenv_linter:recursive=True"
# Skip specific checks (maps to --ignore-checks)
lintro check --tools dotenv_linter \
--tool-options "dotenv_linter:skip_checks=LowercaseKey|UnorderedKey"
# Exclude paths from linting
lintro check --tools dotenv_linter --tool-options "dotenv_linter:exclude=vendor"
# Validate against a schema file
lintro check --tools dotenv_linter --tool-options "dotenv_linter:schema=env.schema.json"
# Auto-fix issues in place (no .env.bak backups are created)
lintro format --tools dotenv_linter
Available Options:
| Option | Type | Description |
|---|---|---|
recursive | bool | Recursively scan directories for .env files. Default False |
exclude | list[str] | File or directory paths to exclude |
skip_checks | list[str] | Check names to bypass (maps to --ignore-checks) |
schema | str | Path to a schema file to validate .env contents |
Note: real
.envfiles are frequently.gitignored, so they may not be discovered during a normal repository scan. Point Lintro at the file explicitly, or commit a template such as.env.examplefor the linter to check.
TOML Tools
Taplo Configuration
File: taplo.toml or .taplo.toml
# Taplo configuration
[formatting]
align_entries = false
align_comments = true
array_trailing_comma = true
array_auto_expand = true
array_auto_collapse = true
compact_arrays = true
compact_inline_tables = false
column_width = 80
indent_tables = false
indent_entries = false
indent_string = " "
trailing_newline = true
reorder_keys = false
allowed_blank_lines = 2
crlf = false
[[rule]]
# Apply to all TOML files
include = ["**/*.toml"]
keys = ["Cargo.toml"]
[rule.formatting]
reorder_keys = true
Available Options:
| Option | Type | Description |
|---|---|---|
schema | string | Path or URL to JSON schema for validation |
aligned_arrays | boolean | Align array entries vertically |
aligned_entries | boolean | Align table entries (key = value) |
array_trailing_comma | boolean | Add trailing comma in multi-line arrays |
indent_string | string | Indentation string (default: 2 spaces) |
reorder_keys | boolean | Reorder keys alphabetically |
Usage Examples:
# Basic TOML check
lintro check --tools taplo
# Format TOML files
lintro format --tools taplo
# Check with aligned entries
lintro check --tools taplo --tool-options taplo:aligned_entries=true
# Format with specific indent
lintro format --tools taplo --tool-options taplo:indent_string=" "
# Use custom schema for validation
lintro check --tools taplo --tool-options taplo:schema=pyproject.schema.json
Buf Configuration
File: buf.yaml (or buf.work.yaml for multi-module workspaces)
buf works with or without a buf.yaml. When no config is present, buf lints against its
STANDARD default rule set with the current directory as the module root. Add a
buf.yaml to select rule categories (MINIMAL, BASIC, STANDARD) or to opt into
COMMENTS/UNARY_RPC:
version: v2
lint:
use:
- STANDARD
except:
- PACKAGE_VERSION_SUFFIX
Available Options:
| Option | Type | Description |
|---|---|---|
config | string | Path to a buf.yaml (CLI --tool-options cannot pass inline YAML) |
disable_symlinks | boolean | Do not follow symlinks when reading sources |
Usage Examples:
# Lint Protocol Buffer files (buf lint)
lintro check --tools buf
# Format .proto files in place (buf format --write)
lintro format --tools buf
# Use an explicit buf.yaml (path relative to the project root)
lintro check --tools buf --tool-options buf:config=proto/buf.yaml
Note on module roots: lintro runs buf from your project root — the nearest ancestor of the selected
.protofiles holding a project marker (.git,pyproject.toml,package.json, …), not their common parent directory. A relativeconfigpath is therefore resolved from the project root. buf’s directory-based rules (e.g.PACKAGE_DIRECTORY_MATCH) resolve package paths relative to that same directory, so add abuf.yamlwith amodulespathwhen your protos live in a subdirectory and their packages are not laid out relative to the project root.
Infrastructure Tools
Hadolint Configuration
File: .hadolint.yaml
ignored:
- DL3008 # Pin versions in apt-get install
- DL3009 # Delete apt-get lists
- DL3015 # Avoid additional packages
trustedRegistries:
- docker.io
- gcr.io
allowedRegistries:
- docker.io
- gcr.io
- quay.io
Inline ignoring:
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y \
python3 \
python3-pip
Checkov Configuration
Checkov scans Terraform sources (*.tf, *.tf.json) for security and compliance
misconfigurations. Dockerfiles are intentionally left to hadolint to avoid
double-reporting the same file under two rule sets. Lintro never opts checkov into the
platform: --skip-download, --download-external-modules False and
--skip-results-upload are always passed and there is no --bc-api-key code path, so
no lintro option can make checkov fetch policies or modules, or upload a finding. A
BC_API_KEY in the environment is the one remaining route to an outbound request —
checkov reads that variable itself, so unset it when a run must be fully offline. It
buys no enrichment either way: --skip-download suppresses the platform metadata
download, so severity and guideline URLs stay unpopulated in every lintro-driven
run, keyed or not.
- Discovery: Terraform files (
*.tf,*.tf.json) - Native config:
.checkov.yaml/.checkov.yml - Install:
uv tool install checkov(isolated venv), orscripts/utils/install-tools.sh --local --tools checkov. Checkov requirespackaging<24while lintro requirespackaging>=25, so it is deliberately not part oflintro[tools]and must never be pip-installed into lintro’s own environment. The pinned version lives inlintro/_tool_versions.py;brew install checkovalso works but Homebrew’s formula lags upstream, so it may sit below that pin (the supported floor inmanifest.src.jsonis deliberately lower, so a brew install still runs).
Tool options:
| Option | Type | Default | Description |
|---|---|---|---|
checks | list / string | - | Run only these check IDs (--check) |
skip_checks | list / string | - | Skip these check IDs (--skip-check) |
compact | bool | true | Omit the offending code block from the report |
timeout | int | 120 | Per-invocation timeout in seconds |
--tool-options splits on commas, so several check IDs are passed pipe-delimited:
# Skip specific policies
lintro check --tools checkov --tool-options "checkov:skip_checks=CKV_AWS_18|CKV_AWS_21"
# Run only specific policies
lintro check --tools checkov --tool-options "checkov:checks=CKV_AWS_260"
Note: Only the
terraformandterraform_jsonframeworks run — lintro pins--framework terraform,terraform_jsonso checkov’s secrets framework does not double-report what gitleaks and trufflehog own.checksandskip_checkscan therefore only select Terraform policies; another framework’s ID (sayCKV_SECRET_6) matches nothing and reports a clean scan.Checkov’s own
severityandguidelinefields come from platform metadata it downloads, which--skip-downloadsuppresses on every lintro run. Findings therefore normalize to lintro’s default severity and link to the Checkov policy index. The parser still reads both fields so nothing is lost if that ever changes.
Checkov is listed under both terraform and security in the manifest language map, so
a no-config run that detects Terraform — or any run that selects the security set with
checkov on PATH — will invoke it. That can fail previously green Terraform CI on
first upgrade. Disable it with tools.checkov.enabled: false, or skip individual
policies with checkov:skip_checks.
Actionlint Configuration
Actionlint validates GitHub Actions workflows. Lintro discovers workflow files under
/.github/workflows/ when you run lintro check . and invokes the actionlint binary.
- Discovery: YAML files filtered to those in
/.github/workflows/ - Defaults: Lintro does not pass special flags; native actionlint defaults are used
- Local install: use
scripts/utils/install-tools.sh --localto placeactionlinton PATH - Docker/CI: the Docker image installs
actionlintduring build, so CI tests run it
# Validate workflows only
lintro check --tools actionlint
# Validate workflows along with other tools
lintro check --tools ruff,actionlint
Git Tools
Commitlint Configuration
Commitlint validates git commit messages against the
Conventional Commits specification. Unlike
file-based tools, it inspects git state: Lintro runs commitlint --last to validate the
repository’s most recent commit message.
- Requires a config; Lintro skips it as a non-error when none is present.
- Install:
bun add -D @commitlint/cli @commitlint/config-conventionalornpm install -D @commitlint/cli @commitlint/config-conventional. A project devDependency is preferred; a global or Homebrew install is used when there is no local one. See Node.js Tool Resolution. - Cannot auto-fix — amend the commit to satisfy the rules.
File: commitlint.config.js (or .commitlintrc.{js,cjs,json,yaml,yml}, or a
commitlint key in package.json)
// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };
Available --tool-options:
| Option | Type | Default | Description |
|---|---|---|---|
timeout | int | 30 | Max seconds to wait for commitlint |
# Validate the latest commit message
lintro check --tools commitlint
# Increase the timeout
lintro check --tools commitlint --tool-options "commitlint:timeout=60"
Project-Specific Configuration
Multi-Language Projects
For projects with multiple languages, organize configuration by component:
project/
├── .lintro.toml # Lintro-specific config
├── pyproject.toml # Python tools
├── .prettierrc # JavaScript/CSS
├── .yamllint # YAML files
├── .hadolint.yaml # Docker files
├── frontend/
│ └── .prettierrc # Frontend-specific overrides
└── backend/
└── pyproject.toml # Backend-specific overrides
Lintro Project Configuration
File: .lintro.toml (future feature)
[lintro]
default_tools = ["ruff", "pydoclint", "prettier", "yamllint"]
table_format = true
group_by = "auto"
exclude_patterns = ["migrations", "node_modules", "dist"]
[lintro.timeouts]
default = 30
pydoclint = 45
prettier = 60
[lintro.paths]
python = ["src/", "tests/"]
javascript = ["frontend/", "assets/"]
yaml = [".github/", "config/"]
docker = ["Dockerfile*", "docker/"]
[lintro.output]
format = "table"
save_to_file = true
file_prefix = "lintro-report"
Output System: Auto-Generated Reports
Lintro now generates all output formats for every run in a timestamped directory under
.lintro/ (e.g., .lintro/run-20240722-153000/).
You do not need to specify output format or file options. Each run produces:
console.log: The full console outputresults.json: Machine-readable resultsreport.md: Human-readable Markdown reportreport.html: Web-viewable HTML reportsummary.csv: Spreadsheet-friendly summary
This ensures you always have every format available for your workflow, CI, or reporting needs.
AI Configuration
Lintro includes optional AI-powered features for actionable summaries and interactive fix suggestions. See the full AI Features Guide for detailed usage.
Quick Setup
# Install AI dependencies (published package)
uv pip install 'lintro[ai]'
# Or from source checkout:
uv sync --extra ai
# Set the API key for your configured provider. `ai.provider` is required once
# `ai.lint` or `ai.review` is enabled; lintro has no default provider.
# Anthropic: ANTHROPIC_API_KEY
# Cursor: CURSOR_API_KEY
# OpenAI: OPENAI_API_KEY
# Custom: set ai.api_key_env in config to use any env var name
export ANTHROPIC_API_KEY=sk-ant-... # whichever provider you chose
# .lintro-config.yaml
ai:
enabled: true
lint: true # AI summaries / --fix on chk/fmt
review: false # lintro review (opt-in separately)
provider: anthropic # or cursor / openai — required, no default
transport: api # or cli — no default; doctor flags it unset
AI CLI Flags
| Flag | Effect |
|---|---|
| (none) | AI summary only (1 API call) |
--fix | Add interactive AI fix suggestions |
Config Defaults for Flags
Set default_fix to avoid typing the flag every time. This is a partial overlay showing
only the flag defaults — ai.provider is still required whenever ai.lint or
ai.review is enabled (and ai.transport has no default; lintro doctor flags it
unset), so merge it with the complete example in Quick Setup:
ai:
enabled: true
lint: true
review: true
default_fix: false # only run --fix when explicitly requested
Full AI Config Reference
| Setting | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Master switch; ANDs with lint / review |
lint | bool | false | Enable AI lint summaries on chk/fmt |
review | bool | false | Enable the lintro review AI diff review |
provider | string | none | AI provider (anthropic, cursor, or openai); required when ai.lint or ai.review is enabled |
transport | string | none | Transport (api or cli); no default — lintro doctor flags it unset when ai.lint or ai.review is enabled |
model | string | (default) | Model override |
api_key_env | string | (default) | Custom env var for API key |
default_fix | bool | false | Always run --fix in check |
auto_apply | bool | false | Apply fixes without confirmation |
auto_apply_safe_fixes | bool | true | Auto-apply safe-style fixes in non-interactive |
max_tokens | int | 4096 | Max tokens per request |
max_fix_attempts | int | 20 | Max issues to attempt fixing per run |
max_parallel_calls | int | 5 | Concurrent AI calls (1-20); honored with a cost cap; n−1 overshoot possible |
max_retries | int | 2 | Max retries for transient errors (0-10) |
rate_limit_max_retries | int | 6 | Retries for HTTP 429 only (0-20); waits Retry-After clamped to 300 s |
max_cost_usd | float | null | Legacy USD cap; prefer profiles. Overlay uncapped lifts; overlay 0 is an error |
api_timeout | float | 60.0 | Legacy timeout (s); prefer transports.*.timeout |
transports | object | empty profiles | Per-transport profiles (api / cli) — see AI review transports |
validate_after_group | bool | false | Validate immediately after each accepted group |
show_cost_estimate | bool | true | Show token/cost info in output |
context_lines | int | 15 | Lines of context sent for fix generation (1-100) |
fix_search_radius | int | 5 | Line search radius for fix application (1-50) |
checkpoint_retention | int | 10 | Git checkpoint refs kept (>=0; 0 = current only) |
checkpoint_fmt | bool | false | Git checkpoint before lintro format mutations |
retry_base_delay | float | 1.0 | Initial retry delay in seconds (min 0.1) |
retry_max_delay | float | 30.0 | Maximum retry delay in seconds (min 1.0) |
retry_backoff_factor | float | 2.0 | Retry delay multiplier (min 1.0) |
transcript_logging | bool | false | Opt-in NDJSON logging of AI provider traffic |
transcript_retention | int | 10 | Max transcript files kept under .lintro-cache |
Idiom Review Tool (idiom-review)
The idiom-review tool uses AI to find issues that syntax-matching linters cannot: code
that is syntactically correct but non-idiomatic or redundantly duplicated across files.
Unlike the AI summary and --fix flows, it is a first-class ToolDefinition plugin. It
is classified advisory, so it runs under lintro review — never under
lintro check or lintro format, whose findings must stay deterministic (#1308).
Install:
uv pip install 'lintro[ai]'
# Set the variable your chosen provider declares: ANTHROPIC_API_KEY,
# CURSOR_API_KEY or OPENAI_API_KEY.
export ANTHROPIC_API_KEY=sk-ant-...
The tool is disabled by default and is a no-op until explicitly opted in. When no AI
provider is available (missing SDK, key, or credits), it degrades gracefully to a
skipped result rather than failing the run. Findings are cached by content hash under
.lintro-cache/idiom, so unchanged files cost nothing on repeat runs.
Options:
| Option | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Opt-in gate — must be true to run |
mode | string | per-file | per-file · duplication · both |
min_confidence | string | medium | Drop findings below this level (low/medium/high) |
max_files | int | 25 | Cap on files reviewed per run (cost bound) |
language | string | python | Language to review; set explicitly for other languages |
Modes:
per-file— flags idiomatic misses per file (e.g. verbose loops instead ofany()/all()comprehensions).duplication— flags the same utility logic reimplemented across files, invisible to per-file linters, with a suggested extraction point.both— runs both modes in one pass.
Usage example:
# .lintro-config.yaml
ai:
enabled: true
provider: anthropic # or cursor / openai — required, no default
transport: api # or cli — no default; doctor flags it unset
tools:
idiom-review:
options:
enabled: true # opt-in gate (default: false)
mode: per-file # per-file | duplication | both
min_confidence: medium
max_files: 25 # cap files reviewed per run (cost bound)
Or enable ad hoc from the CLI without modifying config:
lintro review --advisory-only --tool-options idiom-review:enabled=true
Advanced Configuration
Tool Overlap
Two tools that touch the same files are not a conflict to be resolved by dropping one.
They are ordered by the derived DAG, and where both would format the same pattern the
loser is demoted rather than dropped — ruff keeps its fix capability and black owns
formatting on *.py. No tool is ever removed from a run, so --ignore-conflicts is
inert and kept only for call-site compatibility.
# See the order and the claim behind every constraint
lintro check --explain-order
# List every tool with its position in that order
lintro list-tools
Performance Optimization
Large Codebases
# Use specific tools for faster checks
lintro check --tools ruff
# Process directories separately
lintro check src/ --tools ruff,pydoclint
lintro check tests/ --tools ruff
# Exclude heavy directories
lintro check --exclude "venv,node_modules,migrations"
CI/CD Optimization
# Fast checks for PR validation
lintro check --tools ruff
# Full analysis for main branch
lintro check --tools all --output full-report.txt
Custom Output Formats
JSON Output (planned)
lintro check --output-format json --output results.json
{
"summary": {
"total_issues": 15,
"tools_run": ["ruff", "pydoclint"],
"files_checked": 42
},
"issues": [
{
"file": "src/main.py",
"line": 12,
"column": 5,
"tool": "ruff",
"code": "F401",
"message": "'os' imported but unused",
"severity": "error"
}
]
}
Markdown Output (planned)
lintro check --output-format markdown --output QUALITY_REPORT.md
Integration Patterns
Pre-commit Hooks
File: .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: lintro-check
name: Lintro Quality Check
entry: lintro check --output-format grid
language: system
pass_filenames: false
stages: [commit]
- id: lintro-fix
name: Lintro Auto-fix
entry: lintro format --output-format grid
language: system
pass_filenames: false
stages: [commit]
Makefile Integration
.PHONY: lint fix check quality install-tools
# Quality checks
lint:
lintro check
fix:
lintro format
check: lint
@echo "Quality check completed"
# Comprehensive quality report
quality:
lintro check --tools all --output quality-report.txt
@echo "Full quality report saved to quality-report.txt"
# Tool installation
install-tools:
pip install ruff pydoclint
npm install -D prettier
IDE Integration
VS Code Settings
File: .vscode/settings.json
{
"python.linting.enabled": false,
"python.formatting.provider": "none",
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.organizeImports": false
},
"files.associations": {
".lintro.toml": "toml"
}
}
File: .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Lintro Check",
"type": "shell",
"command": "lintro",
"args": ["check", "--output-format grid"],
"group": "test",
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{
"label": "Lintro Fix",
"type": "shell",
"command": "lintro",
"args": ["format", "--output-format grid"],
"group": "build"
}
]
}
Troubleshooting Configuration
Common Issues
1. Tool not respecting configuration:
# Check if config file is found
lintro check --tools ruff --verbose
# Verify config file syntax
ruff check --show-settings
2. Conflicting configurations:
# Check for multiple config files
find . -name "*.toml" -o -name ".ruff*" -o -name "setup.cfg"
# Use specific config
ruff check --config custom-ruff.toml
3. Performance issues:
# Profile tool execution
time lintro check --tools ruff --output-format grid
# Use more specific file patterns
lintro check "src/**/*.py" --tools ruff --output-format grid
Debug Configuration
# Enable verbose output
lintro check --verbose --output-format grid
# Check tool availability
lintro list-tools
# Test individual tools
ruff check src/
pydoclint src/main.py
prettier --check package.json
This comprehensive configuration guide should help you customize Lintro to fit your project’s specific needs and integrate seamlessly into your development workflow!