Skip to content

Repository files navigation

Unified Check Command

uncheck is a single command that lints, formats, type checks your project and keeps a monorepo consistent. It detects which tools the project already has β€” oxlint and oxfmt for linting and formatting, tsc for types, sherif for workspaces β€” and runs them together, so humans, coding agents and git hooks have one command to remember instead of four.

Usage

npm i -D uncheck   # Node 22.20+ or 24.8+, for stable path.matchesGlob

npx uncheck                 # sherif, oxlint, oxfmt --check and tsc for everything under the current directory
npx uncheck --fix           # sherif --fix and oxlint --fix, then rewrite formatting with oxfmt
npx uncheck src/app         # check some files: paths, directories, globs and !exclusions
npx uncheck --skip=tsc      # skip a check that would otherwise run
npx uncheck --only=oxlint --only=oxfmt   # run only the named checks, here the fast ones
npx uncheck --require=oxfmt # require a check: fail when it cannot run instead of skipping it
npx uncheck --cwd packages/app   # run in another directory, paths are relative to it

Checks run in order and every check runs even if an earlier one fails, so one run reports everything. The exit code is non-zero when any check fails.

Check Runs when Command
sherif sherif is installed and the directory is a workspace root sherif, or sherif --fix --select=highest with --fix
oxlint oxlint is installed oxlint [--fix] [files...]
oxfmt oxfmt is installed oxfmt --check [files...], or oxfmt [files...] with --fix
tsc at least one tsconfig.json is found tsc -b for projects using references, tsc -p for the rest

Paths given on the command line are resolved by uncheck itself into one list of files that every tool receives, so tools never disagree about what a directory or glob means: a file must exist, a directory expands to the project files below it (ignored files stay out, like git ls-files), globs use the usual **/* syntax and !pattern excludes. A path that matches nothing fails the run, unless --no-error-on-unmatched-pattern is passed.

Tools are resolved from node_modules the way Node does, so the versions your project already depends on are used. A tsconfig.json without typescript installed is reported as a failure rather than silently skipped.

Monorepo consistency

sherif lints a monorepo as a whole: dependency versions that differ between packages, unordered dependencies, a missing packageManager field and so on. It runs when the directory is a workspace root (workspaces in package.json or a pnpm-workspace.yaml) and, when paths are given, only if a package.json or pnpm-workspace.yaml is among them, since nothing else changes its verdict. Its options are read from the sherif field of the root package.json as sherif documents, so rules and dependencies to ignore live there. With --fix sherif also runs your package manager's install afterwards, unless that field sets noInstall; aligning versions takes the highest one unless the field sets select, since choosing interactively needs a terminal. sherif refuses to fix anything in CI, so with CI set it only checks.

Typecheck in monorepos

Every tsconfig.json in the project is discovered (through git ls-files, so ignored folders are skipped) and its references are followed recursively to build the project graph:

  • Projects that use references, or are referenced, are built with tsc -b on the roots of that graph. tsc builds the referenced projects first, in dependency order, exactly like running tsc -b in each package.
  • Remaining standalone projects (for example a root tsconfig.json that only covers tests and scripts) are checked afterwards with tsc -p.
  • Circular references are reported as an error.

When files are given, tsc runs only the projects it would actually check for them: a file selects the projects whose files, include and exclude (with extends applied) take it as input, so a test file excluded by its package config but included by the root config runs the root project only. Files tsc never checks, such as Markdown or CSS, select no project.

Presets

uncheck/oxlint, uncheck/oxfmt and uncheck/tsconfig export presets. middleapi is the one the middleapi projects share:

// oxlint.config.ts
import { defineConfig } from 'oxlint'
import { middleapi } from 'uncheck/oxlint'

export default defineConfig({ extends: [middleapi] })
// oxfmt.config.ts
import { defineConfig } from 'oxfmt'
import { middleapi } from 'uncheck/oxfmt'

export default defineConfig({ ...middleapi })
// tsconfig.json, for Node.js code that is only type checked
{
  "extends": "uncheck/tsconfig/middleapi",
  "compilerOptions": { "types": ["node"] },
  "include": ["src"],
}
// packages/*/tsconfig.json, for a Node.js package that emits its declarations to dist
{
  "extends": "uncheck/tsconfig/middleapi/lib",
  "compilerOptions": { "types": ["node"] },
  "include": ["src"],
}

The tsconfig presets target ES2022 and load no runtime types, so name yours: "types": ["node"] for Node.js, or "lib": ["ES2022", "DOM", "DOM.Iterable"] for browsers.

Agent hooks

npx uncheck hooks install                   # pick agents interactively
npx uncheck hooks install claude cursor     # or name them: claude, codebuddy, cursor, windsurf, copilot
npx uncheck hooks install claude --only=oxlint --only=oxfmt   # a fast hook: lint and format, no typecheck

This writes the agent's hook config (.claude/settings.json, .codebuddy/settings.json, .cursor/hooks.json, .windsurf/hooks.json or .github/hooks/uncheck.json), merging into an existing file so other hooks are kept. Whenever the agent finishes a turn, the hook runs:

uncheck hooks run --fix

through your package manager (pnpm exec, yarn, bunx or npx, detected from the lockfile). It runs every check on the files changed since the last commit (modified, staged and untracked, everything under the directory outside git), applies fixes, and prints the report on stderr. When problems remain, the agent is sent back to fix them before it finishes: Claude Code and CodeBuddy through exit code 2, Cursor through a follow-up message, Copilot through a block decision. That happens at most once per turn, so an agent that cannot fix something is never trapped in a loop. Windsurf only shows the report.

