CHECKRIDE
API
Home Docs Languages Agents GITHUB
{{ grp.title }}
{{ it.label }}
Everything here is backed by a contract test — a change that breaks it is a breaking change.
CLI · REFERENCE

Commands

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.

{{ c.name }}
{{ c.desc }}
{{ c.icon }}
{{ c.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.

CLI · REFERENCE

Flags

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.

Run flags

FLAGEFFECT
{{ f.flag }} {{ f.effect }}

Environment variables

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.

VARIABLEEFFECT
{{ e.k }} {{ e.v }}

init flags

FLAGEFFECT
{{ f.flag }} {{ f.effect }}
CLI · REFERENCE

Exit codes

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".

0
Every executed check passed. Proceed — but see the vacuous-green rule below: ok: true with checks_run: 0 means nothing was verified.
1
At least one check failed. A verification failure: the work is not done. Read .check/summary.json, then the failing slot's raw output.
2
The harness itself broke or was misused. A malformed config, an unknown command or flag, an internal error — or --strict with zero checks run. Fix the invocation or environment; do not treat as "checks failed".

Vacuous green

"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.

Timeouts and interrupts

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.

OUTPUT · REFERENCE

The .check/ contract

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.

The files

FILECONTENTS
{{ f.file }} {{ f.contents }}

summary.json

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).

.check/summary.json
{
"schema_version": 1,
"timestamp": "…",
"ok": true,
"checks_run": 8, // checks that actually executed
"total_duration_ms": 4200, // wall-clock for the whole run
"run": { "partial": false }, // the flags given, and whether they narrowed the run
"git": { "head": "…", "dirty": false }, // the commit the checks read
"checks": [
{ "name": "lint", "adapter": "oxlint", "description": "…",
"ok": true, "exit_code": 0, "duration_ms": 470, "output_file": "lint.json",
"origin": "detected", "detected_by": ".oxlintrc.json",
"argv": ["pnpm", "--config.verify-deps-before-run=false", "exec", "oxlint", "--type-aware", "--format=json"],
"pin_source": "js-lockfile", "tool_version": "…" }
]
}

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.

The promises

Raw output stays authoritative
The per-tool files are the tool's own bytes — never normalized, reshaped, or filtered. This is the product's thesis and will not change.
Artifacts are crash-consistent
Everything is written temp-file-then-rename. A run killed at any point leaves each file previous-run-consistent or absent — never torn. Parse without guarding.
digest.md presence semantics
Written only under --digest and only when a check failed; a green run removes any stale digest. Its existence always means "this run had failures".
Deterministic ordering
The same selection produces the same checks array order on every run, even when concurrency interleaves completion.
CONFIG · REFERENCE

Configuration

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.

checkride.config.json
{
"extends": "@acme/checkride-preset", // inherit a shared preset, then override
"timeout": 1200, // global per-check timeout, seconds (default 600)
"changedBase": "origin/develop", // what --changed and {base} measure from
"checks": {
"format": "prettier", // enable an opt-in slot
"lint": "biome", // pick an alternate adapter
"spell": false, // disable a slot
"test": { "use": "vitest", "timeout": 0, "changedArgs": ["--changed", "{base}"] },
"tidy": { // custom check that runs FIRST
"command": "pnpm", "args": ["exec", "some-formatter", "--write"], "order": "first"
},
"licenses": { // custom check (runs last by default)
"command": "node", "args": ["scripts/check-licenses.mjs"]
}
}
}

Top-level keys

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.

KEYWHAT IT SETS
{{ k.k }} {{ k.v }}

Custom checks, detect & extends

+Custom checks (keyed by any non-slot name) run any command; the exit code participates in the gate like every built-in. They run after the catalogue by default; "order": "first" runs one ahead of everything — handy for a bespoke formatter. This is the hatch for a tool no pack covers: it becomes an entry, and one exit code gates the whole repo.
+A check entry takes use (an adapter) or command and args (where {out} expands to the check's artifact directory), plus description, note, outputFile (a .jsonl name keeps JSON lines), changedArgs (where {base} expands to the --changed base), fixArgs, timeout, order and optIn. A custom check adds detect, name and skipExitCode. Three slots take options of their own: profile for attw, exclude and allowlist for links, and exemplars for prose. static marks a check that reads the repo as data, and only a --defaults file may set it.
+"skipExitCode": 77 names the exit code that means "could not run here". A custom check that exits with it records as skipped, not failed, with its last stderr line as the reason, and is left out of checks_run. Pick a code the check never uses for a real failure.
+"detect": ["<file>", …] gates a custom check on marker files: it runs only when one exists, and is skipped, not failed, otherwise — so a shared config stays safe across repos that don't all use the same tools.
+"extends" inherits a file path or installed package (or an array, layered left to right; local config wins). Objects deep-merge; arrays and scalars replace outright. A preset's top-level tool pick that can't apply at the top level, where the repo declares targets or its root's pack lacks the tool, lands in that tool's packs.<pack>.checks instead. A missing base or a self-extending loop fails fast. One org-wide preset + detect = fleet policy without a campaign of PRs.

Ordering: waves

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.

ORDERTHE PROMISE
{{ o.name }} {{ o.promise }}
CONFIG · REFERENCE

The slot catalogue

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.

SLOT scope role blessed default alternates default run?
{{ r.slot }} {{ r.scope }} {{ r.role }} {{ r.tool }} {{ r.alts }} {{ r.def }}

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.

PROGRAMMATIC · REFERENCE

Programmatic API

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.

run-checks.ts
import { runChecks, type RunResult } from 'checkride';

const result: RunResult = await runChecks({ cwd: process.cwd(), strict: true });
process.exitCode = result.exitCode;

The result carries the same summary written to .check/summary.json, plus the exit code the CLI would return.

Functions & values

{{ x.name }}

Exported types

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.

{{ x.name }}

checkride/findings

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.

gating-findings.ts
import { readFile } from 'node:fs/promises';
import type { Summary } from 'checkride';
import { findingsFor, type Finding } from 'checkride/findings';

const dir = '.check';
const summary = JSON.parse(await readFile(`${dir}/summary.json`, 'utf8')) as Summary;
const gating: Finding[] = summary.checks
.flatMap((check) => findingsFor(check, dir, { repoRoot: process.cwd() }) ?? [])
.filter((finding) => finding.gates === true);

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.

Versioning & pin policy

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.

THE TESTED ENVELOPE
Every push runs unit, contract, and end-to-end suites (generated projects, installed and checked for real) on macOS and Linux, at Node 22.18.0 (the exact supported floor), Node 24 and Node 26, across pnpm, npm, yarn, and bun, and runs every language pack against its real tools, installed at pinned versions through mise. Beyond 70% line coverage, the suite is itself tested: Stryker mutation testing runs with a hard floor of 68, and the last full run scored 83%. Windows is not tested and not claimed — an honest "not yet" over an asserted "works everywhere".