Getting Started with Git Hooks and Husky
July 2022 by Bruno Brito

Getting Started with Git Hooks and Husky

When you're looking to enforce policies and ensure consistency among team members, Git Hooks can be a helpful solution. By running custom scripts before or after Git operations occur, you can prevent commits from being added to a repository and notify team members that there are changes needed to be made.

Let's see how this works in practice with Husky, a popular JavaScript package that allows us to add Git Hooks to our JS projects with ease.

How you use hooks is largely up to you, but there are a few popular use cases worth highlighting in this post. Let's have a look at five of them.

In this guide, you'll learn how to:

  1. Validate branch names.
  2. Run a linter to check commit messages.
  3. Run a linter to style/format committed code.
  4. Compress any images added to the project.
  5. Run Jest tests to ensure that nothing will break.

For this fun project, we'll be using a fresh installation of Gatsby, but any JavaScript project would do. Let's get started!

Getting Started with Husky

Like any node package, you can install Husky with npm or yarn:

$ npm install husky --save-dev

Once installed, you will also need to run this command to enable Git hooks:

$ npx husky install

Finally, let's edit the package.json file so that it automatically runs this last command after installation:

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

That's it! Husky is ready to be used on the command line, but we should also make sure everything is set up nicely in Tower, our Git client, as well.

We make Tower, the best Git client.

Not a Tower user yet?
Download our 30-day free trial and experience a better way to work with Git!

Husky and Tower

When you have some Git hooks configured in Husky, you will get an error message in Tower when you attempt to add a commit. Something like:

.husky/pre-commit: line 4: npx: command not found
husky - pre-commit hook exited with code 127 (error)
husky - command not found in PATH=/Library/Developer/CommandLineTools/usr/libexec/git-core:/Applications/Tower.app/Contents/Resources/git-flow:/Applications/Tower.app/Contents/Resources/git-lfs:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin

Let's fix that!

As described here, this happens because GUI apps don't know the environment set up by your shell. To fix this, we will need to add an environment.plist file in ~/Library/Application Support/com.fournova.Tower3/ and define the PATH environment.

The location will vary depending on your environment. I'm using asdf-nodejs to manage my Node.js versions, so my environment.plist file looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>PATH</key>
        <string>/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/Users/brunobrito/.asdf/shims</string>
  </dict>
</plist>

After saving this file and restarting Tower, everything looks good to go.

Back to the command line — time for our first hook!

#1. Validating Branch Names

This hook can be handy when you want to make sure that every branch name follows a specific pattern, such as starting with feature, fix, or release.

There's a little npm package that can help us in this process: validate-branch-name. It comes with nice defaults out of the box, that you can further customize with some regex knowledge.

First, let's install this package:

$ npm install validate-branch-name --save-dev

Now, let's add it to Husky as a "pre-commit" hook by running the following command:

$ npx husky add .husky/pre-commit "npx validate-branch-name"

You'll notice that the npx validate-branch-name has been added to Husky's pre-commit file inside Husky's hidden folder (.husky/pre-commit).

Let's try it! Start by creating a branch that does not follow this naming convention, like "test". As soon as you add a commit, you will be presented with the following error message:

Result: "failed" 
Error Msg: Branch name validate failed please rename your current branch 
Branch Name: "test" 
Pattern:"/^(master|main|develop){1}$|^(feature|fix|hotfix|release)\/.+$/g" 
husky - pre-commit hook exited with code 1 (error)

Great! It's working as intended. Let's shift gears to commit messages.

#2. Linting Commit Messages

If you already follow the guidelines from the Angular team, you're in luck: we'll use commit-msg-linter, a package that enforces them.

Setting it up is similar to the previous example. First we run the usual installation command...

$ npm install git-commit-msg-linter --save-dev

And now we'll add it to Husky (as a "commit-msg" hook this time):

$ npx husky add .husky/commit-msg ".git/hooks/commit-msg \$1"

To try it out, let's add a commit message that does not follow Angular's guidelines, such as "Add Husky hooks". The branch's name, main, is accepted, but the commit message isn't, so the commit is aborted:

Invalid Git Commit Message
Invalid Git Commit Message



