Skip to content
cgraf78Public

About

One format/lint command that dispatches to 30+ tools: ruff, shfmt, biome, stylua, and more

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

checkrun

Tests License Python Platform

One format command and one lint command for 30+ file types.

autoformat FILE... and autolint FILE... pick the right tool for each file: ruff, shfmt and shellcheck, biome, stylua, clang-format, rustfmt, taplo, actionlint and zizmor, hadolint, typos, and two dozen more. Each tool uses the project's own config when it has one, with optional global fallbacks under ~/.config/checkrun. Tools you haven't installed are skipped, so the same setup works on every machine. Editors, commit hooks, CI, and AI agents can all call the same two commands instead of each carrying its own language table. autolint --json emits one diagnostic per line, and checkrun verify runs slower project-wide analyzers (clippy, golangci-lint, govulncheck, cargo-audit) on demand.

Unlike trunk check or MegaLinter, checkrun runs the formatters and linters already on your PATH instead of installing its own. Unlike conform.nvim or nvim-lint, it is not tied to an editor.

Supported Tools

File type Formatter Linter
Python ruff format ruff check
Shell shfmt shellcheck
Zsh shfmt zsh -n
Go goimports + gofumpt -
Lua stylua selene
C/C++ clang-format gated clang-tidy
CMake cmake-format cmake-lint
Rust rustfmt -
Java google-java-format dry-run format check
PHP php-cs-fixer php -l
Ruby rubocop layout autocorrect rubocop
HTML superhtml fmt superhtml check
TOML taplo taplo
JSON/JSONC/CSS/SCSS/LESS/JS/JSX/TS/TSX biome biome
Nix nixfmt statix
Protobuf buf format buf lint
YAML yamlfmt -
GitHub Actions yamlfmt actionlint + zizmor
Markdown rumdl rumdl
Spelling - typos
Starlark buildifier buildifier --lint
Make - checkmake
Dockerfile/Containerfile dockerfmt hadolint
EditorConfig - editorconfig-checker
Git config - git config --file --list
Crontab - crontab -T
Tmux config - tmux source-file -n
Systemd units - systemd-analyze verify

See Tool Details for the clang-tidy gate and the checkrun verify analyzers.

Install

Requirements:

  • Bash.
  • yq v4. autoformat and autolint exit with an error without it.
  • Python 3.11+ (for stdlib tomllib). checkrun tries CHECKRUN_PYTHON, then python3 on PATH, /usr/bin/python3, /opt/homebrew/bin/python3, and /usr/local/bin/python3. macOS's stock python3 is too old.
  • jq for autolint --json.
  • The formatters and linters you want. checkrun skips the rest silently, so a file whose tools are all missing produces no output and exit status 0.
curl -fsSL https://raw.githubusercontent.com/cgraf78/checkrun/main/install.sh | bash

This keeps a durable managed checkout under $XDG_DATA_HOME when that path is absolute, or under $HOME/.local/share otherwise, and publishes links to its commands and manual pages. Rerunning the curl command safely updates the clean managed checkout before republishing the links.

To choose and manage the checkout yourself instead, keep it at a stable path and run:

./install.sh

The installer creates checkout-backed symlinks for all three commands under $HOME/.local/bin and for their manual pages under $HOME/.local/share/man/man1. Set PREFIX to relocate both trees, or set BIN_DIR and MAN_DIR independently. Re-running the installer is safe and retargets existing symlinks, but it refuses to replace a non-symlink path. Moving or deleting the checkout breaks the installed links.

Command links resolve Checkrun's matching library, registry, and schemas from the checkout. The installer therefore creates no second lib/checkrun/ or share/checkrun/ tree and no completion tree. Use the absolute paths of non-binary assets in this checkout.

If you manage tools with shdeps, it can install checkrun as a dependency instead: it links the bin/ entry points and man pages the same way, and resolves non-binary assets with shdeps dep-file.

Example

With ruff installed, in a directory without project config:

$ cat app.py
import os

def f( x ):
    return 1
$ autolint --json app.py
{"path":"/tmp/demo/app.py","line":1,"col":1,"end_line":1,"end_col":10,"severity":"error","code":"I001","message":"Import block is un-sorted or un-formatted","source":"ruff"}
{"path":"/tmp/demo/app.py","line":1,"col":8,"end_line":1,"end_col":10,"severity":"error","code":"F401","message":"`os` imported but unused","source":"ruff"}
$ checkrun explain app.py
/tmp/demo/app.py
  exists: true
  filetype: python
  format: ruff
  lint tools: ruff
  schemas: none
$ autoformat app.py

autolint exits 1 because it found problems. autoformat rewrites app.py in place (def f(x):, two blank lines before it). Without ruff, both commands do nothing and exit 0.

CLIs

checkrun registry --json
checkrun capabilities --json
checkrun capabilities --has autolint-files0-stdin
checkrun editor-metadata --json
checkrun explain [--json] FILE [FILE...]
checkrun plan --json [--phase format|lint] FILE [FILE...]
checkrun schema refresh [--check] [--association NAME] [--timeout SECONDS]
checkrun verify [--json] [--tool cargo-audit|cargo-clippy|clang-tidy|golangci-lint|govulncheck] [PATH...]
checkrun format|fmt FILE [FILE...]
checkrun lint|check [--fix] [--json] FILE [FILE...]
autoformat FILE [FILE...]
autolint [--fix] [--json] FILE [FILE...]
autolint [--fix] [--json] --files0-from MANIFEST|-

autoformat always mutates eligible files. It exits 0 even when a formatter fails so save-time hooks surface stderr without blocking the caller.

autolint is read-only by default, applies fixes with --fix, and emits newline-delimited diagnostics with --json. It exits non-zero when lint findings exist. checkrun lint and checkrun check are aliases for this same fast linter path. Integrations with large file sets can use --files0-from with a NUL-delimited regular file, or --files0-from - with NUL-delimited standard input, instead of expanding every path into argv; manifest input and positional paths are mutually exclusive. checkrun capabilities --has autolint-files0-stdin reports stdin-transport support through its exit status without starting the registry interpreter. Exit 2 (a tool or structural failure) outranks ordinary findings; a caller that may tolerate tool failures can set CHECKRUN_AUTOLINT_REPORT to a file that receives findings=0 or findings=1 once the run completes.

checkrun verify is the explicit project-check surface for work that should not run from save-time editor lint. Its project backends are deduplicated by owner root: Go analyzers run once per go.mod, Rust analyzers run once per Cargo.toml, and C/C++ clang-tidy runs for selected C/C++ files only when project metadata makes the invocation meaningful. Missing path arguments are still treated as scope hints for verify, so deleted or renamed Go/Rust/C++ files can trigger checks for the nearest surviving owning project context. Deleted C/C++ paths only select a surviving directory context when that context is narrower than the repository or invocation root.

The formatter and linter entry points ignore missing, deleted, or explicitly ignored files. Missing language tools are treated as graceful no-ops so a host without a language toolchain does not break unrelated workflows.

checkrun registry --json emits the raw registry for debugging and tests. checkrun capabilities --json emits machine-readable filetype and editor language-ID metadata for editor integrations. checkrun plan --json emits the stable execution-plan API for integrations that need to inspect Checkrun decisions without running tools. checkrun editor-metadata --json combines those capabilities with the active policy's LSP schema configuration in a versioned, deterministic object for update-time editor cache generation. Unlike the live schema projection, it does not resolve dependency assets or expand host paths: $HOME remains a literal placeholder and dependency assets use shdeps:<dependency>/<repository-relative-asset>. checkrun explain reports the normalized path, inferred filetype, phase-specific ignore decisions, candidate formatter/linter tools, fallback config names, and matching schema associations for selected files.

See docs/workflow-contract.md for the cross-surface ownership contract between Checkrun, Sley, editor adapters, dotfiles, humans, and agents.

