CHECKRIDE
DOCS
Home Agents 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 agent hooks), 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. Not behind your back, though: the begin marker carries a hash of what checkride wrote, so a stanza you have since edited stops the run (exit 2, nothing written) instead of being overwritten. Pass --force to discard the edits and refresh.

Make it a hard gate

checkride writes three agent hooks, for Claude Code and (when detected) Cursor. The stop-gate is the centerpiece: a generated script the harness fires when the agent tries to finish, which runs checkride gate and blocks the stop while the pipeline is red — feeding the message back so the agent keeps working until green. Beside it, dirty marks turns that actually edited (so a no-edit turn can skip the run), and protect denies edits to checkride.baseline.json and .check/ — "never add to the baseline to make a check pass" as enforcement, not advice. To add them to a repo you already set up:

shell
$ pnpm exec checkride agent-setup # alias + stanza + hooks (gate, dirty, protect)
$ pnpm exec checkride hooks add # hooks only — never touches AGENTS.md

Both commands are idempotent and honor --no-hook, --hook <a,b>, and --harness <a,b>. Under Claude Code the Stop entry points at the generated script:

.claude/settings.json
{
"hooks": {
"Stop": [{ "hooks": [{
"type": "command",
"command": "sh .claude/hooks/checkride-gate.sh"
}] }]
}
}

The script runs checkride gate, which runs <pm> run check --strict --digest under your detected package manager and answers in the harness's own protocol — under Claude Code, exit 2 with the verdict on stderr is what blocks (a plain exit 1 would be treated as non-blocking); Cursor instead reads a JSON reply on stdout. Three verdicts: green (the turn may end), red (blocked, naming what to read), and "could not run" — nothing executed, so it names the environment cause (usually the hook's Node vs an engines pin) instead of pretending the code is red. The fail-closed --strict run also 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 gate 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. Two levers tune the cost further: the dirty hook plus gate --if-dirty skip the run entirely on turns that edited nothing, and a gate key in checkride.config.json narrows what the stop hook runs (only, skip, changed) — every verdict then says so, so a narrowed green is never reported as a full one. 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.

Going deeper — the bundled /checkride:check and /checkride:qa skills, where Claude Code and Cursor genuinely differ, and what to do when the gate says "could not run" — has its own page: the plugin & harnesses →

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.

When a merge goes worse than that — entries deleted wholesale, or the file mangled past parsing — the run says so: a stderr warning for an unparseable file, and a hint: line on a red run naming the commit the "new" findings were grandfathered until. checkride recover then rebuilds candidate states from the file's git history and restores one additively — a plain working-tree edit to review and commit, never a git checkout. Don't fix that with checkride baseline: a re-capture grandfathers brand-new debt along with the lost entries.

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).
+A merge that silently drops baseline entries turns the next run into a wall of "new, not in baseline" red. The run prints a hint: line when those findings were grandfathered until a recent commit, and checkride recover restores them from git history additively. Both need history in the clone — actions/checkout defaults to fetch-depth: 1 — so recover locally, or deepen the checkout.
+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.