CHECKRIDE
DOCS
Home API GITHUB
{{ grp.title }}
{{ it.label }}
Looking for every flag, the summary schema, or the exports? API reference →
GET STARTED

Getting started

The whole tool reduces to one rule: pnpm check is the definition of done. Exit 0 means complete. Anything else means it is not.

Prerequisites

Three things outside the project: Node ≥ 22.18, a package manager (pnpm ≥ 9 is the default; npm, yarn, and bun work too), and git. Everything else — oxlint, ast-grep, fallow, vitest — is an ordinary project devDependency. Verify any time:

shell
$ pnpm exec checkride doctor

doctor is read-only: it checks Node/pnpm/git, that the project is installed, and reports every slot's status. Exit 0 means you are ready to run.

An existing repository

shell
$ pnpm add -D -E checkride
$ pnpm exec checkride init

init is additive: it inventories the tools you already have, writes only what is missing (a config, a check script alias, the AGENTS.md stanza, the Stop hook), gitignores .check/, and never overwrites an existing tool config. For a repo with real existing findings, prefer init --baseline — see the baseline. To scaffold a tool you don't have yet, name its slot: init --add struct,dead, then pnpm add -D @ast-grep/cli fallow.

A new project

shell
$ pnpm dlx checkride init --shape flat --name my-app
$ pnpm install
$ pnpm check

