Home » Blog » Husky Hooks: The Easy Way to Improve Your Project Workflow

Husky Hooks: The Easy Way to Improve Your Project Workflow

We all miss stuff sometimes — skipping a test run, forgetting to lint, or pushing code that breaks something.
Yes, guilty as charged! Husky hooks help you catch those issues early by running scripts right when you commit or push. 🐶
It’s a simple way to keep your workflow clean, lower the pressure, save time, and avoid nasty surprises later.

Introduction: Why use Husky hooks in your project?

Husky is a great tool to help you out when working on a project and being proactive. It can catch problems before they are committed and make it into your codebase.

How? By running scripts like tests or linters when committing or pushing your code. It’s a simple way to enforce code quality.

But why not just use native Git hooks or a CI tool like CircleCI or GitHub Actions, you might wonder… and honestly, that’s a fair question — I asked the same thing when I first came across Husky.

The answer really comes down to when you want to catch issues in your code, and whether you’re comfortable setting up native Git hooks or working with a team where shared, versioned hooks make more sense.

Do you want to wait for your CI tool to run all steps just to tell you a test failed or a semicolon was missing?
Or would you rather know right away, before even pushing your code? ⚡

What are you waiting for if you’d like to catch issues immediately? 🚀
Let’s go ahead and install Husky and learn more about this cool tool! 🛠️🐶

Installing and initializing Husky in your project

Installing Husky is straightforward; you just type in your terminal

npm
npm install --save-dev husky
Bash

Or (if you are using yarn)

yarn
yarn add --dev husky
Bash

Now that we’ve installed the package, let’s go ahead and initialize it by typing:

Bash
npx husky init
Bash

Initializing Husky does the following:

  • creates a .husky directory in your project, which includes:
    • a pre-commit hook file
    • a _ subfolder for additional settings.
  • Adds a prepare script to your package.json file to ensure Husky is set up when dependencies are installed.

.husky/pre-commit

The pre-commit file is a Git hook managed by Husky. It runs automatically before each commit, allowing you to run scripts like linters or tests to catch issues early.

When Husky initializes its structure, it adds the following content to this hook file:

pre-commit
npm test
Plaintext

This basically means that whenever you are running your git commit command, Husky will first run npm test. If the tests pass, the commit will go through. If they fail, the commit will be blocked, and you’ll need to fix the errors before trying again.

When I tried this in a brand-new project by making a simple change and committing it, here’s what I saw:

git commit -m "A simple change to see pre-commit hook"

> [email protected] test
> jest

 PASS  src/pages/hello.test.ts
 PASS  src/components/heading.test.tsx

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.491 s, estimated 1 s
Ran all test suites.
[playground c6e4aaf] A simple change to see pre-commit hook
 2 files changed, 5 insertions(+), 1 deletion(-)
 create mode 100644 .husky/pre-commit

Bash

See? It ran the two tests included in my project and then finalized the commit process.

.husky/_ folder

This folder is created by Husky and contains setup code that helps your Git hooks run smoothly across different systems.

You usually don’t need to touch anything here — it just works in the background to support the other hook files.

prepare script

When you set up Husky, it adds a prepare script to your package.json file:

package.json
"scripts": {
  "prepare": "husky install"
}
JSON5

This script makes sure git hooks are installed whenever someone installs your project’s dependencies (e.g., by running npm install or yarn install).

This could come in handy if you are sharing your project with a team. The prepare script will ensure Husky works automatically for everyone without any extra steps needed.

Expanding pre-commit: Catch more before it hits your repo

Although running tests before committing is what Husky suggests by default when it initializes, I personally find that a bit too much. It feels like a blocker when all I want is to make a quick commit to save my progress. So instead of leaving npm run test in the pre-commit file, I prefer to remove the testing and go with simpler, quicker checks at that point.

Before we change our pre-commit file, I just want to share a quick tip — did you know you can test Husky hooks without having to write a new commit message every single time?

Good news: you can do exactly that by adding a simple line at the end of your pre-commit file.

pre-commit
npm test

exit 1
Plaintext

exit-1 will stop the script from continuing, which makes it a safe way to test your pre-commit commands without actually committing your changes.

Hook for linting checks

Go to your .husky folder and open the pre-commit file replace its content with:

pre-commit
echo "🐶 Executing pre-commit hook..."
echo ""