If we actually follow the Angular conventions, the commit will go through. Two down, three to go!

#3. Linting Code

We're big believers that linters boost developer productivity, so let's make sure that any code submitted to the repository is properly linted.

We will use not one, but two node packages to achieve this task: lint-staged and Prettier.

lint-staged will be used to run checks on staged files (instead of checking the entire project). Then, Prettier will be responsible for the actual formatting and linting (you could definitely go for ESLint instead!).

Let's install both packages:

$ npm install lint-staged prettier --save-dev

Now, let's add the npx lint-staged command as a new line to the "pre-commit" hook:

$ npx husky add .husky/pre-commit "npx lint-staged"

Finally, let's set up lint-staged in the `package.json file so that it runs Prettier on JS files:

// package.json
{
  "scripts": {
    "prepare": "husky install"
  },
  "lint-staged": {
    "**/*.js": "prettier --write --ignore-unknown"
  },
  ...
}

Done! Our JS code will be properly formatted before our commit is added.

Commit successful after running Prettier
Commit successful after running Prettier

We make Tower, the best Git client.

Not a Tower user yet?
Download our 30-day free trial and experience a better way to work with Git!

#4. Compressing Staged Images

While Gatsby does a great job at delivering the best possible images to the user, that's not always the case with other frameworks — and running tools like ImageOptim every time can be tedious and easy to forget.

Let's add a little script that automatically compresses any images added to the project. We'll use ImageOptim-CLI to accomplish this task. You can, of course, use squoosh or imagemin instead.

Let's first install it (globally):

$ npm install -g imageoptim-cli

Since we only want to compress staged files, let's add the imageoptim command to lint-staged (ImageOptim is smart enough to only look for image file formats):

// package.json
{
  "scripts": {
    "prepare": "husky install"
  },
  "lint-staged": {
    "**/*.js": "prettier --write --ignore-unknown",
    "**/*": "imageoptim"
  },
  ...
}

That's all there is to it! ImageOptim will automatically launch, compress any images you had in the staging area, and close.

#5. Running Tests

Last one! Let's install Jest to run tests by typing a command I'm sure you already know by heart:

$ npm install --save-dev jest

Now, let's add a "test" script to our package.json file:

// package.json
{
  "scripts": {
    "prepare": "husky install",
    "test": "jest"
  },
  "lint-staged": {
    "**/*.js": "prettier --write --ignore-unknown",
    "**/*": "imageoptim"
  },
  ...
}

After adding a new JS file and a .test.js file with the actual test, we can run npm test to run Jest.

The output should look similar to this:

> jest

 PASS  ./sum.test.js
  ✓ adds 1 + 2 to equal 3 (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.213 s
Ran all test suites.

Finally, let's add our npm test script to our "pre-commit" hook by following the steps mentioned previously:

$ npx husky add .husky/pre-commit "npm test"

Now, when we add a commit, all five hooks will run. A thing of beauty!

Commit successful after running Jest
Commit successful after running Jest


Hurray! We have successfully set up five different Git hooks! 🥳

While hooks can be helpful, there will be times where skipping the execution of these scripts can make sense. While it should be the exception, not the norm, let's figure out how we can bypass them.

Skipping Hooks

In the terminal, you can bypass hooks by adding the --no-verify option, like in this example:

$ git commit -m "skipping hooks" --no-verify

In Tower, you can easily skip the execution of hooks by checking the "Skip Hooks" checkbox in the "Commit Composing" window. The option will automatically be displayed if you have any "pre-commit" hooks configured.

Skipping Hooks in Tower — Commit Composing
Skipping Hooks in Tower — Commit Composing


To skip the execution of hooks for push, pull, merge, rebase, and apply operations, click on "Options" to access the list of additional options.

Skipping Hooks in Tower — Commit Composing
Skipping Hooks in Tower — Push Operation

Final Words

When it comes to automation, each case is unique, and the sky is the limit — hooks are no exception. We hope you found this post useful to get started with Husky!

For more tips, don't forget to sign up for our newsletter below and follow Tower on Twitter!

Your Download is in Progress…

Giveaways. Cheat Sheets. eBooks. Discounts. And great content from our blog!