From an empty directory, init generates a complete repo, green out of the box. Pick a shape: flat (single package, deep-modules layout under src/), monorepo (a pnpm workspace of apps/* and libs/*), or hybrid (root app plus internal packages/*). Preview with --dry-run.

Next: The daily loop →
Working with agents →
NO LOCK-IN
checkride's whole footprint is the devDependency, two config files, the script alias, the stanza, and the hook. Remove them and every tool keeps working untouched — you've merely dropped the orchestrator. The worst case of adopting checkride is ending up where you already are.
DAILY USE

The daily loop

Once set up, daily usage is one command regardless of the tool's name, because init writes a check script alias.

shell
$ pnpm check

That runs the full pipeline — types, lint, structure, dead code, tests, docs, links, spelling — cheapest checks first. Exit 0 and you are done.

Narrow the loop while iterating

shell
$ pnpm check --bail # stop at the first failing check
$ pnpm check --only types,lint # run just the named slots
$ pnpm check --changed # affected-only mode

Run the full pnpm check once at the end to confirm green. Some failures have a one-shot fix — checkride fix runs every active adapter's fix command (oxlint --fix, markdownlint-cli2 --fix, …).

The aliases init writes

SCRIPTruns
{{ a.script }} {{ a.runs }}
DAILY USE

When a check fails

Every run writes machine-readable output to .check/. The debugging path is always the same four steps.

1
Read .check/summary.json — the aggregate report. Find the check whose ok is false.
2
Read that slot's raw output — .check/<slot>.json (for example .check/lint.json), or .check/<slot>.stdout.txt when the tool's output is not JSON. The raw file is the truth; the summary is an index.
3
Fix the root cause, not the symptom. Fixable lint/format/markdown errors have a one-shot answer: pnpm exec checkride fix.
4
Re-run pnpm check. Never claim a task is finished while it is red.

Big red repo? Use the digest

--digest writes .check/digest.md — a token-bounded Markdown excerpt of the failing slots (ten findings per slot, 8 kB overall, roughly two thousand tokens), each section linking the authoritative raw file. It truncates, never normalizes; a green run leaves no digest, so its presence always means "this run failed".

shell
$ pnpm check --digest

Quick triage

SYMPTOMDO THIS
{{ t.symptom }} {{ t.fix }}
ADOPTION

Working with agents

checkride is an agent harness: the goal of init is that a coding agent adopts the "exit 0 = done" rule on its own — and can't finish without it.

How agents pick it up

init writes the contract into two files. AGENTS.md gets a stanza (between <!-- checkride:begin --> / <!-- checkride:end --> markers) stating the rule, how to read .check/ on failure, the module-boundary conventions, and the tight-loop commands. CLAUDE.md gets a short pointer (only if you don't already have one). Claude Code, Codex, Cursor, and Amp load these at session start — the agent runs pnpm check because it read the instruction. That's guidance; the hook below is enforcement. Keep your own edits outside the markers — init rewrites the stanza in place.

Make it a hard gate

checkride writes a Claude Code Stop hook to .claude/settings.json. It fires when the agent tries to finish; exiting 2 blocks the stop and feeds the message back, so the agent keeps working until the pipeline is green. To add it to a repo you already set up:

shell
$ pnpm exec checkride agent-setup # alias + stanza + Stop hook, nothing else

Both commands are idempotent and honor --no-hook. The generated hook uses your detected package manager (pnpm/npm/yarn/bun run check):

.claude/settings.json
{
"hooks": {
"Stop": [{ "hooks": [{
"type": "command",
"command": "pnpm run check || { echo 'checkride: the gate is red — read .check/summary.json, fix the failing slot, then finish (do not stop while checkride is red).' >&2; exit 2; }"
}] }]
}
}

The || { …; exit 2; } wrapper matters: a plain run exits 1 on failure, which Claude Code treats as non-blocking and lets the agent stop anyway. Exit 2 is the code that blocks. The hook deliberately runs without --strict — it fails open locally so a misconfigured repo can't trap the agent; the fail-closed --strict run belongs in CI, which protects the branch.

Avoiding duplicate runs

The stanza and the hook both want pnpm check, so a naive setup runs the pipeline twice. The generated stanza already applies the simplest fix: when a Stop hook is configured, the hook owns the final full run — the agent iterates with the narrow commands and lets the hook run the authoritative pipeline once at the end. For an expensive suite, the most robust fix is a hook that verifies the artifact instead of recomputing: accept a complete, green summary.json newer than your sources, and only re-run when it's missing or stale. And don't delete the AGENTS.md block to dodge the duplicate — it also teaches the agent how to fix what the hook flags, and it's what non-Claude agents have.

ADOPTION

The baseline ratchet

Adopting checkride on an existing repo shouldn't be a cleanup project. A baseline grandfathers the diagnostics a repo has today so day-one runs pass, while any new diagnostic still fails — "don't make it worse" as the definition of done for legacy code.

shell
$ pnpm exec checkride baseline # record current diagnostics into checkride.baseline.json

checkride.baseline.json lives at the repo root and is committed. It records per-slot fingerprints — a stable file:rule:message key that survives line moves — not raw output. Adopting from scratch? checkride init --baseline does this as part of setup instead of disabling failing slots.

How the ratchet works

Old debt is masked
A slot is green when only baselined findings remain. The raw .check/<slot>.json still holds everything, and the check gains a "baselined": n count.
New debt fails
Current findings have the grandfathered ones subtracted; the slot fails listing only the new ones.
Fixed debt is pruned
A full green run drops fixed fingerprints from the file — it only ever shrinks, so debt can't silently creep back. Partial runs (--only, --changed, an early --bail) never prune.

The rules of the road

Never add to the baseline to make a check pass — fix the finding, or re-run checkride baseline deliberately and read the diff in the PR like code (it re-records everything currently failing, including brand-new debt). The file is canonical (sorted slots and keys), so parallel branches usually merge cleanly; when they conflict, keep both sides' entries and run a full checkride — the ratchet prunes anything already fixed, and a dropped entry that still fails simply resurfaces red.

Only slots with a fingerprint extractor participate — currently lint (oxlint), struct (ast-grep), spell (cspell), and the fallow slots dead/dupes/health. A crash or empty output is never masked: a slot only goes green when there are findings and all of them are grandfathered.

ADOPTION

Running in CI

One job, one command, exit 0 = done. Copy-paste for a pnpm repo on GitHub Actions — and always pass --strict in CI: a gate should never mistake "nothing ran" for "everything passed".

.github/workflows/check.yaml
name: check

on:
push: { branches: [main] }
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with: { node-version: 24, cache: pnpm }
- run: pnpm install --frozen-lockfile
# --strict: zero checks running is exit 2, never a silent pass
- run: pnpm exec checkride --strict
# on failure, keep the raw diagnostics for humans and agents
- if: failure()
uses: actions/upload-artifact@v4
with: { name: check-report, path: .check/ }

Notes

+Exit codes: 0 pass, 1 a check failed ("red build"), 2 the harness broke or was misused ("fix the pipeline"). A gate can branch on the difference.
+If the repo uses --changed locally, still run the full set in CI — CI is where the whole pipeline gets observed, which is also what lets a committed baseline ratchet.
+With a committed checkride.baseline.json, CI needs nothing extra: grandfathered diagnostics are masked, new ones fail, and a full green run prunes fixed entries (in the CI workspace only — the developer commits the same prune locally).
+Nothing here is GitHub-specific. On GitLab, CircleCI, Buildkite, Jenkins, or a bare shell: check out, install from the lockfile, run checkride --strict, gate on the exit code, and archive .check/ on failure.

Other package managers

Replace the pnpm steps; the checkride invocation is identical.

variants
# npm
- run: npm ci
- run: npx checkride --strict

# yarn
- run: yarn install --immutable
- run: yarn checkride --strict

# bun
- run: bun install --frozen-lockfile
- run: bunx checkride --strict

The security slot (pnpm audit) is pnpm-specific and reports itself unavailable under the others.

ADOPTION

Tools & installation

checkride has no runtime dependency on any tool it runs. For each slot it spawns <pm> exec <tool>, so the tools are ordinary project devDependencies, pinned and owned by your repository. "Installing a missing tool" means adding an npm package — not a system binary.

Slot tools

A slot runs only when its detect file is present (built-in checks always run). Note the npm package names that differ from the binaries: ast-grep ships in @ast-grep/cli, stryker in @stryker-mutator/core, attw in @arethetypeswrong/cli.

SLOT tool install with detect file
{{ r.slot }} {{ r.tool }} {{ r.install }} {{ r.detect }}

fallow, the unfamiliar name

A Rust-native codebase-intelligence tool (unused code, duplication, cycles, complexity, architecture drift). checkride splits it across three slots sharing one fallow.toml: dead (default), and opt-in dupes and health. Uniquely, checkride owns the pass/fail decision here: fallow's JSON modes exit 0 even with findings, so checkride reads the report and gates on the issue count — and an unrecognized report fails loudly rather than passing silently. Needs fallow ≥ 3.5.

The publish-ready bundle

Four opt-in library slots gate the artifact a consumer actually installs, with zero new devDependencies: build (wave 10) runs your build so the rest inspect fresh output; then pack (the tarball ships the right files), smoke (the built package imports cleanly, every export live), and snippets (tagged doc fences type-check) share wave 20 with publint and attw. Enable with --include build,pack,smoke,snippets or by naming them in config — the bundle orders itself.

When doctor reports a missing tool

shell
$ pnpm exec checkride init --add dead,struct # writes fallow.toml, sgconfig.yml, rules/
$ pnpm add -D fallow @ast-grep/cli # installs the tools
$ pnpm check # the slots now run

If the config exists but the package is gone (fresh clone, pruned node_modules), it's just pnpm install. A slot that's irrelevant to the repo is disabled in config — "spell": false — and doctor reports it as such instead of a failure.