Dependencies

  • Bash for the CLI entry points and shell libraries.
  • yq is required by autoformat and by autolint once at least one lintable file remains after filtering.
  • jq is required by autolint --json, schema association policy, and any checks that need JSON diagnostics.
  • Python 3.11+ is required for registry planning and schema association helpers. Set CHECKRUN_PYTHON to a compatible interpreter when the host default python3 does not include stdlib tomllib.
  • shdeps is required only when a schema association policy references a dependency-owned schema with "dependency": "owner/repo".

Formatter, linter, and verification backends such as ruff, shfmt, shellcheck, stylua, biome, rumdl, cargo-audit, and govulncheck are optional. Missing backends are graceful no-ops for their file types or project roots so hosts can install only the language tools they use.

CI installs a representative backend set from .github/mise/checkrun-ci.toml before running the full test suite. The adjacent mise.lock pins exact tool versions and download URLs for the Linux and macOS architectures used by CI, plus checksums when the tool backend publishes them. A weekly workflow refreshes that lock on a bot-owned branch and dispatches the full test matrix on its pull request. The manifest and lock are test infrastructure for this repo; installed consumers should provide their own toolchain through the host environment or integration layer.

test/checkrun-test runs independent behavioral suites with four workers by default. Set CHECKRUN_TEST_JOBS to a positive integer to tune that bound for a specific host. Cancellation and hook-latency suites always run alone so faster feedback does not weaken their timing and process-lifecycle assertions.

Examples

examples/ contains checked, copyable examples for schema association policy, a shfmt fallback, and a Shdeps-backed Neovim integration. Their tests exercise Checkrun's real policy parser, metadata generator, TOML reader, and Neovim adapter so documentation cannot silently drift from the public interfaces it demonstrates.

Public API

  • bin/checkrun, bin/autoformat, and bin/autolint are the PATH-visible CLIs.
  • share/checkrun/registry.json is the shared tooling registry. It drives filetype inference, formatter/linter selection, checkrun plan, checkrun explain, editor language-ID aliases, and the derived checkrun capabilities --json output.
  • share/checkrun/schemas/registry.schema.json validates the registry shape; the internal lib/checkrun/registry.py interpreter enforces cross-object invariants that JSON Schema cannot express cleanly.
  • lib/checkrun/schemas/schema_policy.py is the public schema association API shared by editors and linting. --lsp-schemas emits the editor/LSP projection. Its documented Python facade is limited to the module's __all__; other helpers are internal. Associations may set "dependency": "owner/repo" and a repo-relative "schema" path when a schema is public API owned by a shdeps-managed dependency; the interpreter resolves those through shdeps dep-file. Associations may set editorSource when an editor-native schema URI should be exposed to LSP/editor clients without making it a refreshable or offline-enforced schema source.
  • lib/checkrun/nvim.lua is the optional Neovim adapter API. It does not depend on shdeps, Sley, LazyVim, Mason, or local editor policy; callers pass explicit commands, environment, and working directories when they do not want the PATH/default-script behavior. Its editor_metadata(opts) API also materializes the portable checkrun editor-metadata --json contract for a host. Callers pass their checked metadata path and an optional dependency resolver callback, keeping path and URI semantics in Checkrun without making Checkrun depend on a particular dependency manager.
  • lib/checkrun/schemas/schema-lint.py validates files through that policy.
  • lib/checkrun/schemas/schema_refresh.py refreshes pinned public schema payloads from association source URLs. It is exposed as checkrun schema refresh so host repos can run the same updater directly or from scheduled CI.
  • lib/checkrun/verify.py runs explicit project-scope verification checks. These checks are intentionally separate from registry lint capabilities so editor integrations do not inherit slow, broad, or network-sensitive scans.
  • share/checkrun/schemas/associations.schema.json is the JSON Schema for schema association policy files.
  • share/checkrun/schemas/diagnostics.schema.json is the JSON Schema for one newline-delimited diagnostic emitted by autolint --json. Editor adapters use this producer-owned contract when translating Checkrun diagnostics into their native diagnostic APIs.
  • share/checkrun/schemas/shfmt-config.schema.json is the JSON Schema for the fallback TOML options that Checkrun translates into shfmt arguments when a project has no EditorConfig.
  • share/checkrun/shell.sh is a stable no-op shell loader for integration harnesses that source each dependency's shell API uniformly.

