🧹 Keeping Your Pre-commit Hook When You Move From Prettier to oxfmt
The Husky + pretty-quick setup from the previous article works great, and it will keep working for as long as you use Prettier. But recently I moved a project's formatter to oxfmt — the Rust formatter from the Oxc project — and found one loose end: pretty-quick runs Prettier, by name and by design. Swap the formatter and that piece of the hook needs a counterpart.
So I wrote one: oxfmt-quick.
npm i -D oxfmt oxfmt-quick
🐌 It turns out this is about correctness, not speed
I assumed this was a performance story at first, so let me save you the same detour.
With Prettier, scoping to staged files really is a speed win — process startup, module resolution and parsing every file in a JavaScript VM all add up. oxfmt is in a different league. On the monorepo where this started, it formats 1,554 files in about 860 milliseconds. Narrow that to the few files in a commit and you get ~300ms. That difference is invisible.
So if the whole repo is already that cheap, what is the staged-file mode actually for?
Re-staging. Git commits what is in the index, not what is on disk. A hook that runs plain oxfmt formats your working tree, and the commit happily records the older, unformatted version. The hook looks like it worked. The commit says otherwise.
Handling that is what pretty-quick has always quietly done for Prettier, and it is what oxfmt-quick does for oxfmt. Speed was always the bonus.
✨ Rewiring the hook
Husky itself carries over untouched — everything the previous article says about husky init and the prepare script still applies. Starting clean:
npm i -D husky && npx husky init
Then .husky/pre-commit is a single line:
npx oxfmt-quick --staged
That's the whole setup. Every commit formats its own staged files and re-stages the result, and a non-zero exit stops the commit so only formatted code lands.
Before this package existed, the same hook in my repo was nine lines of shell piping git diff into xargs into oxfmt into git add — hard to test, and quietly reliant on GNU xargs behaviour that differs on macOS. One line is better.
🎯 Two modes
oxfmt-quick # everything changed since the merge-base, plus untracked files
oxfmt-quick --staged # only the index, re-staged after formatting
The default mirrors pretty-quick: it looks at what you've changed on your branch. Reach for it at the terminal to tidy up the work in progress.
--staged is the pre-commit mode. It looks only at the index, since that's exactly what the commit will contain.
Both compare against the merge-base, so a feature branch that has fallen behind main stays focused on your own changes instead of reformatting everything someone else touched.
🛡️ The case worth knowing about
Here's the scenario that makes a formatting hook interesting.
You stage a file, then keep editing it. Now the index and your working tree hold different versions, and only the staged one is going into the commit.
A naive hook formats the file on disk and git adds it — which sweeps in every edit you deliberately left out. The commit quietly grows past what you intended.
oxfmt-quick formats the file, leaves it unstaged, tells you, and exits non-zero so you stay in control:
🎯 Found 3 changed files.
✍️ Fixing up src/hooks/useCopyToClipboard.ts.
✗ Found partially staged file src/utils/format.ts.
✗ Partially staged files were formatted but left unstaged, so this commit is not
widened with edits you did not stage. Stage them before committing.
Doing less is the right answer here: your unstaged work stays unstaged, and you decide what goes in. It's a real distinction between tools — there's a known bug in a comparable tool where --staged reads working-directory content instead of the staged blob.
🧩 What it leaves to oxfmt
oxfmt-quick has no config file, no ignore handling and no extension filter — all on purpose.
oxfmt already reads .oxfmtrc, .gitignore, .prettierignore and .editorconfig, and skips files it can't format. Reimplementing any of that would create a second source of truth that drifts. So the file list goes straight to oxfmt, and oxfmt decides. Gitignored files never show up regardless, because git diff only reports tracked files.
Two runtime dependencies: mri and picocolors. That's the lot.
🔁 Coming from the previous setup
The mapping is almost one-to-one:
| Prettier setup | oxc setup |
|---|---|
prettier --write . | oxfmt |
prettier --check . | oxfmt --check |
pretty-quick --staged | oxfmt-quick --staged |
.prettierrc | .oxfmtrc.json (oxfmt --migrate=prettier generates it) |
eslint-plugin-prettier | drop it — run oxfmt --check in CI instead |
That last row is worth a pause. Prettier's own docs suggest keeping formatting out of your lint rules: a dedicated --check step in CI is the cleaner, faster answer, and your editor stays free of formatting squiggles.
One practical tip for the switch: do the bulk reformat as a single standalone commit and add its SHA to a .git-blame-ignore-revs file, so git blame keeps pointing at the people who actually wrote each line.
⚙️ The flags
| Flag | What it does |
|---|---|
--staged | Pre-commit mode: index only, re-staged after formatting |
--since <rev> | Compare against a revision instead of the merge-base |
--branch <name> | Branch to find the merge-base against (default main) |
--check | Report without writing, exit non-zero if anything is unformatted |
--bail | Exit non-zero if any file needed formatting |
--no-restage | Format without re-staging |
--config <path> | Pass an oxfmt config file through |
--verbose | Print every file considered |
It's a library too, if you want to build on it:
import { oxfmtQuick } from 'oxfmt-quick'
const { success, errors } = oxfmtQuick(process.cwd(), {
staged: true,
onWriteFile: (file) => console.log(`formatted ${file}`),
})
Every callback is optional — the CLI is a thin reporting layer over that one function.
🪟 A note for Windows users
Two details matter here, and both are handled for you.
oxfmt on PATH is a .CMD shim on Windows, which Node needs a shell to spawn — and a shell brings back every quoting problem that passing an argument array exists to avoid. So oxfmt is located through its own bin script and run with the current node.
File lists are read with git diff -z and split on NUL, because Git escapes non-ASCII paths when it prints them newline-separated. Splitting on \n would corrupt those names; splitting on NUL keeps them intact.
Tested on Linux, macOS and Windows on every commit.
✅ Final thoughts
A pre-commit hook is a convenience, and CI is the authority:
- run: npx oxfmt --check .
Since the whole-repo run is now this cheap, that step is essentially free — which was never quite true of the Prettier equivalent.
If you've already moved to oxfmt, oxfmt-quick closes the last gap in the workflow. And if you're still on the setup from the previous article and eyeing the migration, the pre-commit story is ready for you.
- oxfmt formats, fast
- oxfmt-quick makes sure the formatting is what you actually commit
- Husky wires it up in one line
Install it, add one line to your hook, and enjoy never thinking about formatting again.
npm i -D oxfmt oxfmt-quick
📦 oxfmt-quick on npm · 🐙 Source on GitHub · MIT
Tags: oxc, oxfmt, prettier, pretty-quick, husky, pre-commit, clean code, typescript, javascript, formatting