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 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.
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.
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:
Both commands are idempotent and honor --no-hook. The generated hook uses your detected package manager (pnpm/npm/yarn/bun run check):
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.
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.
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.
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.