One command to lint, format check and type check your project, and keep a monorepo consistent. uncheck runs the oxlint, oxfmt, tsc and sherif you installed, so you, your git hooks and your coding agents all run the same check.
npx uncheck init # set up your project, step by step
npx uncheck # check everything
npx uncheck --fix # fix what can be fixed, report the restYou need Node 22.20 or later and a package.json. In your project folder, run:
npx uncheck init # or pnpm dlx, yarn dlx, bunxinit asks up to four quick questions. Space toggles a choice and Enter confirms.
- Which tools to install: oxlint and oxfmt, plus sherif at a workspace root. It lists the ones you're missing, all selected.
- Which tools get a preset config: opt-in, nothing is preselected.
- Whether to check every commit: adds a pre-commit hook.
- Which coding agents run it after each turn: agents whose config folder you have, such as
.claudeor.github/hooks, are preselected.
Then it installs everything with your package manager and sets it up:
$ npx uncheck init
uncheck init in /home/me/my-app
✔ Which tools should uncheck install? … oxlint, oxfmt
✔ Which tools should get a config from the middleapi preset? … oxlint, oxfmt
✔ Check the staged files before every commit? … yes
✔ Which agents should run uncheck when they finish a turn? … Claude Code
▶ npm install --save-dev uncheck oxlint oxfmt
✔ oxlint oxlint.config.ts created
✔ oxfmt oxfmt.config.ts created
✔ package.json scripts check, fix and prepare written
✔ pre-commit .git/hooks/pre-commit created
✔ Claude Code .claude/settings.json created
Run npm run check to check the project, and npm run fix to fix what can be fixed.
That's it! TypeScript is up to you: tsc joins in once you install it and add a tsconfig.json. init keeps any check or fix scripts you already have, and running it again only sets up what's missing.
Set up without questions
Without a terminal, npx uncheck init --yes takes the default answers: it installs the missing tools, checks every commit, and sets up the agents whose config folders exist. It writes no preset config.
Or set up by hand: npm i -D uncheck oxlint oxfmt typescript (plus sherif in a monorepo), then add the scripts "check": "uncheck", "fix": "uncheck --fix" and prepare.
Run npx uncheck, or npm run check, locally and in CI. Every check runs, even after one fails, so one run shows you every problem:
$ npx uncheck
uncheck in /home/me/my-app
○ sherif skipped, not installed
▶ oxlint --ignore-pattern=node_modules --no-error-on-unmatched-pattern
✔ oxlint passed 67ms
▶ oxfmt --check --no-error-on-unmatched-pattern
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
A check runs only when your project uses its tool, with the version and config you already have:
| Check | Checks | Runs when |
|---|---|---|
sherif |
monorepo consistency | sherif is installed (needs 1.10+), at the workspace root |
oxlint |
lint rules | oxlint is installed (needs 1.60+) |
oxfmt |
formatting | oxfmt is installed |
tsc |
types | the project has a tsconfig.json |
uncheck exits with code 1 when a check fails, when no check could run, or when there's a tsconfig.json but no TypeScript, so a broken setup never passes quietly.
npx uncheck --fix # or npm run fixThis applies oxlint's fixes, rewrites the formatting with oxfmt, and applies sherif's fixes, after which sherif runs your install. Type errors are yours to fix.
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 folderYou can repeat --only, --skip and --require, and the commit and agent hooks take them too. Run npx uncheck <command> --help to see every flag.
npx uncheck src/index.ts src/cli.ts # files
npx uncheck src/app # a folder
npx uncheck 'src/**/*.test.ts' # a glob, quoted so your shell leaves it alone
npx uncheck src '!src/generated' # a folder, minus a part of it
npx uncheck '!**/*.gen.ts' # everything except some filesEvery tool gets the same file list, so they never disagree about what a path means.
- Folders and globs match the files git knows about: tracked, or new and not ignored.
- A path that exists is never read as a glob, so
'app/[id]/page.tsx'just works. - A path that matches nothing fails the run, unless you pass
--no-error-on-unmatched-pattern. - tsc checks only the projects that include those files, and the projects that depend on them. sherif runs only when a
package.jsonorpnpm-workspace.yamlis among them.
Said yes to this in init? You're all set. (If you ran init before git init, run npm run prepare once to write the hook.)
Otherwise, run npx uncheck init again and say yes, or add the prepare script yourself and run npm run prepare once. Every clone then gets the hook on install:
{
"scripts": {
"prepare": "uncheck prepare --pre-commit"
}
}Already have a prepare script? Chain them: "prepare": "husky && uncheck prepare --pre-commit".
Now every commit runs uncheck staged --fix, no lint-staged needed. It checks your staged files, fixes what oxlint and oxfmt can, and stages those fixes:
uncheck staged in /home/me/my-app
○ sherif skipped, not installed
▶ oxlint --fix --no-error-on-unmatched-pattern src/y.ts
✔ oxlint passed 111ms
▶ oxfmt --no-error-on-unmatched-pattern src/y.ts
✔ oxfmt passed 106ms
○ tsc skipped, no tsconfig.json found
✔ staged the fixes to src/y.ts
✔ all checks passed (oxlint, oxfmt)
A failing check stops the commit. In a hurry? git commit --no-verify skips the hook.
To change the hook, add flags to the prepare script and run it again, for example "prepare": "uncheck prepare --pre-commit --only=oxlint --only=oxfmt". A flag you pass only by hand is undone by the next install.
| Flag | Effect |
|---|---|
--no-fix |
Only check, never change your files |
--allow-empty |
Let a commit through when the fixes undo every staged change |
--only, --skip, --require |
Pick the checks the hook runs |
Your work stays safe. After git add -p, the unstaged part of a file is set aside during the checks and put back afterwards, even if you press Ctrl-C. If a fix clashes with your unstaged changes, every fix is undone and the commit stops.
Good to know:
- tsc checks whole projects as they are on disk, so it can report errors in files you didn't stage, or pass thanks to one you forgot to
git add.--only=oxlint --only=oxfmtkeeps the hook fast and limited to what you staged. - Yarn 2+ doesn't run
prepare, so usepostinstall(initdoes this for you). In a package you publish, turnpostinstalloff while packing, for example with pinst. - Production installs that skip devDependencies (
npm ci --omit=dev,NODE_ENV=production) still run the script, but without uncheck. Append|| exit 0so they pass:"prepare": "uncheck prepare --pre-commit || exit 0".
More about the pre-commit hook
- An existing shell hook is kept, and uncheck adds its own line. With husky 9 or Vite+, it writes the
pre-commitfile they run. - When uncheck can't safely write the hook, for example outside git, with a global
core.hooksPathor when the existing hook isn't a shell script, it says why and your install keeps working. - Removing uncheck? Also delete its line from the
pre-commithook and its entries in your agent configs, or they fail.
Let your coding agent clean up after itself. Picked your agents in init? You're all set. Otherwise, add uncheck to your project (npm i -D uncheck, the hook never downloads it) and pick your agents from the list:
$ npx uncheck hooks install
✔ Which agents should run uncheck when they finish a turn? … Claude Code
✔ Claude Code .claude/settings.json created
The hook runs npx --no uncheck hooks run --fix whenever the agent finishes a turn.
In a script, name them instead: npx uncheck hooks install claude cursor.
| 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 |
When the agent finishes a turn, the hook checks the files changed since the last commit and fixes what oxlint and oxfmt can. If problems remain, it sends the agent back to fix them, but never twice in a row, so an agent can't get stuck in a loop. Outside git, or before the first commit, it checks and fixes the whole folder.
- Too slow or noisy? tsc checks whole projects, so it can flag type errors the agent didn't cause. Leave type checks to CI:
npx uncheck hooks install --only=oxlint --only=oxfmt. Install again to change the flags. - Avoid double runs. Cursor and Copilot CLI also run the hooks in
.claude/settings.json. If you set upclaude, addcursororcopilotonly where they don't read that file. - Your config is kept. Other hooks and settings stay, but comments in the file are lost.
Run both init and uncheck at the workspace root: the folder whose package.json has workspaces or whose pnpm-workspace.yaml lists packages. One run checks every package.
sherif checks the workspace as a whole, so it runs only at the root. Configure it in the sherif field of the root package.json, as sherif documents. It only reports in the hooks and when CI is set, so run npx uncheck --fix locally to apply its fixes. Leave out "fix": true, or every run that only reports fails.
TypeScript. uncheck finds every tsconfig.json and follows their references, so a config with another name, like tsconfig.app.json, is checked when a reference leads to it. Projects linked by references are built together with one tsc -b, and the rest are checked with tsc -p --noEmit.
Hooks. The root's pre-commit hook already covers every package. A package whose own prepare script runs uncheck prepare --pre-commit adds a line with its own flags, which also runs when a commit touches that package. An agent hook installed in a package checks only that package; add uncheck to the workspace root too if the agent may work outside it.
How uncheck runs tsc
tsc -bwrites what your configs ask for, such as declarations. A project in a references graph that would write JavaScript next to its sources, like thetsconfig.node.jsonof older Vite templates, is checked withtsc -p --noEmitinstead, and so are the projects that reference it. If git ignores that JavaScript, the project stays intsc -b.- In a folder without its own
tsconfig.json, such as a package, uncheck uses the nearesttsconfig.jsonabove it in the same git repository, if that one includes files of the folder.
uncheck ships the lint, format and TypeScript configs the middleapi projects share. They're optional, and init can write the oxlint and oxfmt ones for you. They need oxlint 1.70+, oxfmt 0.43+ and TypeScript 5.6+. Copying one by hand? Name the file .mts unless your package.json has "type": "module".
// oxlint.config.ts: oxlint's defaults plus a few rules that catch real bugs
import { defineConfig } from 'oxlint'
import { middleapi } from 'uncheck/oxlint'
export default defineConfig({ extends: [middleapi] })// oxfmt.config.ts: no semicolons, single quotes and sorted imports
import { defineConfig } from 'oxfmt'
import { middleapi } from 'uncheck/oxfmt'
export default defineConfig({ ...middleapi })The tsconfig presets include no Node.js or browser types, so add the ones you need: "types": ["node"] for Node.js, or "lib": ["ES2022", "DOM", "DOM.Iterable"] for browsers.
"✘ nothing to check". No check could run, and the message says why. Usually no tool is installed yet, so run npx uncheck init. A "○ nothing to check" line is only a notice, and the run passes.
A path looks like a command, a flag or an exclusion. Start it with ./: ./staged, ./-draft.ts, './!notes.ts'.
npx uncheck dist says "No files match". git ignores that folder. You can still name an ignored file directly.
"detected dubious ownership". git doesn't trust a repository another user owns, for example in a container mount, so the hooks can't run there. Run the safe.directory command that git status prints, then npm run prepare to write the hook.
A commit stops with "An earlier run left the unstaged versions of your files in …". A pre-commit run was killed, or couldn't put your unstaged changes back. Unless another commit is still running, copy back what your files are missing from that folder, delete it, 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 38 past sponsors who helped get us here.
Distributed under the MIT License. See LICENCE for more information.
MisskeyHQ