The ultimate guide to Git Hooks
Tags: git, hooks, githooks
Published: 2023-05-23 09:24:00

---
title: The ultimate guide to Git Hooks
published: true
description: A comprehensive guide around what Git Hooks are and what to use them for, their challenges and how to deal with them, tools to manage their dependencies easier, how to cross-check Git Hooks in when checking pull requests and how to mitigate findings and policy violations.
tags: git, hooks, githooks
cover_image: https://pumpingco.de/content/images/size/w2000/2023/05/joe-dudeck-M_EKk0FRIew-unsplash.jpg
# Use a ratio of 100:42 for best results.
published_at: 2023-05-23 09:24 +0000
---

## What are Git Hooks, and why should I use them?

Git Hooks are a Git feature, which allows developers to execute scripts at certain points in their Git lifecycle, like before accepting a commit, before pushing to a remote repository or before accepting a commit message. Git Hooks can be used to  "shift left" and automate tasks and enforce policies throughout the Git workflow.

Git Hooks live in the `.git/hooks` folder of a repository. Here, we can place scripts with the same name as the Git Hooks they should execute on (max. one per hook). Below, an example which checks the commit message for a certain pattern.

```bash
#!/bin/bash

MESSAGE=$(cat $1) 
COMMITFORMAT="^(feat|fix|docs|style|refactor|test|chore|perf|other)(\((.*)\))?: #([0-9]+) (.*)$"

if ! [[ "$MESSAGE" =~ $COMMITFORMAT ]]; then
  echo "Your commit was rejected due to the commit message. Skipping..."   
  exit 1
fi
```

Keep in mind, that each script in this folder has to be marked as executable by running `chmod +x pre-commit-msg`.

You can find more information and a list of available Git Hooks at [githooks.com](https://githooks.com).

## What to use them for?

- **Check commit messages:** Especially useful, if you use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). Also, to avoid the famous "WIP" or "fix" commit messages.
- **Check coding style and Linters:** A no-brainer. Nothing is more frustrating than going back to your code and fix that trailing space, after the CI pipeline failed because of it.
- **Check for secrets:** (Optional) We should check our code for accidentally checked-in secrets, like passwords or connection strings.
- **Build and Test:** (Optional) At least before pushing changes to a remote, we could make sure the code builds and the tests pass.

## Problems

### Can be bypassed

As Git Hooks can be bypassed. For example, with the `--no-verfify` flag for the `git commit` command. So we should never rely on their execution but rather see them as a helper to avoid frustration amongst developers, when they find their Pull Request checks failing.

> As we can not rely on their execution, Git Hook checks should be repeated when checking the Pull Request on the server!

### Not installed to each development environment by default

As Git Hooks live in the `.git/hooks` folder and the `.git` folder itself is not part of the version control, scripts that are placed into that folder will not be checked in. So new developers that are cloning the repository won't have the Git Hooks in their local .git folder. Also, remote environments, like GitHub's In-Browser editing or [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) won't have the Git Hooks setup by default.

A common practice around this behavior is placing the scripts into a `.githooks` folder in the repository root, and then configuring Git to use its Hooks from there with the `git config core.hooksPath .githooks` command. This allows to check in the scripts into version control, but the command still needs to be run in every new environment and won't make the Hooks available by default on new machines or remote environments.

### Dependencies might not be installed

Most scripts for checking coding style violations or commit message patterns aren't as basic as the one from above but use other tools like [Prettier](https://prettier.io/), [ESLint](https://eslint.org/) or [editorconfig-checker](https://github.com/editorconfig-checker/editorconfig-checker) for Coding Style checks or [Gitlint](https://jorisroovers.com/gitlint/latest/) (highly recommended) for commit message policies. These tools might not be installed on new or remote environments.

## Managing Git Hooks (and their dependencies) with pre-commit

To deal with the problems from above, there are many tools out there which make working with Git Hooks easier and more straight-forward. Amongst the Node.js and JavaScript community, [Husky](https://typicode.github.io/husky/) gained some popularity, as it natively integrates into the `package.json` file, where app dependencies and development dependencies are managed.

As I deal with projects across different programming languages and frameworks, I was looking for a more generic approach, which can be used with any project. The tool that convinced me the most is [pre-commit](https://pre-commit.com/), as it

- manages and installs Git Hooks
- manages and installs dependencies (= tools, that my Git Hooks use)
- can be run in CI pipelines to check, if Git Hooks haven't been skipped
- is technology independent (in contrast to popular Husky)

The configuration for pre-commit is stored in a `.pre-commit-config.yaml` file at the repository root and contains the hooks to configure and the tools that should be executed.

```yaml
default_install_hook_types:
- pre-commit
- pre-push
- commit-msg

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v4.0.1
  hooks:
  - name: Check, that no large files have been committed
    id: check-added-large-files

- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
  rev: '2.7.1'
  hooks:
  - name: Check EditorConfig
    id: editorconfig-checker
    alias: ec
    stages: ["pre-push"]

- repo: https://github.com/jorisroovers/gitlint
  rev: v0.19.1
  hooks:
  - name: Lint commit messages
    id: gitlint
  - id: gitlint-ci
    args: ['--commits', 'origin/main..HEAD']
```

## Installing Git Hooks for each developer

The configuration above allows to manage Git Hooks and their dependencies centrally in the repository. But the pre-commit tool itself still needs to be installed on each machine. Once installed, the hooks (and their dependencies) can be [installed](https://pre-commit.com/#install) with a single command.

```bash
pre-commit install
```

## Cross-Checking Git Hooks in the CI pipeline

As Git Hooks can be bypassed, it is highly recommended to cross-check them in Pull Request Checks or the CI pipeline. This is another reason, why I like pre-commit so much: The scripts can be executed at-hoc for checking the current Git repository and its commits for violations.

With GitHub Actions for example, you can check Pull Requests with the following workflow.

```yaml
name: PR Check
on:
  pull_request:

jobs:
  pr-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install pre-commit
        run: pip install pre-commit

      - name: Check commits
        run: pre-commit run --hook-stage manual
```

## Mitigate violations

But what to do, when Pre-Push or Pull Request checks find policy violations in our code or commit messages, but the commit is already created and part of the current branch's history? 

In case of a committed file that needs to be adjusted, the history of the current branch can be soft-reset to undo all commits that happened on that branch. Don't worry, your work won't be lost. This will put all modifications back to the list of uncommitted changes.

```bash
git checkout pr_branch
git reset --soft main
```

Before creating a commit, we can now adjust the files. Afterward, we can create a new commit without the violation.

```bash
git add -A && git commit -m "commit message goes here"
git push --force
```

To change a commit message afterward, we can follow a [comprehensive Guide from GitHub](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/changing-a-commit-message).

Both, resetting the history and changing commit messages is annoying. To avoid that, it is generally recommended to do most checks at the pre-commit level.