SOROUSH™
MODE

🧹 Moving From Prettier to oxfmt? Your Pre-commit Hook Breaks Quietly

🧹 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 setupoxc setup
prettier --write .oxfmt
prettier --check .oxfmt --check
pretty-quick --stagedoxfmt-quick --staged
.prettierrc.oxfmtrc.json (oxfmt --migrate=prettier generates it)
eslint-plugin-prettierdrop 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

FlagWhat it does
--stagedPre-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)
--checkReport without writing, exit non-zero if anything is unformatted
--bailExit non-zero if any file needed formatting
--no-restageFormat without re-staging
--config <path>Pass an oxfmt config file through
--verbosePrint 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

Cookie-Free by Design. The only cookies we like are the ones that come fresh from the oven.