CHECKRIDE
LANGUAGES
Home Docs Agents API GITHUB
{{ grp.title }}
{{ it.label }}
One repo with a TypeScript app, a Go API, OpenTofu and shell scripts, adopted from install to CI: the polyglot walkthrough →
LANGUAGE PACKS

Every language, its own tools.

checkride's slots — types, lint, dead, test and the rest — are roles, not tools. A pack fills them for one language: its adapters, where each tool is pinned, and the environment its checks run with. Seven ship: five languages beside TypeScript, and repo, which checks the repo as a whole.

In each card, a slot's first tool is its blessed default: the one detection prefers and init scaffolds. The others are alternates, which run when their own config file is the one the repo holds. A slot marked opt-in stays out of the default run until the config names it or --include does.

{{ pk.name }}
{{ pk.what }}
{{ pk.markers }}
{{ sl.slot }} {{ sl.tools }}
PINNED BY {{ pk.chain }}
FETCH FIRST {{ pk.setup }}

Grey tools are opt-in. publish is the ts pack's six-slot publish-ready bundle. terraform validate runs only where a .terraform.lock.hcl marks a root module, with terraform or tofu as the version file names. A bash target has no marker file: its checks run over its shell files, every .sh and .bash file and each extensionless file with a shell shebang, narrowed with include and exclude globs. Every adapter's argv is in Packs and pins.

One language needs no config

A repo whose root marks one language is checked as one project of that pack, with no checkride.config.json at all. The first match wins:

THE ROOT HOLDSCHECKED AS
{{ r.marker }} {{ r.pack }}

A Go, Rust, Python or Terraform root runs its pack's checks under their bare slot names (types, lint, test), from the root, into .check/, with that pack's tools, pin chain and environment, and a slot the pack doesn't fill sits out. A repo with code in more than one language, or one language in more than one place, declares targets.

Raw output, in every language

checkride never reshapes what a tool prints. golangci-lint's JSON, go test's JSON lines and shellcheck's json1 land in .check/ as the tools wrote them, and the agent reads them that way. A reader that wants findings by file and line imports checkride/findings, whose parsers cover 23 adapters across the packs. The baseline keys with the same parsers, so eslint, biome, golangci-lint, staticcheck, clippy, shellcheck, ruff and tflint findings can be grandfathered like oxlint's.

Next: Targets →
The findings API →
MIXED REPOS

One repo, many roots.

A repo with more than one language, or one language in more than one place, declares targets: named parts of the repo, each checked from its own root by the pack that knows its language.

checkride.config.json
{{ configSample }}

Each target runs the slots its pack fills, once, from its root, named <target>:<slot>. The repo pack's checks (struct, docs, links, spell, prose and unclaimed) still run once for the whole repo, under their bare names. Each line ends with why its check runs:

pnpm check (slots with no tool trimmed)
{{ runSample }}

What a target changes

+Each target writes under .check/<target>/, so two TypeScript targets' reports never overwrite each other.
+A target's slot takes its tool from its own checks, then packs.<pack>.checks, then the top level, then detection at its root.
+--only web runs one target's checks, --only web:lint one check, and --only lint every target's linter; --skip reads names the same way.
+--changed skips each target nothing changed under, measured from the merge base, and narrows a Go workspace to the modules a change touched and the ones that import them.
+Baseline keys sit under <target>:<slot> and name files from the repo root, so a key stays valid wherever the tool ran from.
+A ts target runs under the package manager nearest its root, and checkride fix runs each target's fixers from there.

Declaring them

checkride target add declares one, adopting the tools its root already configures. In a repo with no targets yet, the project at the root becomes a target too, named app, when the root holds one. init proposes the targets a repo's files suggest and writes them, and --discover, on a run or on doctor, shows the proposal without writing anything.

shell
$ pnpm exec checkride doctor --discover # what the files propose
$ pnpm exec checkride init # write the proposal
$ pnpm exec checkride target add scripts --lang bash --root scripts
READ THE PROPOSAL
A root package.json is one ts target only when the root runs a ts tool of its own: a tsconfig.json, a linter or test config beside it, or such a tool among its dependencies. That target claims every package below it, and its shell files too. A workspace root that only installs tools proposes each package below it instead, its package.json goes in unclaimed.ignore, and the scripts outside every package get a bash target of their own. The polyglot walkthrough adopts a repo like that, with the output at each step.

