Skip to content

Husky, lint-staged, or nano-staged? Understanding pre-commit tools

Messy code on your main branch is a classic headache that usually happens when you only run linters in CI.

3 min read
Husky
lint-staged
nano-staged
Git Hooks
DX

Messy code on your main branch is a classic headache that usually happens when you only run linters in CI. Inconsistent styles or silly bugs eventually slip into main, and they end up becoming a burden for the whole team.

The fix is using pre-commit hooks, but people often get the tooling mixed up. I see many people comparing Husky to lint-staged as if they're competitors, when they actually do very different things. It's like comparing a key to a lock; they work together rather than replacing one another.

Defining the roles

Husky is your Git hook manager. Its job is to make sure a specific command runs the moment you commit. In the Node.js ecosystem, Husky is basically the standard because it’s easy to set up and well-documented.

On the other hand, the job of running commands only on the files you've actually staged belongs to lint-staged or nano-staged.

A major mistake I often see in teams is running the linter against the entire project every single time someone commits. In a large monorepo, running eslint . on every commit is a disaster. It’s incredibly slow, and eventually, the team will just get tired of waiting and stop using the hooks altogether.

Choosing the right runner

If you want to run a linter only on selected files, lint-staged is usually the go-to. It’s smart enough to map out which files are currently staged. But there’s a catch: your linter script must be able to accept a list of files as arguments. If your script is hardcoded to lint the entire folder regardless, using a staged runner is a waste of time.

Then there's nano-staged.

nano-staged is a lighter, faster alternative. It’s worth a look if the overhead of lint-staged starts to feel like too much, especially if you only need very simple configurations. But keep in mind: don't swap tools just for the sake of it. If your linting feels slow, the problem is likely a bloated ESLint config, not the runner itself. Fix your rules before you replace your tools.

What do we actually need?

We really only have three goals: format the files we're working on, run the linter with the same rules used in CI, and make sure the process is fast enough that the team doesn't start using --no-verify just to skip the wait.

Not everything needs to be checked at every commit. We don't need to run a full monorepo typecheck or a massive test suite at this stage. Heavy lifting like that belongs in CI or perhaps at the pre-push stage.

Using pre-commit hooks can definitely disrupt your flow if the configuration is a mess. But when it's set up right, it’s a small investment that effectively keeps code quality in check without having to wait for a CI report.