uncheck lints, format checks and type checks your project with one command, and keeps a monorepo consistent. It runs the tools your project already has, so you, your git hooks and your coding agents all run the same check.
npm i -D uncheck oxlint oxfmt typescript # add sherif in a monorepo
npx uncheck # check everything
npx uncheck --fix # fix what can be fixed, report the rest
npx uncheck prepare --pre-commit # check every commit
npx uncheck hooks install claude # check every agent turnuncheck needs Node 22.20 or later. Install only the tools you want: a check runs when its tool is installed and is skipped otherwise, except that a tsconfig.json without TypeScript installed fails. uncheck always uses the versions you installed.
| Check | Checks | Runs when |
|---|---|---|
sherif |
monorepo consistency | sherif 1.10+ is installed, at the workspace root |
oxlint |
lint rules | oxlint 1.60+ is installed |
oxfmt |
formatting | oxfmt is installed |
tsc |
types | the project has a tsconfig.json |
$ npx uncheck
uncheck in /home/me/my-app
β sherif skipped, not installed
βΆ oxlint
β oxlint passed 67ms
βΆ oxfmt --check
Format issues found in above 2 files. Run without `--check` to fix.
β oxfmt failed 65ms
βΆ tsc -p tsconfig.json --noEmit
src/index.ts(1,14): error TS2322: Type 'string' is not assignable to type 'number'.
β tsc failed 384ms
β 2 of 3 checks failed: oxfmt, tsc
rerun with `--fix` to apply oxfmt fixes
Every check runs, so one run shows every problem. uncheck exits with 1 when a check fails, and also when no check could run, so a broken setup never passes quietly.
npx uncheck --only=oxlint --only=oxfmt # run only these checks
npx uncheck --skip=tsc # skip a check
npx uncheck --require=tsc # fail when tsc cannot run, instead of skipping it
npx uncheck --cwd packages/app # run in another directory--only, --skip and --require can be repeated and work on every command. Flags go after the command name (npx uncheck staged --fix), and npx uncheck <command> --help lists them all.
--fix applies oxlint's fixes, rewrites the formatting with oxfmt, and applies sherif's fixes, which runs your package manager's install afterwards. Type errors are yours to fix.
npx uncheck src/index.ts src/cli.ts # files
npx uncheck src/app # a directory
npx uncheck 'src/**/*.test.ts' # a glob, quoted so your shell leaves it alone
npx uncheck src '!src/generated' # a directory, minus a part of it
npx uncheck '!**/*.gen.ts' # everything except some filesuncheck turns your paths into one file list that every tool gets, so they never disagree about what a path means. Directories and globs match the files git knows about (tracked, or new and not ignored), dot files included. A path that exists is never read as a glob, so 'app/[id]/page.tsx' and 'app/(marketing)/**' just work. A path that matches nothing fails the run, unless you pass --no-error-on-unmatched-pattern.
tsc then checks only the projects that include one of the files, and sherif runs only when a package.json or pnpm-workspace.yaml is among them.
Add a prepare script, so every clone sets up the hook on install, and run it once now:
{
"scripts": {
"prepare": "uncheck prepare --pre-commit"
}
}Every commit then runs uncheck staged --fix: it checks the staged files, fixes what oxlint and oxfmt can, and stages those fixes. A failing check blocks the commit, and git commit --no-verify skips the hook. No lint-staged or simple-git-hooks needed.
prepare flag |
Effect |
|---|---|
--no-fix |
The hook only checks and never changes your files |
--allow-empty |
The hook lets a commit through when the fixes undo every staged change |
--only, --skip, --require |
Written into the hook command |
Run prepare again with other flags to change the hook. What it guarantees:
- You commit what was checked. After
git add -p, the unstaged part of a file is set aside while the checks run and put back afterwards, even after Ctrl-C. - Nothing is lost. If a fix clashes with your unstaged changes, every fix is undone and the commit stops. Stage the whole file, or stash the rest, and commit again.
- Only fixes to staged files are staged. During a merge, only files that differ from the branch being merged in are checked.
- No empty commits. If the fixes undo every staged change, the commit fails, unless you pass
--allow-empty.
Good to know:
- tsc checks whole projects, so it can report errors in files you did not stage.
--only=oxlint --only=oxfmtkeeps the hook inside the commit. - sherif only reports in the hook, since its fixes reach beyond the commit. Run
npx uncheck --fixfor them. - A commit no selected check covers, such as a README change with
--only=tsc, passes. Add--require=tscto make it fail. - An existing hook is kept: uncheck adds one line after its setup (comments,
source,export, variables) and before its commands. With husky 9 or Vite+, it writes thepre-commitfile they run. - uncheck writes nothing, and says why, outside a git repository, when
core.hooksPathcomes from your global or system git config, or when the existing hook is not a shell script. Your install keeps working. - The hook runs uncheck through your package manager (
pnpm exec,yarn run --silent,bunx --no-installornpx --no), so a missing install fails instead of downloading uncheck. - Yarn 2+ does not run
prepare. Usepostinstallinstead, and in a package you publish, turn it off while packing, for example with"prepack": "pinst --disable"and"postpack": "pinst --enable".
npx uncheck hooks install claude codebuddy # name the agents
npx uncheck hooks install # or pick them from a list| Agent | Name | Config file |
|---|---|---|
| Claude Code | claude |
.claude/settings.json |
| CodeBuddy | codebuddy |
.codebuddy/settings.json |
| Cursor | cursor |
.cursor/hooks.json |
| GitHub Copilot | copilot |
.github/hooks/uncheck.json |
Whenever the agent finishes a turn, the hook runs uncheck hooks run --fix. It checks the files changed since the last commit, fixes what it can, and when problems remain, sends the agent back to fix them. That happens at most once per turn, so an agent that cannot fix something is never stuck in a loop.
- Too slow? Leave the typecheck to CI:
npx uncheck hooks install claude --only=oxlint --only=oxfmt. Install again to change the flags. - Your config is kept. Other hooks and settings stay, and installing again only updates uncheck's entry. Comments in the file are lost when it is rewritten.
- Avoid double runs. Cursor and Copilot CLI also run the hooks in
.claude/settings.json, so addcursororcopilotnext toclaudeonly where they do not read that file. - Copilot reads
.github/hooksonly at the top of the repository, so installcopilotfrom there.
Run uncheck from the workspace root, the folder whose package.json has workspaces or that has a pnpm-workspace.yaml, to check the whole monorepo.
sherif checks the workspace as a whole, so it only runs at the root. Configure it in the sherif field of the root package.json, as sherif documents. With --fix, mismatched versions move to the highest one (unless you set select), and your install runs afterwards (unless you set "noInstall": true). When CI is set, sherif only reports.
TypeScript. uncheck finds every tsconfig.json and follows their references:
- Projects linked by
referencesare built with onetsc -b, which writes what your configs ask for, such as declarations. - Every other project is checked with
tsc -p --noEmit, a few at a time. - When only some files are checked, tsc runs just the projects that include them, and the projects that reference those. A changed tsconfig selects every project that extends it.
Hooks. Each package that runs uncheck prepare --pre-commit gets its own line in the one pre-commit hook, with its own flags:
#!/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 1An agent hook installed from a package folder checks only that package, wherever the agent moves to.
uncheck also ships the lint, format and TypeScript configs the middleapi projects share. They are optional.
// 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 })The presets need oxlint 1.70+, oxfmt 0.41+ and TypeScript 5.6+. The tsconfig presets load no runtime types, so name yours: "types": ["node"] for Node.js, or "lib": ["ES2022", "DOM", "DOM.Iterable"] for browsers.
A path looks like a command, a flag or an exclusion. Start it with ./: ./staged, ./-draft.ts, './!notes.ts'.
uncheck dist says "No files match". git ignores that folder, so it holds no project files. You can still name an ignored file directly.
A commit stops with "An earlier run left the unstaged versions of your files in β¦". A pre-commit run was killed before it could put your unstaged changes back. Copy what your files are missing from the folder the message names, delete the folder, and commit again.
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