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. Use enforce.line_length to 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:

TierPurposeWhen Applied
executionWhat tools run and howAlways
enforceCross-cutting settings (line_length, target_python)Always (via CLI flags)
defaultsFallback config when no native config existsOnly when no native config
toolsPer-tool enable/disable and config sourceAlways
aiAI-powered summaries and fixesWhen 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

  1. Native configs are respected by default - Tools use their own .prettierrc, pyproject.toml [tool.ruff], etc.
  2. enforce settings override via CLI flags - Line length and target Python are injected as CLI arguments to ensure consistency
  3. defaults provide fallbacks - Only used when a tool has no native config file
  4. Simple and transparent - Users know exactly which config is used

Tiered Configuration Flow

The configuration system works in a specific order:

  1. 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.yaml or a non-empty [tool.lintro] table in pyproject.toml). A no-config first run scopes tools to languages detected from manifests and source files (a directory of *.py / *.js with no pyproject.toml / package.json still selects ruff/prettier). Use --tools all or lintro init for the full registry. An empty [tool.lintro] table is not a config, so language scoping still applies. An explicit named --tools list on the CLI bypasses this allowlist; default runs and --tools all remain filtered by it. This is a feat/minor default change: kitchen-sink CI that relied on the unscoped no-config toolset should pass --tools all or 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 example commitlint) are still selected when their native config file is present at a scan root. Nested YAML/Markdown/shell files count; a lone root README.md does not enable Markdown tools.
    • fail_fast: Whether to stop on first tool failure
    • parallel: 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)
  2. Enforce Tier - Cross-cutting settings injected as CLI flags

    • These settings override native configs via CLI arguments
    • Example: line_length: 88 becomes --line-length 88 for ruff/black
    • Applied to all tools that support the setting
  3. Defaults Tier - Fallback configuration when no native config exists

    • Only used if tool’s native config file is not found
    • Example: If .prettierrc doesn’t exist, use defaults.prettier
    • Generated as temporary config files when needed
  4. Tools Tier - Per-tool enable/disable, config source, and auto-install

    • enabled: Whether the tool is enabled
    • config_source: Optional explicit path to native config file
    • auto_install: Per-tool auto-install override (true/false/omit to inherit global)
  5. AI Tier - AI-powered summaries and fix suggestions (opt-in)

    • enabled: Whether AI features are active (default: false)
    • provider: AI provider to use (anthropic, cursor or openai) — required once ai.lint or ai.review is enabled; there is no default
    • Applied only when enabled and a valid API key is set

Configuration Resolution Example

