The whole tool reduces to one rule: pnpm check is the definition of done. Exit 0 means complete. Anything else means it is not.
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:
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.
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.
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.
Once set up, daily usage is one command regardless of the tool's name, because init writes a check script alias.
That runs the full pipeline — types, lint, structure, dead code, tests, docs, links, spelling — cheapest checks first. Exit 0 and you are done.
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, …).
Every run writes machine-readable output to .check/. The debugging path is always the same four steps.
--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".
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.
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.
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:
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:
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.
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 →
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.
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.
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.
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".
Replace the pnpm steps; the checkride invocation is identical.
The security slot (pnpm audit) is pnpm-specific and reports itself unavailable under the others.
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.
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.
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.
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.
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.