With shdeps, source non-binary assets through it so install locations stay under the dependency manager's contract; without it, use the same paths inside your checkout:

. "$(shdeps dep-file cgraf78/checkrun share/checkrun/shell.sh)"
python3 "$(shdeps dep-file cgraf78/checkrun lib/checkrun/schemas/schema_policy.py)" --lsp-schemas
python3 "$(shdeps dep-file cgraf78/checkrun lib/checkrun/schemas/schema_policy.py)" --lsp-schemas --editor-sources
checkrun capabilities --json
checkrun editor-metadata --json

The editor metadata object has version 1, with the existing versioned capability object under capabilities and portable LSP maps under schemas. Consumers materialize it while generating their local cache: replace $HOME in ordinary paths and URLs, regex-escape HOME when replacing it in TOML pattern keys, and resolve each shdeps: reference through shdeps dep-file before converting it to a file:// URL. This command is intended for explicit update and CI workflows, not Neovim startup or save-time hooks.

Neovim Adapter API

The Neovim module is a protocol adapter, not a complete plugin. It translates Checkrun's public capability and schema-policy outputs into Neovim data structures while leaving routing policy in the consuming config.

local checkrun_nvim = dofile("lib/checkrun/nvim.lua")

vim.filetype.add(checkrun_nvim.filetypes({
  command = { "checkrun", "capabilities", "--json" },
}))

local yaml_before_init = checkrun_nvim.yaml_before_init({
  command = { "python3", "lib/checkrun/schemas/schema_policy.py", "--lsp-schemas" },
})

Public functions:

  • editor_metadata(opts) reads direct or file-backed portable editor metadata, expands $HOME, resolves dependency schema assets through a caller callback, URI-encodes local files, and regex-escapes TOML association keys.
  • capabilities(opts) reads and normalizes checkrun capabilities --json.
  • filetypes(opts) converts Checkrun custom filetype metadata into vim.filetype.add input.
  • add_filetypes(opts) registers those filetypes and returns the registered table.
  • json_schemas(opts), yaml_schemas(opts), and toml_schema_associations(opts) return the corresponding LSP schema maps.
  • yaml_before_init(opts) returns a yamlls before_init callback that merges Checkrun's YAML schema policy with SchemaStore when SchemaStore is installed.

Supported options are command, env, cwd, direct capabilities or config tables for tests/pre-fetched callers, and script/python for the default schema-policy command. editor_metadata accepts direct metadata or a file path, an explicit home, and resolve_dependency(dependency, asset). The module does not choose formatter/linter plugin names, Treesitter parsers, LSP servers, package managers, keymaps, or workspace roots.

Editor And Hook Flow

Editors, agent hooks, Git hooks, or Sapling hooks can either call autoformat and autolint directly or route through a higher-level policy tool such as Sley. Sley's default hook policy delegates back to these CLIs, while consumers can override that layer for repo-specific policy.

The important contract is one-way: checkrun owns formatter/linter dispatch, and consumers decide when to invoke it. Sley and editor adapters should consume Checkrun's public CLIs and JSON APIs instead of carrying their own language maps, selector tables, or direct low-level tool dispatch.

Fast Check Policy

checkrun check, checkrun lint, and autolint are the fast automatic check surface for editors, hooks, and agents. A backend belongs there only when it is safe to run from changed-file or save-time workflows:

  • it is bounded to the selected file, or to a small owner project that the adapter can discover cheaply from that file
  • it does not require network access, dependency installation, vulnerability database updates, full test suites, or broad repository scans
  • it produces deterministic diagnostics or fixes without hidden repository mutations
  • missing tool binaries remain graceful no-ops
  • project-contextual tools are gated by explicit metadata when running them on standalone files would be noisy, slow, or conceptually wrong

The current automatic lint surface includes schema validation, spelling, and the supported file/path-scoped tools in the table below. That includes Python ruff, GitHub Actions actionlint and zizmor, and other registry-selected linters. Broader analyzers such as dependency vulnerability scanners, package audits, full type-check suites, and repository health commands belong in checkrun verify or in Sley's verify registry unless they can satisfy the same fast-check contract.

The registry encodes that policy with adapter executionScope metadata. Automatic lint selectors may only use file-scoped adapters; broader generic analyzers live behind checkrun verify, which accepts files or directories and chooses the owning project checks itself. This keeps Sley, editor adapters, and hooks from needing their own Go/Rust/C++ tool-selection tables.

Configs

Global fallback configs live under $XDG_CONFIG_HOME/checkrun when XDG_CONFIG_HOME is an absolute path, falling back to ~/.config/checkrun. Set CHECKRUN_CONFIG_DIR to override the complete config root for a run. The single config root is intentional: several backends, including Ruff, Biome, Rubocop, Rumdl, and Taplo, use one policy file for both formatting and linting.

Ignore policy can be phase-specific. ignore remains the backward-compatible all-phase skip. Use format-ignore to skip formatting only, lint-ignore to skip every lint phase, spell-ignore to skip only typos, schema-ignore to skip only schema validation, and tool-ignore to skip only the language- or filetype-specific backend linter. This lets vendored config data avoid formatting or spelling churn while still receiving structural validation.

Schema association policy defaults to associations.json in the Checkrun config root. Set CHECKRUN_SCHEMA_ASSOCIATIONS to point at a different policy file for a single run, test fixture, or integration harness. Local schema payload names resolve under $XDG_DATA_HOME/checkrun/schemas when XDG_DATA_HOME is absolute, falling back to ~/.local/share/checkrun/schemas; a policy can override that with schemaDataDir.

A missing policy means "no associations", which suits hosts that never configured one. A host that ships a policy can set CHECKRUN_SCHEMA_POLICY_REQUIRED=1 so that losing it fails loudly instead: planning then exits 2 when the policy is missing, and schema-lint exits 2 when its validation dependencies are unavailable and uv cannot provide them, rather than printing a notice and passing. Commit gates treat exit 2 as a failure, so a broken install cannot look like a clean run.

The policy is optional when no file exists. Once a policy file is present, it must be a regular file; symlinks resolving to regular files are accepted, while special files are rejected without reading. Checkrun validates the complete policy shape before planning, explaining, linting, refreshing, or producing editor configuration. Invalid JSON, unreadable files, and malformed associations fail with an actionable configuration error instead of silently disabling schema policy.

Normal schema validation never fetches association source URLs. schema-lint uses local schema payloads through schema_policy.schema_path() so hooks and CI remain deterministic offline. Remote $ref targets inside a payload are offline too: they resolve only from copies vendored beside it under refs/<host>/<path>, and a reference that was never vendored is a finding that names checkrun schema refresh as the fix rather than a network fetch. Portable LSP consumers can prefer public source URLs through the --lsp-schemas projection. VS Code and other editor-native consumers that can resolve custom schema schemes can add --editor-sources to prefer editorSource URLs such as vscode://schemas/settings/user.

Refresh pinned public schema payloads explicitly:

checkrun schema refresh
checkrun schema refresh --check
checkrun schema refresh --association "Ruff fallback config"

The refresh command selects associations that declare source and do not declare dependency; dependency-owned schemas are refreshed by their owning repos. Fetched payloads must be valid JSON, and when jsonschema is available they must also validate as JSON Schemas before replacing the pinned file. Each run also vendors every document the payload references remotely, transitively, under refs/ beside it; references are re-checked on every run because they can change while the payload does not, and a reference that cannot map to a safe local path (for example, one with a query string) fails the refresh. Writes are atomic and use a canonical generated JSON layout so future diffs are stable. --check reports drift without writing and is intended for scheduled CI in host repos that track schema payloads.

