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.
| 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.
Requirements:
- Bash.
- yq v4.
autoformatandautolintexit with an error without it. - Python 3.11+ (for stdlib
tomllib). checkrun triesCHECKRUN_PYTHON, thenpython3onPATH,/usr/bin/python3,/opt/homebrew/bin/python3, and/usr/local/bin/python3. macOS's stockpython3is too old. jqforautolint --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 | bashThis 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.shThe 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.
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.pyautolint 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.
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.
- Bash for the CLI entry points and shell libraries.
yqis required byautoformatand byautolintonce at least one lintable file remains after filtering.jqis required byautolint --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_PYTHONto a compatible interpreter when the host defaultpython3does not include stdlibtomllib. shdepsis 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/ 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.
bin/checkrun,bin/autoformat, andbin/autolintare the PATH-visible CLIs.share/checkrun/registry.jsonis the shared tooling registry. It drives filetype inference, formatter/linter selection,checkrun plan,checkrun explain, editor language-ID aliases, and the derivedcheckrun capabilities --jsonoutput.share/checkrun/schemas/registry.schema.jsonvalidates the registry shape; the internallib/checkrun/registry.pyinterpreter enforces cross-object invariants that JSON Schema cannot express cleanly.lib/checkrun/schemas/schema_policy.pyis the public schema association API shared by editors and linting.--lsp-schemasemits 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 throughshdeps dep-file. Associations may seteditorSourcewhen 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.luais 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. Itseditor_metadata(opts)API also materializes the portablecheckrun editor-metadata --jsoncontract 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.pyvalidates files through that policy.lib/checkrun/schemas/schema_refresh.pyrefreshes pinned public schema payloads from associationsourceURLs. It is exposed ascheckrun schema refreshso host repos can run the same updater directly or from scheduled CI.lib/checkrun/verify.pyruns 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.jsonis the JSON Schema for schema association policy files.share/checkrun/schemas/diagnostics.schema.jsonis the JSON Schema for one newline-delimited diagnostic emitted byautolint --json. Editor adapters use this producer-owned contract when translating Checkrun diagnostics into their native diagnostic APIs.share/checkrun/schemas/shfmt-config.schema.jsonis the JSON Schema for the fallback TOML options that Checkrun translates into shfmt arguments when a project has no EditorConfig.share/checkrun/shell.shis 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 --jsonThe 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.
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 normalizescheckrun capabilities --json.filetypes(opts)converts Checkrun custom filetype metadata intovim.filetype.addinput.add_filetypes(opts)registers those filetypes and returns the registered table.json_schemas(opts),yaml_schemas(opts), andtoml_schema_associations(opts)return the corresponding LSP schema maps.yaml_before_init(opts)returns a yamllsbefore_initcallback 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.
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.
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.
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.
common.showns shared path normalization and shell helpers that adapters still need at execution time.autoformat.showns formatter CLI behavior and adapter-id dispatch.autolint.showns linter CLI behavior, diagnostic normalization, linter adapter-id dispatch, and read-only batching.linters/*.showns the linter backend adapters grouped by domain:shell,web,build,config,languages,docs, andgithub-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:
- Add or reuse filetype inference under
filetypes. - 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. - 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
pathPatternsonly on selector steps to narrow a tool within an already inferred filetype. UserequiresConfigMatchfor tools that should run only when their config policy finds project metadata. - 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 undercheckrun verifyor a Sley verify registry instead of the automatic lint surface. - Add the adapter id under
adaptersand implement the named shell function. - Dispatch that adapter id from
autoformat.shorautolint.sh. - 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.
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-lintwalks togo.modroots and runsgolangci-lint run ./...once per module.govulncheckwalks togo.modroots and runsgovulncheck ./...once per module.cargo clippywalks to Rust project roots that haveCargo.toml, then runscargo clippy --all-targetsonce per project.cargo-auditwalks to Rust project roots that have bothCargo.tomlandCargo.lock, then runscargo auditonce per project. The lockfile gate is intentional: dependency-audit results belong to locked project state, not standalone Rust source files.- Verify-time
clang-tidywalks selected C/C++ files and directories while preserving the samecompile_commands.jsonorcompile_flags.txtmetadata 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.
MIT. See LICENSE.