Skip to main content

Command Palette

Search for a command to run...

Why Husky Is Underrated

Published
5 min read
Why Husky Is Underrated
T
Hello World! I'm Tito. I write about designing and building real-world software — frontend, backend, infrastructure, system design, and data.

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:

  1. Version-controlled: stored in your repo instead of .git/hooks. Everyone on your team gets access to the same environment.

  2. Cross-platform: works the same on macOS, Linux, and Windows

  3. 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 test in pre-push hook).

  • 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 committed

  • commit-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 messages

  • pre-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 git merge. 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.

  1. Install Husky

     npm install husky --save-dev
     npx husky init
    
  2. Add script to package.json

     "scripts": {
       "prepare": "husky install"
     }
    
  3. Add a pre-commit hook

     echo "npm run LINT" > .husky/pre-commit  // overwrites file
    

    Make a change in your project, stage and then commit and you should see the lint script run.

    💡Confirm that lint is one of the scripts in your package.json file

  4. Combine 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.

  1. Install Commitlint CLI + the Conventional Commits configuration (a ready to use ruleset)

     npm install @commitlint/cli @commitlint/{config-conventional,cli} --save-dev
    
  2. Create a file called commitlint.config.js in 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'] };
    
  3. Add a Husky commit-msg hook

     echo 'npx --no -- commitlint --edit "$i"' >> .husky/commit-msg
    

    Now, Husky will automatically run Commitlint every time you try to commit.

  4. 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
     fi
    

    And when you try committing something invalid:

    With a valid commit message format, however:

That’s it! Happy coding🎉

More from this blog

Engineering with Tito

29 posts

Hello World! I am Bolatito. I am a full stack software engineer. I am obsessed with building great products and currently, I am specializing in building products for the web.