Implementation Layout

  • common.sh owns shared path normalization and shell helpers that adapters still need at execution time.
  • autoformat.sh owns formatter CLI behavior and adapter-id dispatch.
  • autolint.sh owns linter CLI behavior, diagnostic normalization, linter adapter-id dispatch, and read-only batching.
  • linters/*.sh owns the linter backend adapters grouped by domain: shell, web, build, config, languages, docs, and github-actions.

Each executable in bin/ is installed as a PATH-visible symlink (by install.sh or shdeps), so these entry points resolve their dependency libraries without relying on consumer wrapper scripts.

To add a formatter or linter, update the registry first:

  1. Add or reuse filetype inference under filetypes.
  2. If an editor uses a different language ID for that normalized filetype, add the alias under editorLanguageIds. Keep editor-specific naming there so VS Code, Neovim, hooks, and CLI planning share one Checkrun vocabulary instead of carrying separate translation tables.
  3. Add a selector for the normalized filetype, then add a step with the tool name, adapter id, and config policy when the tool has Checkrun-owned config discovery. Use step-level pathPatterns only on selector steps to narrow a tool within an already inferred filetype. Use requiresConfigMatch for tools that should run only when their config policy finds project metadata.
  4. Confirm lint steps satisfy the fast-check policy above, and mark their adapter with executionScope: "file" when they do. If the tool is project-wide, network-sensitive, or expected to be slow, add it under checkrun verify or a Sley verify registry instead of the automatic lint surface.
  5. Add the adapter id under adapters and implement the named shell function.
  6. Dispatch that adapter id from autoformat.sh or autolint.sh.
  7. Add registry-plan coverage plus adapter behavior tests.

Do not add a second filename or extension decision table in selectors or shell. The top-level filetypes table answers what the file is; selectors answer which tools apply to that normalized filetype; shell adapters only answer how to invoke that tool. Adapter helpers should return 0 when the underlying tool is missing, keep formatter failures advisory where appropriate, and preserve autolint's dynamically scoped fix and json behavior.

Tool Details

clang-tidy is intentionally gated: it runs only when the target file's parent chain contains compile_commands.json or compile_flags.txt. A nearby .clang-tidy configures rules once that compile context exists, but does not enable clang-tidy by itself. That keeps save-time editor lint quiet for standalone C/C++ files while still using project-owned rule and compile metadata when it exists.

Broad project analyzers are intentionally not listed as file linters. Run them explicitly with checkrun verify [PATH...] or --tool filters:

  • golangci-lint walks to go.mod roots and runs golangci-lint run ./... once per module.
  • govulncheck walks to go.mod roots and runs govulncheck ./... once per module.
  • cargo clippy walks to Rust project roots that have Cargo.toml, then runs cargo clippy --all-targets once per project.
  • cargo-audit walks to Rust project roots that have both Cargo.toml and Cargo.lock, then runs cargo audit once per project. The lockfile gate is intentional: dependency-audit results belong to locked project state, not standalone Rust source files.
  • Verify-time clang-tidy walks selected C/C++ files and directories while preserving the same compile_commands.json or compile_flags.txt metadata gate as fast lint.
  • Deleted or renamed file paths are still useful verify scope hints: Go/Rust paths select their nearest surviving module or project, while deleted C/C++ files select the nearest surviving non-root directory context.

Missing verification tools are no-ops, matching the rest of Checkrun's optional backend policy.

Basename-only files such as Dockerfile, BUCK, BUILD, TARGETS, WORKSPACE, MODULE.bazel, and Containerfile are dispatched before extension-based handling. Extensionless shell scripts are detected by shebang or dotfile name.

The shared shell library provides config walking helpers, shell classification, batched TOML key reads, nested-key config walks, and ignore-file matching for both autoformat and autolint.

License

MIT. See LICENSE.

About

One format/lint command that dispatches to 30+ tools: ruff, shfmt, biome, stylua, and more

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages