Stop Breaking the Build: A Hands-On Guide to Git Hooks

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Shift Left: Why Wait for the Cloud?

Most developers treat CI/CD as a distant process running on a remote server like GitHub Actions or Jenkins. You push your code, grab a coffee, and wait five minutes for a green checkmark. If you see a red ‘X’ instead, you have to context-switch, fix a silly typo, and start the cycle over. This feedback loop is a silent productivity killer.

Git Hooks flip this script by moving those essential checks to your local machine. These are simple scripts that Git triggers automatically before or after events like committing or pushing. By catching a missing semicolon or a failing unit test on your laptop, you save your team from broken builds. I’ve seen teams reduce their CI failure rates by over 40% simply by implementing a basic pre-commit routine.

Think of it as a personal assistant that proofreads your work before you hand it in. It prevents those embarrassing moments where a minor syntax error halts the entire pipeline for everyone else.

Where Do These Hooks Live?

Every Git repository has a built-in automation engine hidden in plain sight. When you run git init, Git creates a .git/hooks directory. Inside, you’ll find several template files.

cd .git/hooks
ls -l

You will see files like pre-commit.sample and pre-push.sample. These are just shell scripts. To activate one, remove the .sample extension and ensure the file is executable. It’s that simple.

There are two categories to keep in mind:

  • Client-side hooks: These run on your computer. They are perfect for linting, formatting, and quick unit tests.
  • Server-side hooks: These run on the remote server. They are used to enforce branch naming conventions or trigger deployment notifications.

For most daily work, client-side hooks like pre-commit provide the fastest return on investment.

Creating Your First Gatekeeper (Pre-Commit)

Let’s build a hook that blocks a commit if the code is messy. This keeps the repository clean and ensures everyone follows the same style guide. We’ll use a Node.js example, but you can swap this for Python’s flake8 or Go’s gofmt.

First, create the file and give it the right permissions:

touch .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Add this logic to the file. It tells Git: “Run the linter. If it finds problems, stop the commit immediately.”

#!/bin/sh

echo "🔍 Running style checks..."
npm run lint

# Capture the result of the last command
status=$?

if [ $status -ne 0 ]; then
    echo "❌ Linting failed! Fix the errors above before committing."
    exit 1
fi

echo "✅ Style looks good. Committing now."
exit 0

Now, every time you type git commit, the script runs. If your code is messy, Git cancels the operation. You are forced to fix the issue locally in seconds rather than waiting minutes for a CI server to tell you the same thing.

The “Works on My Machine” Problem

The standard .git/hooks folder has one major flaw: Git does not track it. If you create a great hook, your teammates won’t see it when they clone the project. To fix this, use a tool like Husky (for JavaScript) or the pre-commit framework (for Python and multi-language projects).

With Husky, you can store hooks in a .husky folder that stays in version control. Setup is quick:

npx husky-init && npm install
npx husky add .husky/pre-commit "npm test"

This ensures every developer on the team runs the same checks. No excuses.

Pre-Push: The Final Safety Net

While linting is fast, you probably don’t want to run a full 10-minute integration test suite every time you save a small change. This is where the pre-push hook shines. It runs only when you try to send code to the remote server.

#!/bin/sh

echo "🧪 Running full test suite before pushing..."
npm test

if [ $? -ne 0 ]; then
    echo "❌ Tests failed. Push aborted to protect the main branch."
    exit 1
fi

exit 0

By splitting your automation—linting on commit and testing on push—you keep your local workflow fast while still guarding the shared codebase.

Troubleshooting and Bypassing

If a hook isn’t firing, check the basics. Is it executable? Run chmod +x .git/hooks/pre-commit. Is the filename exact? Git won’t recognize pre-commit.sh. Also, remember that hooks run in a non-interactive shell. Always use local paths like ./node_modules/.bin/eslint to avoid “command not found” errors.

Sometimes you might need to bypass the rules, like during an emergency hotfix. You can skip the hooks by adding a single flag:

git commit -m "Emergency fix" --no-verify

Use this sparingly. If you find yourself using --no-verify daily, your tests are likely too slow or your rules are too pedantic. A good hook should be helpful, not a hurdle.

The Bottom Line

Automation isn’t just for big cloud servers. It starts at your keyboard. By spending ten minutes setting up Git Hooks, you can save hours of cumulative waiting time every month. Start small with a linter, then expand to secret-scanning (to prevent leaking API keys) or auto-formatting. Your CI server—and your teammates—will thank you.

Share: