Twelve commands. Invoke as pnpm check (the alias init writes), or directly with pnpm exec checkride <command>; in a repo without Node, whose mise.toml pins checkride, with mise exec -- checkride <command>. Click a command to expand its flags and an example.
Stream discipline: stdout carries machine output only (the summary JSON under --json; otherwise nothing). Human-readable progress goes to stderr. checkride --json | jq . is safe.
The promised flag set. A name in --only, --skip, or --include that matches nothing is a usage error (exit 2), never a silently-empty selection — a typo like --only lints can't quietly disable the gate.
Each of the first five stands in for its flag, and the flag wins when both are set. fix reads CHECKRIDE_CONFIG and ignores the rest; the gate ignores all five, so the stop hook always checks the repo as it is.
The exit codes are a taxonomy, not a number. The 1-vs-2 distinction is a promise: a gate may safely branch on "red build" versus "fix the pipeline". checkride doctor uses the same 0/1 split for "environment ok"/"environment has problems".
"Green because everything passed" and "green because nothing ran" are distinguishable by every consumer, three ways: summary.json carries checks_run; a zero-run gets a loud stderr warning naming why each slot sat out; and --strict turns a zero-run into exit 2. Anything that gates on checkride — CI, commit hooks, other tools — should run with --strict; a human exploring a fresh repo isn't punished by default. A custom check that exits its skipExitCode counts as skipped, not passed, so a run where every check skipped itself is a zero-run too.
Every check runs under a timeout by default — the check's own timeout, else the config-level one, else 600 seconds; 0 at any level disables the cap. A timed-out check is killed (SIGTERM, then SIGKILL after a short grace) and recorded as failed with a timed out after <n>s note — red, never vacuous, never hung. An interrupted run never tears an artifact and never prunes the baseline. The one uncapped catalogue default: the mutation slot, whose stryker and cargo-mutants adapters ship timeout: 0 — a real mutation run legitimately outlasts the cap, and it's opt-in, never in the gate the cap protects.
Every run writes to .check/ (gitignored). Treat it as a stable API for agents: the summary is an index, the raw file is the truth.
Versioned by schema_version (currently 1) and published as a JSON Schema that ships in the package. Additive-only discipline: under a given version, fields are added, never renamed, removed, or retyped — a consumer written against version 1 keeps parsing every 1.x summary. The checks array order is deterministic (run group sequence, not completion order).
Optional fields are present only when meaningful — absence is part of the shape. Per check: skipped and reason (why it sat out, the refusal that stopped it before its tool ran, or the output that failed a tool that exited 0), baselined (how many findings a baseline masked), narrowed (the Go workspace modules --changed narrowed it to), and origin, detected_by, argv, pin_source and tool_version, which say why it ran and which copy of the tool did. With targets, every check adds slot and pack, a target's check adds target and, below the root, cwd, and the summary adds a top-level targets list; a Go, Rust, Python or Terraform root records pack without them. git.base appears under --changed. The contract names every field and the rule for when it's present.
checkride.config.json is optional — add it only to deviate from the defaults. The "$schema" pointer turns on validation and autocompletion in editors; the schema ships in the package.
targets, packs, requirePins and unclaimed are the polyglot keys; Languages shows them at work. A key outside this list warns on stderr, naming the file that holds it — the root config, a preset, or a --config file — and the run goes on. A known key with a malformed value is exit 2.
Every check has an effective order — config entry, else adapter default, else slot default, else 'any'. Checks sharing a wave run concurrently through a bounded pool (--concurrency sizes it; --bail goes fully sequential). Numbering your own waves? Use gaps — 10, 20, 30.
A slot is a role in the pipeline (cheapest first); an adapter is the concrete tool that fills it. One blessed default per slot; alternates run if their config is detected, but init only scaffolds the blessed one. Opt-in slots join a run via --include, --all, or by being named in checks. A target slot checks one language's code, once for each target whose pack fills it; a repo slot checks the repo as a whole, once.
Zero-config: for each slot, checkride runs the first adapter it detects, by its config file or, for some, a package.json dependency or script, and skips slots with no detected tool. Package-manager-agnostic: it detects pnpm/npm/yarn/bun from packageManager or the lockfile and translates pnpm exec into that manager's form. Exceptions: security is pnpm-only; pack is npm/pnpm-only — both report themselves unavailable elsewhere rather than failing.
The table is the ts and repo packs' tools. The bash, go, rust, python and terraform packs fill the target slots with their own — golangci-lint, clippy, ruff, tflint, shellcheck and the rest — and each pack resolves its tools through its own pin chain. The Node-free alternates rumdl and typos are detected only where mise.toml or PATH can supply them: in a repo with targets, or one whose root marks another pack. Packs and pins lists every pack's slots, adapters and pin sources, and Languages puts them side by side.
The CLI is the primary interface, but every command is also a function. Everything exported from the package root is public and semver-bound; everything else is internal by definition, even if technically importable.
The result carries the same summary written to .check/summary.json, plus the exit code the CLI would return.
PACKS maps each pack to its adapter rows, root markers, pin sources and environment; TOOLS has a row per external tool, naming the npm package that provides it or the mise.toml keys that pin it. Both, like ADAPTERS and SLOTS, are plain data that survive a JSON round trip.
A second public subpath reads a run's findings by file and line without reimplementing checkride's parsers. parseFindings(adapter, raw, options?) parses one adapter's raw output; findingsFor(check, dir, { repoRoot }) opens a summary.json entry's own artifact, under <target>/ for a target's check; isParseable(adapter) says whether an adapter has a parser — 23 do today, and the set only grows. Each Finding names its file from the repo root, with line, column, rule, message, the tool's own severity, and gates: whether it can fail the check under the argv that ran. The subpath also exports the Finding, FindingsCheck and ParseOptions types.
null means no parser, a skipped check, an unreadable artifact, or output the parser can't trust; an empty array means parsed and clean. The API never writes under the output directory and never decides a verdict: the raw artifact stays authoritative, and the check's ok is still the verdict.
checkride follows semver. Pre-1.0, minor versions may break (the semver 0.x rule) — consumers should pin exactly ("checkride": "0.15.0", no caret; that's the -E in the install command) and upgrade deliberately. Post-1.0, a caret range is the intended usage. Any change touching a promised surface names itself in the CHANGELOG under a Contract heading — a reader scanning release notes finds every contract-relevant change without reading diffs.