For a tool like Prettier:

  1. Check if tool is enabled (tools.prettier.enabled)
  2. Check for native config (.prettierrc, .prettierrc.json, etc.)
  3. If native config found:
    • Use native config
    • Inject enforce.line_length as --print-width CLI flag
    • Ignore defaults.prettier
  4. If no native config found:
    • Generate temp config from defaults.prettier
    • Inject enforce.line_length as --print-width CLI flag
    • Use generated config

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:

  1. Built-in defaults — the empty baseline configuration.
  2. User-level global config — ~/.lintro-config.yaml (see below). Supplies base values shared across all your projects.
  3. Project config — the first of: an upward-searched .lintro-config.yaml variant, or [tool.lintro] in pyproject.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):

  1. LINTRO_GLOBAL_CONFIG — an explicit file path, or off (also 0, 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; use off to opt out deliberately.
  2. ~/.lintro-config.yaml — the primary, authoritative location.
  3. $XDG_CONFIG_HOME/lintro/config.yaml — an XDG fallback, where $XDG_CONFIG_HOME defaults to ~/.config when 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 (an ai: 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 set LINTRO_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). lintro used to compute a 0-100 “health score” along with check --score and check --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-under gated CI on that fabricated number; it is gone with no replacement, because chk already 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 ![Lintro Issues](https://img.shields.io/badge/lintro-0%20issues-brightgreen). 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-readable output string.
  • issues_count counting only genuine findings. No synthetic TIMEOUT pseudo-issue is invented, and the timeout never reaches summary.total_issues. Issues legitimately detected before the timeout (for example the pre-fix check of a format run) 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:

FormatPathPurpose
SARIF.lintro/artifacts/sarif/results.sarif.jsonGitHub Code Scanning ingestion
JSON.lintro/artifacts/json/results.jsonStructured 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: false is set in config, or
  • the --no-art flag is passed to lintro 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
VariableDescriptionDefault
LINTRO_LOG_DIRBase directory for run logs and artifacts.lintro
LINTRO_VERSION_TIMEOUTTimeout in seconds for tool version checks (must be >= 1)30
LINTRO_DOCKERForce Docker install-context detection when set to 1-
LINTRO_CONFIGShown in the lintro environment report; informational only-
LINTRO_ENABLE_EXTERNAL_PLUGINSOpt in to loading external (third-party) plugins (1/0)0
LINTRO_NO_CACHEForce-clear caches each invoke (1/true/yes/on)-
LINTRO_AI_PROVIDEROverride ai.provider (anthropic / cursor / openai)-
LINTRO_AI_MODELOverride ai.model-
LINTRO_AI_TRANSPORTOverride ai.transport (api / cli)-
LINTRO_AI_ENABLEDOverride ai.enabled (1/0/true/false)-
LINTRO_AI_REVIEWOverride ai.review (1/0/true/false)-
LINTRO_AI_MAX_COST_USDOverride 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.yaml for those settings. Auto-install is resolved from the --auto-install flag, then execution.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_PLUGINS is truthy or the plugins config opts in (a trusted allowlist is itself an opt-in, as is enabled: true).
  • When a trusted allowlist 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.plugins entry 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:

ReasonDescription
node_modules not foundNode.js deps missing and auto-install is disabled
disabled in configTool disabled via tools.<name>.enabled: false
not in enabled_toolsTool 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 messagesTool 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:

  1. Explicit choice — lintro install --node-package-manager npm.
  2. packageManager metadata — the Corepack field in package.json ("packageManager": "pnpm@9.1.0").
  3. Lockfile evidence — bun.lock/bun.lockb, pnpm-lock.yaml, yarn.lock, package-lock.json, npm-shrinkwrap.json.
  4. 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.

FlagEffect
--node-package-manager {bun,npm,pnpm,yarn}Force the manager, overriding packageManager and lockfiles.
--globalInstall 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:

ToolCLI Flag for line_lengthCLI Flag for target_python
Ruff--line-length 88--target-version py313
Black--line-length 88--target-version py313
Prettier--print-width 88N/A

Tools without CLI support:

Some tools (Yamllint, Markdownlint) don’t have CLI flags for line length. For these:

  • Use the defaults tier 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:

  1. Lintro checks if the tool has a native config file (e.g., .prettierrc)
  2. If NO native config exists, Lintro generates a temp file from defaults
  3. If native config EXISTS, defaults are 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”.

RuleBehaviour
Phase per patternA tool occupies the earliest phase it holds there: FIX → FORMAT → CHECK
One invocationA tool that both fixes and checks a pattern sits in FIX; its diagnostics come out with it
EdgesEvery earlier-phase tool precedes every later-phase tool for that pattern
TiesEqual phases derive no phase edge; two writers that share a file still get a conflict edge
Broad claimsOnly * subsumes: *.py never joins the test_*.py group, though two writers of one test_a.py still conflict
Project-scopedA claim with no patterns (osv-scanner) derives no phase edge; a patternless writer conflicts with every writer under its roots
CyclesReported 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:

  1. Mutation. Every mutating capability (FIX, FORMAT) runs in derived DAG order.
  2. Verify. One pass runs the CHECK capability of the same tools. For every tool that declares CHECK, 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:

SurfaceKeyMeaning
JSON, per toolnet_resolvedBefore minus after; null when the residual is unknown
JSON, per toolremainingMeasured residual; null when the residual is unknown
JSON, per toolresidual_unknowntrue when no after-count was taken
JSON, per toolresidual_unknown_reasonWhy — the check crashed, timed out or was skipped
JSON, run summarysummary.total_net_resolvedSum over the tools that were measured
JSON, run summarysummary.total_remainingSum over the tools that were measured
JSON, run summarysummary.residual_unknown_toolsTools the two totals above do not cover
JSONL stream, per toolnet_resolved_countPer-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:

  1. a configured override, execution.precedence;
  2. FIX before FORMAT — a fix rewrites structure and leaves layout dirty;
  3. 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);
  4. 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 COM812 trailing commas and E501 line 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
  • lintro check

    • Ruff runs lint checks (Ruff format_check=False) unless explicitly overridden
    • Black runs --check as a post-check to enforce formatting

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:

  1. node_modules/.bin/<binary>, searched upward from the directory being checked until the nearest package.json or .git (whichever is hit first). A nested package with its own package.json stops there; a decoy node_modules above that boundary is ignored. This is the preferred answer: it is lockfile-pinned, offline, and it is the same binary your editor and your own npm run scripts use.
  2. the PATH-resolved absolute path of <binary> — a global install (bun add -g, npm install -g) or a Homebrew formula.
  3. bunx <package>@<pinned> / npx --yes --package <package>@<pinned> <binary> — a registry fetch at the version Lintro pins in its manifest. bunx is tried first, then npx. bunx keeps the short form when the executable name matches the package name (bunx prettier@<pinned>); it uses --package when they differ (bunx --package typescript@<pinned> tsc). npx always uses --yes --package so it cannot hang waiting for a TTY prompt.
  4. 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 to bunx <binary>/npx <binary> (never consulting PATH, and resolving @latest) or preferred PATH ahead 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.
  • prettier is no longer PATH-only. It previously had no Node builder at all and was resolved as a bare prettier name against PATH; a project’s lockfile-pinned prettier was never used. It now follows the same chain as everything else.

Also fixed on the way: commitlint and markdownlint-cli2 had no npx branch, 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: --strict with --ignore-missing-imports enabled to avoid third-party stub noise (applied unless you override via set_options() or --tool-options).
  • Config discovery: mypy auto-discovers pyproject.toml [tool.mypy], mypy.ini, or setup.cfg [mypy] and the discovered config is passed via --config-file. When a native config provides exclude, 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 run
  • skips: Comma-separated list of test IDs to skip
  • exclude: Comma-separated list of paths to exclude
  • exclude_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:

OptionTypeDescription
configstringRule config: auto, p/python, p/javascript, or path
excludelistPatterns to exclude from scanning
includelistPatterns to include in scanning
severitystringMinimum severity: INFO, WARNING, ERROR
timeout_thresholdintPer-file timeout in seconds
jobsintNumber 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:

OptionTypeDescription
enablestring | listCheck 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.
inconclusiveboolReport findings cppcheck cannot fully confirm.
stdstringLanguage standard (e.g. c11, c++17).
inline_supprboolHonor inline // cppcheck-suppress comments.
suppressstring | listSuppression 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:

OptionTypeDescription
no_gitbooleanScan without git history (files only)
configstringPath to custom gitleaks config file
baseline_pathstringPath to baseline file (ignore known secrets)
redactbooleanRedact secrets in output (default: true)
max_target_megabytesintegerSkip 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:

OptionTypeDescription
no_verificationbooleanDisable live credential verification (default: true)
resultsstringFilter result type — single value (see note below)
configstringPath to a custom detector configuration file
exclude_pathsstringPath to a file of newline-separated exclude regexes
concurrencyintegerNumber of concurrent workers

Note: --tool-options uses commas to separate options, so a comma-separated results value (e.g. verified,unverified) cannot be passed on the CLI. Use a single value such as results=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:

OptionTypeDescription
timeoutintegerScan timeout in seconds (default: 120)
check_suppressionsbooleanRun 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:

OptionTypeDescription
timeoutintegerScan 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.txt or services/api/requirements.txt are 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, sphinx
  • arg-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 signature
  • skip-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 format never 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 pylint invocation. Cross-module checkers only see clones that appear in one run, so per-file execution would silently miss every R0801.
  • 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 R0801 message 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/error are ERROR, warning and refactor (which includes duplicate-code) are WARNING, and convention/info are INFO.
  • Check-only. lintro format never runs pylint.

Available --tool-options:

  • disable: Message or category to disable, forwarded to --disable=. Commas separate --tool-options entries, 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:

OptionTypeDescription
projectstringPath to tsconfig.json (—project)
strictbooleanEnable all strict type checking options
skip_lib_checkbooleanSkip type checking of declaration files (default: true)
timeoutintegerExecution 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.json
  • oxlint.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:

OptionTypeDescription
configstringPath to config file (—config)
tsconfigstringPath to tsconfig.json (—tsconfig)
allowlist[str]Rules to allow (turn off)
denylist[str]Rules to deny (report as errors)
warnlist[str]Rules to warn on (report as warnings)
quietbooleanSuppress warnings, only report errors (—quiet)
timeoutintegerExecution 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.mjs
  • stylelint.config.js, stylelint.config.cjs, stylelint.config.mjs
  • a stylelint key in package.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:

OptionTypeDescription
configstringPath to a stylelint config file (--config)
verbose_fix_outputbooleanInclude raw stylelint output in fix()
timeoutintegerExecution 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:

OptionTypeDescription
configstringPath to config file (—config)
ignore_pathstringPath to ignore file (—ignore-path)
timeoutintegerExecution 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:

OptionTypeDescription
rulesetstringExplicit path to a ruleset (overrides discovery)
timeoutintegerExecution 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:

OptionTypeDescription
rootstringRoot directory for the Astro project
timeoutintegerExecution 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:

OptionTypeDescription
thresholdstringMinimum severity to report: error or warning (default: warning)
tsconfigstringPath to tsconfig.json file
timeoutintegerExecution 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:

OptionTypeDescription
projectstringPath to tsconfig.json file
strictbooleanEnable strict type checking mode
skip_lib_checkbooleanSkip checking declaration files (default: true)
use_project_filesbooleanUse tsconfig’s include patterns (default: false)
timeoutintegerExecution 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:

  1. node_modules/.bin/html-validate, searched upward from the directory being checked until the nearest package.json or .git.
  2. the PATH-resolved absolute path of html-validate (e.g. a global install).
  3. bunx html-validate@<pinned> — registry fallback, only if bunx is available.
  4. npx --yes --package html-validate@<pinned> html-validate — registry fallback, only if npx is available.
  5. 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:

OptionTypeDescription
timeoutintegerExecution 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:

OptionTypeDescription
dialectstringSQL dialect (ansi, bigquery, postgres, mysql, etc.)
exclude_ruleslistList of rules to exclude from checking
ruleslistList of specific rules to include
templaterstringTemplater to use (raw, jinja, python, placeholder)

Supported Dialects:

  • ansi - ANSI SQL standard
  • bigquery - Google BigQuery
  • clickhouse - ClickHouse
  • databricks - Databricks SQL
  • db2 - IBM Db2
  • exasol - Exasol
  • hive - Apache Hive
  • mysql - MySQL
  • oracle - Oracle Database
  • postgres - PostgreSQL
  • redshift - Amazon Redshift
  • snowflake - Snowflake
  • soql - Salesforce SOQL
  • sparksql - Apache Spark SQL
  • sqlite - SQLite
  • teradata - Teradata
  • tsql - 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] in pyproject.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 E100 runtime 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:

OptionTypeDescription
configstringPath to a Vale config file (maps to --config)
min_alert_levelstringMinimum alert level: suggestion, warning, or error
timeoutintPer-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.toml or _typos.toml exists at a scan root. crate-ci/typos also reads [tool.typos] in pyproject.toml and [package.metadata.typos] / [workspace.metadata.typos] in Cargo.toml; those files are not in Lintro’s native_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 in pyproject.toml, or an in-memory tools: section. Typos then runs when the binary is on PATH and it is not filtered out by execution.enabled_tools or tools.typos.enabled: false. lintro init’s recommended profile writes a language-based enabled_tools allowlist that does not include typos, so those projects do not start spell-checking on upgrade. An existing unscoped lintro check (enabled_tools: [] or omitted) will.

lintro check --tools typos selects 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_tools allowlist runs the full unscoped registry, including typos. On a first run, omit typos.toml / .typos.toml / _typos.toml instead. On a project that already has a Lintro config, disable the tool:

    tools:
      typos:
        enabled: false

    Or leave it out of execution.enabled_tools if you use an allowlist (lintro init --profile recommended already does).

    --tools is an allowlist rather than an opt-out: lintro check --tools ruff runs 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.toml rather 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-re pattern so the same word is still caught elsewhere. lintro format --tools typos applies 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:

OptionTypeDescription
timeoutintegerPer-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:

OptionTypeDescription
timeoutintegerExecution 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:

OptionTypeDescription
timeoutintegerExecution 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:

OptionTypeDescription
severitystrMinimum severity: error, warning, info, style
excludelist[str]List of codes to exclude (e.g., SC2086, SC2046)
shellstrForce shell dialect: bash, sh, dash, ksh, zsh
external_sourcesboolFollow sourced files external to the script (-x). Default False
source_pathslist[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:

CodeDescription
SC2086Double quote to prevent globbing and word splitting
SC2046Quote this to prevent word splitting
SC2002Useless use of cat
SC2006Use $(…) notation instead of backticks
SC2034Variable appears unused
SC2155Declare 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:

OptionTypeDescription
indentintIndentation size. 0 for tabs, >0 for spaces
binary_next_lineboolBinary ops like && and | may start a line
switch_case_indentboolIndent switch cases
space_redirectsboolRedirect operators followed by space
language_dialectstrShell dialect: bash, posix, mksh, bats
simplifyboolSimplify 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:

OptionTypeDescription
recursiveboolRecursively scan directories for .env files. Default False
excludelist[str]File or directory paths to exclude
skip_checkslist[str]Check names to bypass (maps to --ignore-checks)
schemastrPath to a schema file to validate .env contents

Note: real .env files 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.example for 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:

OptionTypeDescription
schemastringPath or URL to JSON schema for validation
aligned_arraysbooleanAlign array entries vertically
aligned_entriesbooleanAlign table entries (key = value)
array_trailing_commabooleanAdd trailing comma in multi-line arrays
indent_stringstringIndentation string (default: 2 spaces)
reorder_keysbooleanReorder 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:

OptionTypeDescription
configstringPath to a buf.yaml (CLI --tool-options cannot pass inline YAML)
disable_symlinksbooleanDo 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 .proto files holding a project marker (.git, pyproject.toml, package.json, …), not their common parent directory. A relative config path 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 a buf.yaml with a modules path when 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), or scripts/utils/install-tools.sh --local --tools checkov. Checkov requires packaging<24 while lintro requires packaging>=25, so it is deliberately not part of lintro[tools] and must never be pip-installed into lintro’s own environment. The pinned version lives in lintro/_tool_versions.py; brew install checkov also works but Homebrew’s formula lags upstream, so it may sit below that pin (the supported floor in manifest.src.json is deliberately lower, so a brew install still runs).

Tool options:

OptionTypeDefaultDescription
checkslist / string-Run only these check IDs (--check)
skip_checkslist / string-Skip these check IDs (--skip-check)
compactbooltrueOmit the offending code block from the report
timeoutint120Per-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 terraform and terraform_json frameworks run — lintro pins --framework terraform,terraform_json so checkov’s secrets framework does not double-report what gitleaks and trufflehog own. checks and skip_checks can therefore only select Terraform policies; another framework’s ID (say CKV_SECRET_6) matches nothing and reports a clean scan.

Checkov’s own severity and guideline fields come from platform metadata it downloads, which --skip-download suppresses 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 --local to place actionlint on PATH
  • Docker/CI: the Docker image installs actionlint during 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-conventional or npm 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:

OptionTypeDefaultDescription
timeoutint30Max 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 output
  • results.json: Machine-readable results
  • report.md: Human-readable Markdown report
  • report.html: Web-viewable HTML report
  • summary.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

FlagEffect
(none)AI summary only (1 API call)
--fixAdd 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

SettingTypeDefaultDescription
enabledboolfalseMaster switch; ANDs with lint / review
lintboolfalseEnable AI lint summaries on chk/fmt
reviewboolfalseEnable the lintro review AI diff review
providerstringnoneAI provider (anthropic, cursor, or openai); required when ai.lint or ai.review is enabled
transportstringnoneTransport (api or cli); no default — lintro doctor flags it unset when ai.lint or ai.review is enabled
modelstring(default)Model override
api_key_envstring(default)Custom env var for API key
default_fixboolfalseAlways run --fix in check
auto_applyboolfalseApply fixes without confirmation
auto_apply_safe_fixesbooltrueAuto-apply safe-style fixes in non-interactive
max_tokensint4096Max tokens per request
max_fix_attemptsint20Max issues to attempt fixing per run
max_parallel_callsint5Concurrent AI calls (1-20); honored with a cost cap; n−1 overshoot possible
max_retriesint2Max retries for transient errors (0-10)
rate_limit_max_retriesint6Retries for HTTP 429 only (0-20); waits Retry-After clamped to 300 s
max_cost_usdfloatnullLegacy USD cap; prefer profiles. Overlay uncapped lifts; overlay 0 is an error
api_timeoutfloat60.0Legacy timeout (s); prefer transports.*.timeout
transportsobjectempty profilesPer-transport profiles (api / cli) — see AI review transports
validate_after_groupboolfalseValidate immediately after each accepted group
show_cost_estimatebooltrueShow token/cost info in output
context_linesint15Lines of context sent for fix generation (1-100)
fix_search_radiusint5Line search radius for fix application (1-50)
checkpoint_retentionint10Git checkpoint refs kept (>=0; 0 = current only)
checkpoint_fmtboolfalseGit checkpoint before lintro format mutations
retry_base_delayfloat1.0Initial retry delay in seconds (min 0.1)
retry_max_delayfloat30.0Maximum retry delay in seconds (min 1.0)
retry_backoff_factorfloat2.0Retry delay multiplier (min 1.0)
transcript_loggingboolfalseOpt-in NDJSON logging of AI provider traffic
transcript_retentionint10Max 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:

OptionTypeDefaultDescription
enabledboolfalseOpt-in gate — must be true to run
modestringper-fileper-file · duplication · both
min_confidencestringmediumDrop findings below this level (low/medium/high)
max_filesint25Cap on files reviewed per run (cost bound)
languagestringpythonLanguage to review; set explicitly for other languages

Modes:

  • per-file — flags idiomatic misses per file (e.g. verbose loops instead of any()/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!