# Run lint on staged files

echo "🔍 Running ESLint on staged files..."

npx eslint .

echo "✅ ESLint passed."
Plaintext

By doing this, we’ve added ESLint checks right before committing — simple as that!

To make sure it works, try committing some new changes, and you’ll see the results show up right in your terminal.

🐶 Executing pre-commit hook...

🔍 Running ESLint on staged files...
 ESLint passed.
Bash

Awesome, right? No linting errors! 🎉
But what if there were some? What would that look like?

I added a small linting issue (couple of spaces) to test it, and here’s what showed up when I ran it again:

🐶 Executing pre-commit hook...

🔍 Running ESLint on staged files...

/my-project/src/components/Heading.test.tsx
  6:5  error  Delete `··`  

 1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.
Bash

All that’s left now is to fix the issue and commit the changes successfully. ✅

Hooks to catch console.log statements

Now that we’ve got linting checks in place, why not expand them a bit and catch things we definitely don’t want sneaking into our code — like console.logs maybe?

Just add the following content to your pre-commit file:

pre-commit
echo "🐶 Executing pre-commit hook..."
echo ""

# Run lint on staged files

echo "🔍 Running ESLint on staged files..."

npx eslint .

echo "✅ ESLint passed."
echo ""

# Match staged JS/TS files
FILES_PATTERN='\.(js|ts)(x)?$'

# Disallowed patterns: console methods
FORBIDDEN='(console.log)'

echo "🔎 Checking staged files for forbidden patterns (e.g. console.log)..."

# Scan staged files for forbidden patterns
if git diff --cached --name-only --diff-filter=ACM | \
    grep -E "$FILES_PATTERN" | \
    xargs grep --with-filename -n -E "$FORBIDDEN" | \
    grep -v '^\s*//'; then

    echo ""
    echo "🚫 COMMIT REJECTED! Found forbidden patterns"
    echo "😅 Please clean them up before committing."
    echo ""
    exit 1
fi

echo "✅ No forbidden patterns found!"
echo "✅ Git pre-commit hook passed. You're good to go 🚀"

exit 0

Plaintext

Let’s check what each part does

echo is a command used to print informational messages in the terminal — basically just outputting text so we can see what’s happening.

exit 0 means the script completed successfully, while exit 1 signals an error or that something failed.

Next, we see:

pre-commit
# Match staged JS/TS files
FILES_PATTERN='\.(js|ts)(x)?$'

# Disallowed patterns: console methods
FORBIDDEN='(console.log)'
Plaintext

In this part, by using FILES_PATTERN, we’re basically saying we only want to match files with the extensions .js, .jsx, .ts, or .tsx — and yep, you guessed it, I’m working in a project that’s transitioning from JS to TS!

The next constant, FORBIDDEN, defines what I don’t want slipping into my commits — for now, that’s just console.logs.

The next part is the trickiest one:

pre-commit
if git diff --cached --name-only --diff-filter=ACM | \
    grep -E "$FILES_PATTERN" | \
    xargs grep --with-filename -n -E "$FORBIDDEN" | \
    grep -v '^\s*//'; then

    echo ""
    echo "🚫 COMMIT REJECTED! Found forbidden patterns"
    echo "😅 Please clean them up before committing."
    echo ""
    exit 1
fi
Plaintext

It’s a loop that goes through all the staged files and checks each one against every pattern we’ve defined, like console.log. If it finds a match, it prints an error message showing the file and the exact pattern it caught.

Otherwise, it lets us know everything’s clean and we’re good to move forward with the commit:

pre-commit
echo "✅ No forbidden patterns found!"
echo "✅ Git pre-commit hook passed. You're good to go 🚀"
Plaintext

To test this script, I added a console.log to one of my files, just to see what it would print when running this hook

🐶 Executing pre-commit hook...

🔍 Running ESLint on staged files...
 ESLint passed.

🔎 Checking staged files for forbidden patterns (e.g. console.log)...
src/components/Heading.test.tsx:6:    console.log('test');

🚫 COMMIT REJECTED! Found forbidden patterns
😅 Please clean them up before committing.
Bash

After fixing that and removing the console.log, I ran it again and saw everything passed — no errors, no blocks. Just a smooth, satisfying commit! 🙌

🐶 Executing pre-commit hook...

🔍 Running ESLint on staged files...
 ESLint passed.

🔎 Checking staged files for forbidden patterns (e.g. console.log)...
 No forbidden patterns found!
 Git pre-commit hook passed. You're good to go 🚀

Bash

This script could easily be expanded to catch all console methods like .log, .info, and so on — or even .only in your test files. All you need to do is update the FORBIDDEN constant like this:

pre-commit
# Disallowed patterns: console methods
FORBIDDEN='(console\.(log|info|warn|error|clear|dir)|it\.only)'
Plaintext

Other Git hook ideas to level up your workflow

Although using the pre-commit hook may seem like it’s blocking your workflow or might appear straightforward in terms of what you can do with it, there are other use cases where you can run scripts at different stages, not just on commit.
Here are a few common Husky hooks you can use:

  • commit-msg – Validate or format the commit message using your project’s guidelines or some conventional ones
  • pre-push – Run final checks before pushing to remote
  • post-merge – Automatically sync dependencies or regenerate files after a merge (e.g., run npm install or pnpm install) 🔄
  • post-checkout – Run scripts after switching branches (e.g., load env files, clean build cache)

These hooks are just examples of what’s possible — you don’t need to use them all, but knowing they exist can help you pick the right tool for the right task.
Start small, and build up as your project grows!

Husky vs GitHub actions: When to use each tool

You might be working on a project that already uses a CI/CD tool like GitHub Actions or CircleCI, and wondering — how is that different from Husky? 🤔
Let’s look at some key differences to help you decide whether Husky is a good fit for your project.

Feature
Husky
CI/CD Tools (e.g., GitHub Actions, GitLab CI)
Runs when?
Locally, before code is committed or pushed
After code is pushed to a remote repository
Best for
Catching issues early, enforcing local standards
Automating builds, tests, deployments
Speed
Instant feedback (runs on your machine)
Slower (runs in external environments)
Purpose
Prevent bad code from leaving your laptop 💻
Handle what happens after it leaves your laptop 🚀

The way I see it, it’s great to save time and feel confident that everything I’m pushing follows the project’s rules and guidelines. That’s why I’d choose to use both: CI/CD tools for automation after pushing, and Husky for those extra checks before the code even leaves my machine.

Local git hooks vs Husky: What’s the difference?

If you’ve worked with Git hooks before, it’s only natural to question what Husky really adds on top of them.
I had the exact same thought when I first came across it. And then it clicked — let’s take a closer look at how they differ. 🔍

Feature
Local Git Hooks
Husky
Setup
Manual – you write and place hook scripts
Managed by Husky with simple commands
Version control
Not tracked by default
Fully version-controlled in your repo
Sharing with team
Hard to sync
Easy to share and maintain across the team

I’d say the most significant and most useful advantage of Husky is that it lets you share your hooks with your team and track them through Git.
I used to try setting up Git hooks manually, even when working solo — but it always felt like this mysterious black box, and I kept putting it off.
Husky makes the whole process way easier and more straightforward. Plus, being able to commit your hooks with the repo and even reuse the same setup across projects? Huge time-saver. Count me in! 🙌

Frequently Asked Questions

Is there a way to temporarily skip Git hooks?

Yes, you can!
If you’re under pressure or just need to skip Git hooks (including Husky hooks) for any reason, you can do it by adding the --no-verify flag to your Git command.

For example

git commit -m "My quick fix" --no-verify
Bash

Just keep in mind — skipping hooks should be the exception, not the habit. Hooks are there to help your workflow, not get in the way. 😉

Another way to do this is by using HUSKY=0 in your script e.g

HUSKY=0 git commit ...
Bash

I got used to using --no-verify when I want to skip hooks, but it’s nice to know that Husky offers an alternative.

Can I use Husky locally even if the rest of the team isn’t?

Absolutely! You can set up Husky on your own without forcing it on others. Since Husky hooks live in the .husky/ folder, you can simply add that folder to .gitignore if you don’t want to commit it.
Alternatively, you can commit it and let others opt in if they want — it’s up to your team’s workflow.

Have you used Husky in your projects, or do you prefer sticking with native Git hooks or CI tools?
Drop a comment and share how you manage your Git workflow — We’d love to hear what’s worked for you! 💬👇

Leave a reply

Your email address will not be published. Required fields are marked *