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.
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 itChecks 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.
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.
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 withtsc -bon the roots of that graph.tscbuilds the referenced projects first, in dependency order, exactly like runningtsc -bin each package. - Remaining standalone projects (for example a root
tsconfig.jsonthat only covers tests and scripts) are checked afterwards withtsc -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.
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 })// 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.
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 typecheckThis 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 --fixthrough 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.
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 1Every 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.
Like what we build over at middleapi? You can help keep it going through GitHub Sponsors or Open Collective. Every bit helps! π
The screenshot API for developers |
We're hiring NYC based engineers |
MisskeyHQDecentralized microblogging SNS born on Earth |
Guillermo Rauch |
Nexa |
LN Markets |
With thanks to 36 past sponsors who helped get us here.
Distributed under the MIT License. See LICENCE for more information.
MisskeyHQ