Code no target checks

With targets, the unclaimed check fails when a toolchain marker (a package.json, tsconfig.json, go.mod, go.work, Cargo.toml, pyproject.toml, setup.cfg, setup.py, .terraform.lock.hcl or a .tf file) sits outside every target of its language. .check/unclaimed.json names the target that would claim it, so a new service can't land unchecked: the fix is one target add, or a glob in unclaimed.ignore for a marker that holds no code to check.

Next: Pins →
Every targets rule →
TOOLCHAINS

The copy that runs.

A check never downloads a tool, and never picks one up by accident. Each pack resolves a tool through its pin chain: the repo's own pin file first, then a mise.toml, then PATH. The first source that declares the tool runs it, and when that copy can't run, the check fails naming the source rather than reach for another.

PACKPIN CHAIN
{{ r.pack }} {{ r.chain }}

Where each source looks

SOURCEDECLARES A TOOL WHENRUNS IT AS
{{ r.source }} {{ r.when }} {{ r.runs }}

Every check records the source that ran its tool as pin_source, the version when the source names one as tool_version, and the exact argv it spawned, so two runs that disagree can say why. checkride doctor shows the same per check, and lists each tool nothing pins with the mise use command that would.

requirePins

Set "requirePins": true in checkride.config.json and a tool only PATH provides fails its check instead of running whatever the machine holds:

an unpinned shellcheck
{{ pinRefusal }}

A toolchain is the exception: go, cargo and the Terraform or OpenTofu CLI still run from PATH, since the repo's own files set the version each accepts and the pack's environment stops it fetching another.

Offline by construction

Each pack's checks spawn with the variables that switch its downloads off, so a missing dependency fails the check rather than fetch, and the line under the red check names the step to run first:

PACKSET ON EVERY CHECKRUN FIRST
{{ r.pack }} {{ r.vars }} {{ r.first }}
Next: Without Node →
Packs and pins, in full →
TOOLCHAINS

No package.json required.

checkride runs on Node, but a Go, Rust, Python or Terraform repo doesn't have to carry one. mise installs checkride and a Node for it beside the repo's other tools, and runs it as mise exec -- checkride.

mise.toml
{{ miseToml }}
shell
$ mise use node@22.18.0 'npm:checkride[allow_low_downloads=true]@<version>'
$ mise trust # once per clone
$ mise install # every pin, node and checkride among them
$ mise exec -- checkride init # adopt the repo's tools, write the stanza and hooks
$ mise exec -- checkride # the gate

mise trust comes first. checkride's entry sets a tool option, allow_low_downloads, which mise only loads from a file it trusts, and which it needs before it installs an npm package with fewer than 1000 weekly downloads. init then adopts the repo through the pack its root marks, and writes the AGENTS.md stanza and the hooks, both spelled mise exec -- checkride. With no package.json, there's no check script to write.

A new Go or Rust project

init --lang go or --lang rust scaffolds one with no package.json: a mise.toml that pins checkride and every tool its checks run, a config with one target that names every slot and sets requirePins, the pack's dialect rules under rules/go/ or rules/rust/, and code that's green once its setup steps have run. The shapes and the steps are on the Getting started page.

A root with no marker

A repo whose languages all live in subdirectories has no go.mod, Cargo.toml, pyproject.toml or .tf file of its own at the root, and a mise.toml isn't a marker. init still reads it as an existing repo, from the projects discovery finds below the root, and declares a target for each, so the same five commands adopt it. mise exec -- checkride doctor --discover shows what it will propose before anything is written.

The gate, the hooks and CI

Where a root mise.toml pins npm:checkride and there's no package.json, the stop gate, the stanza and the plugin skills run mise exec -- checkride; with no pin they run the checkride on PATH, which doctor reports as the harness unpinned. A fresh clone's mise.toml starts untrusted, and the gate stands down naming mise trust rather than blocking; the agents page shows the verdict. In CI, jdx/mise-action installs every pin, and MISE_TRUSTED_CONFIG_PATHS set to the workspace trusts the repo's own file.