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 install --save-dev husky
BashOr (if you are using yarn)
yarn add --dev husky
BashNow that we’ve installed the package, let’s go ahead and initialize it by typing:
npx husky init
BashInitializing Husky does the following:
- creates a
.husky
directory in your project, which includes:- a
pre-commit
hook file - a
_
subfolder for additional settings.
- a
- Adds a
prepare
script to yourpackage.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:
npm test
PlaintextThis 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
BashSee? 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:
"scripts": {
"prepare": "husky install"
}
JSON5This 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.
npm test
exit 1
Plaintextexit-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:
echo "🐶 Executing pre-commit hook..."
echo ""
# Run lint on staged files
echo "🔍 Running ESLint on staged files..."
npx eslint .
echo "✅ ESLint passed."
PlaintextBy 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.
BashAwesome, 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.
BashAll 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.log
s maybe?
Just add the following content to your pre-commit
file:
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
PlaintextLet’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:
# Match staged JS/TS files
FILES_PATTERN='\.(js|ts)(x)?$'
# Disallowed patterns: console methods
FORBIDDEN='(console.log)'
PlaintextIn 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.log
s.
The next part is the trickiest one:
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
PlaintextIt’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:
echo "✅ No forbidden patterns found!"
echo "✅ Git pre-commit hook passed. You're good to go 🚀"
PlaintextTo 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.
BashAfter 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 🚀
BashThis 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:
# Disallowed patterns: console methods
FORBIDDEN='(console\.(log|info|warn|error|clear|dir)|it\.only)'
PlaintextOther 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 onespre-push
– Run final checks before pushing to remotepost-merge
– Automatically sync dependencies or regenerate files after a merge (e.g., runnpm install
orpnpm 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
BashJust 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 ...
BashI 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