Running once per turn instead of after every edit keeps the agent fast: a typecheck costs seconds, and one run per turn covers everything the agent touched. When even that is too slow for a project, leave the typecheck to CI: --only, --skip and --require given to install are written into the hook command as they are, and reinstalling with other flags updates it, so install claude --only=oxlint --only=oxfmt gives a hook that only lints and formats.

Pre-commit hook

npx uncheck staged          # check the staged files only, what a pre-commit hook should run
npx uncheck staged --fix    # also apply the fixes and stage them
npx uncheck prepare --pre-commit   # write .git/hooks/pre-commit so every commit runs `uncheck staged --fix`

staged runs the checks on the files staged for commit. The unstaged hunks of partially staged files (git add -p) are set aside while the checks run, so what oxlint and oxfmt see is what gets committed, then put back. The typecheck works differently by nature: tsc checks whole projects, so it also reports type errors in files you have not staged. Add --only=oxlint --only=oxfmt for a hook that never looks beyond the commit. With --fix the fixes are staged too. When a fix conflicts with an unstaged hunk, the fixes are undone and the commit fails, so nothing is ever lost: stage the whole file or stash its unstaged changes and commit again. A run killed before it puts them back leaves copies of the files in the git directory, and the next run stops and says where they are. When the fixes undo every staged change, the commit fails rather than recording an empty one, unless --allow-empty is passed.

prepare --pre-commit replaces lint-staged and simple-git-hooks: it writes the git hook itself, running uncheck staged --fix through your package manager, and adds itself to an existing pre-commit hook rather than replacing it. With husky 9 or Vite+ (vp config) managing the hooks, it writes to .husky/pre-commit or .vite-hooks/pre-commit, the file their dispatcher runs, rather than to the generated shim in _/, which never reaches an added line and is rewritten on the next install. Running it again only ever rewrites the line it wrote itself, so lines you added by hand stay, a repeated copy of its own line is dropped, and in a monorepo each package that prepares gets its own line with its own flags. Without a flag prepare sets nothing up. Register it as the prepare script so every clone installs the hook:

{
  "scripts": {
    "prepare": "uncheck prepare --pre-commit"
  }
}

--only, --skip and --require given to prepare are written into the hook command as for hooks install, --no-fix gives a hook that only checks, and --allow-empty one that lets through a commit the fixes made empty. Outside a git repository prepare does nothing, so installs in CI and Docker builds keep working, and git commit --no-verify skips the hook.

In a monorepo where the root and two packages prepare, the hook reads:

#!/bin/sh
# Written by `uncheck prepare`, run it again to change the command.
pnpm exec uncheck staged --fix || exit 1
(cd "packages/a" && pnpm exec uncheck staged --fix --only=oxlint) || exit 1
(cd "packages/b" && pnpm exec uncheck staged --fix) || exit 1

Every line ends in || exit 1, so any failing check blocks the commit, and a package is entered in a subshell, so each line starts from the top of the working tree. A line you add by hand runs as you wrote it: give it || exit 1 too if its failure should block the commit.

Sponsors

Like what we build over at middleapi? You can help keep it going through GitHub Sponsors or Open Collective. Every bit helps! πŸš€

ScreenshotOne.comScreenshotOne.com
The screenshot API for developers
YuzuYuzu
We're hiring NYC based engineers
MisskeyHQMisskeyHQ
Decentralized microblogging SNS born on Earth

Special Sponsors

Guillermo Rauch
Guillermo Rauch

Premium Sponsors

Nexa
Nexa

Organization Sponsors

LN Markets
LN Markets

Sponsors

Reece McDonald
Reece McDonald
あわわわとーにゅ
あわわわとーにゅ
nk
nk
supastarter
supastarter
Dexter Miguel
Dexter Miguel
herrfugbaum
herrfugbaum
Ryota Murakami
Ryota Murakami
David Cramer
David Cramer
Valerii Petryniak
Valerii Petryniak
Valerii Strilets
Valerii Strilets
Kyle Mistele
Kyle Mistele
christ12938
christ12938
Ryan Soderberg
Ryan Soderberg
shota
shota
Ellis Driscoll
Ellis Driscoll
Hoang Nguyen
Hoang Nguyen
Orestis Ioannou
Orestis Ioannou
Stefan Smiljkovic
Stefan Smiljkovic

Backers

David Walsh
David Walsh
IPv4Addr
IPv4Addr
Robbe Vaes
Robbe Vaes
Aidan Sunbury
Aidan Sunbury
soonoo
soonoo
Kevin Porten
Kevin Porten
Denis
Denis
Christopher Kapic
Christopher Kapic
Tom Ballinger
Tom Ballinger
Sam
Sam
Titoine
Titoine
Igor Makowski
Igor Makowski
hanayashiki
hanayashiki
Lev Dubinets
Lev Dubinets
Kelly Peilin Chan
Kelly Peilin Chan
Guy Ariely
Guy Ariely
PaulSenon
PaulSenon
Alex
Alex
Andrey Gubanov
Andrey Gubanov

With thanks to 36 past sponsors who helped get us here.

License

Distributed under the MIT License. See LICENCE for more information.

About

Unified check command that lints, formats, and type checks in one command for your project

Resources

Code of conduct

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages