Why Husky Is Underrated

We automate everything — builds, tests, deploys — yet somehow forget about the one place where bad code first enters the system: the commit. Husky solves that. It’s a simple, reliable way to hook into Git and run scripts before code gets merged, but it rarely gets the spotlight it deserves. If you care about clean commits and consistent workflows, Husky deserves a second look.
What’s Husky?
Husky is a Git hooks manager for Javascript/Typescript projects (though it can work in any repo).
Git hooks are scripts Git automatically runs at specific events - like before a commit (pre-commit), before a push (pre-push), right after a merge, etc.
Husky makes managing these hooks easier by letting you define and run them directly from your project. Think of them like triggers in a GitHub Actions workflow, except instead of jobs in YAML, you’re running executable scripts locally before your code even leaves your machine.
There are Native Git Hooks (without Husky) supported by Git, which can be found in the .git/hooks directory. Each hook is a script (like pre-commit, pre-push, etc.) that you can manually write in bash or another shell language. The downside is that they’re local only and cannot be shared via Git, which makes them hard to maintain across teams.
Navigate to an existing projects .git folder, and then to it’s hooks folder and you should find multiple .sample git hooks.
Here’s a sample of one of those hooks (pre-push.sample):

Now, Husky make these hooks:
Version-controlled: stored in your repo instead of .git/hooks. Everyone on your team gets access to the same environment.
Cross-platform: works the same on macOS, Linux, and Windows
Automated: runs scripts like linting, formatting, or tests before commits/pushes
Why use it?
Husky exists to guarantee code quality and consistency before code ever leaves a developer’s laptop.
Typical use cases include:
Running linters or formatters (ESLint, Prettier) before commits.
Preventing broken code from being pushed (
npm testinpre-pushhook).Enforce commit conventions (e.g., via Commitlint).
Auto-fix small errors like missing semicolons, wrong imports, etc.
This way you avoid “works on my machine” PRs.
Common Hooks & Their Use Cases
pre-commit: This hook is triggered before
git commit. Common use cases include running ESLint, Prettier, or even tests. It ensures only clean code gets committedcommit-msg: This hook is triggered after entering a commit message (
git commit -m message). Common use cases include running Commitlint. This lets you enforce standardized commit messagespre-push: This hook is triggered before
git push. Common use cases include running a test suite, preventing broken builds from reaching remote.post-merge: This hook is triggered after
gitmerge. Common use cases include installing dependencies or re-run builds. This keeps local dev env synced
Examples
Let’s say you want to ensure all code is linted and formatted before commit.
Install Husky
npm install husky --save-dev npx husky initAdd script to
package.json"scripts": { "prepare": "husky install" }Add a pre-commit hook
echo "npm run LINT" > .husky/pre-commit // overwrites fileMake a change in your project, stage and then commit and you should see the lint script run.

💡Confirm that
lintis one of the scripts in yourpackage.jsonfileCombine with lint-staged (Optional)
Lint-staged only runs linters on staged files (faster).
npm install lint-staged --save-dev"lint-staged": { // add on the same level with scripts i.e "scripts": {}, "lint-staged": {} "*.{js,ts,tsx}": ["eslint --fix", "prettier --write"] }Then update your pre-commit:
echo "npx lint-staged" >> .husky/pre-commit // ">>" appends to the file
The result is every commit runs eslint & prettier on staged files and fails if the code doesn’t meet your standards.

If your files are not staged and you try to run lint-staged, you get this:

Combining Husky with Commitlint
Commitlint helps enforce consistent commit messages (e.g. following Conventional Commits). It works well with Husky hooks to prevent bad commit messages before they even land in your repo.
Install Commitlint CLI + the Conventional Commits configuration (a ready to use ruleset)
npm install @commitlint/cli @commitlint/{config-conventional,cli} --save-devCreate a file called
commitlint.config.jsin your project root and extend the Commitlint conventional configuration// commitlint.config.js export default { extends: ['@commitlint/config-conventional'], };If your using CommonJS instead of ES modules, use this instead:
module.exports = { extends: ['@commitlint/config-conventional'] };Add a Husky commit-msg hook
echo 'npx --no -- commitlint --edit "$i"' >> .husky/commit-msgNow, Husky will automatically run Commitlint every time you try to commit.
Enforce Conventional Commits conventionally (Optional)
Try committing something invalid:
git commit -m "fixed bug"You’ll get an error because it doesn’t follow the correct format.

Try again with a valid message:
git commit -m "fix: resolve login bug"Commitlint passes — commit succeeds. This ensures commit messages follow a pattern like
feat: add new auth flow.
If you do not want to have to install Commitlint, you can utilize a manual shell-based commit message checker using a regex instead of Commitlint. It’s a common pattern in projects that want a lightweight, no-dependency way to enforce commit message conventions.
// in .husky/commit-msg commit_regex='^(feat|fix|docs|style|refactor|test|chore|merge|conflict|perf)(\(.+\))?: .{1,50}' if ! grep -qE "$commit_regex" "$1"; then echo "ERROR: Commit message format is invalid." echo "Expected format: <type>(<scope>): <description>" echo "Types: feat, fix, docs, style, refactor, test, chore" echo "Example: feat(auth): add login functionality" exit 1 fiAnd when you try committing something invalid:

With a valid commit message format, however:

That’s it! Happy coding🎉



