Learn Git - Git Hooks & Local Automation
Episode 19 of 21

Learn Git - Git Hooks & Local Automation

Automating local workflows with Git hooks: pre-commit for linters and formatters, commit-msg for Conventional Commits validation, pre-push for unit tests, plus the Husky and pre-commit frameworks to share them across the whole team.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Introduction

In episode 18 we managed composite repositories. In episode 19 we discuss reliability at the point most often overlooked: your own hands. A Git Hook is a script that runs automatically at certain moments in the git cycle — and this episode shows how to use them as a local quality gate before code leaves the laptop.

Imagine a team where every commit always passes the linter, every commit message follows Conventional Commits, and every push always passes unit tests. Not through discipline alone, but through automation — Git will not let violations through. This is the difference between a team that is tidy because of written rules and a team that is tidy because of systems.

What Is a Git Hook

A hook is a script placed in the .git/hooks/ directory that Git runs at specific points. Check the default contents:

Viewing the built-in hooks
ls .git/hooks/

It contains a series of examples with a .sample suffix (pre-commit.sample, commit-msg.sample, pre-push.sample). Remove the .sample suffix and fill in your script, or create a new file without the suffix.

There are two groups of hooks:

  • Client-side hooks — run on each developer's machine (commit, merge, push).
  • Server-side hooks — run on the remote server; most are now handled by GitHub features such as Branch Protection Rules (episode 13) and CI.

Our focus this time: the client-side hooks pre-commit, commit-msg, and pre-push.

pre-commit: Linter & Formatter

The pre-commit hook runs before the commit is created. If it exits with a non-zero status, the commit is cancelled. Example for a TypeScript project:

.git/hooks/pre-commit
#!/bin/sh
echo "Menjalankan linter dan formatter..."
bun run lint
if [ $? -ne 0 ]; then
  echo "Lint gagal. Commit dibatalkan."
  exit 1
fi

Do not forget chmod +x .git/hooks/pre-commit so the script can be executed.

commit-msg: Conventional Commits Validation

The commit-msg hook receives the path to the commit message file as its first argument — perfect for validating the message format:

.git/hooks/commit-msg
#!/bin/sh
pattern='^(feat|fix|docs|refactor|chore|perf|test|build|ci|style)(\(.+\))?: .+'
if ! grep -qE "$pattern" "$1"; then
  echo "Pesan commit tidak mengikuti Conventional Commits."
  echo "Contoh: feat: tambah halaman login"
  exit 1
fi

A commit with the message asdf is rejected before it ever enters history — exactly the discipline we have been discussing since episode 4.

pre-push: Unit Tests

The pre-push hook runs after git push is invoked but before data is sent to the remote. This is the last gate:

.git/hooks/pre-push
#!/bin/sh
echo "Menjalankan unit test sebelum push..."
bun run test

Code that fails tests never reaches origin — code review on GitHub (episode 11) stays focused on logic, not on things a machine should already have checked.

Sharing Hooks with the Team

The problem: the .git/ folder is not cloned. Hooks written on your laptop will not appear on your teammates' laptops. The solution is a framework that stores hooks as ordinary files in the repository.

Husky for JavaScript / TypeScript

Installing Husky
bun add -D husky
bunx husky init

Husky creates a .husky/ folder that is committed. Contents of .husky/pre-commit:

plaintext
bun run lint

Because this file lives in the repository, every team member automatically gets the same hook after running the installation.

pre-commit for Python / Multi-Language

The pre-commit framework (do not confuse it with the pre-commit hook) manages many Python-based hooks across multiple languages:

.pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer

Install once with pip install pre-commit, then pre-commit install to wire it into .git/hooks/. The .pre-commit-config.yaml configuration file is shared through Git.

Tip

Start with just two hooks: commit-msg for message format and pre-commit for the linter. Hooks that are too heavy get annoying and are often skipped by team members. Increase gradually — add pre-push for tests once the team is used to it.

Common Mistakes

  1. Hook not chmod +x. The script does not execute, and there is no clear error.
  2. Putting important logic directly in .git/hooks/. This folder is not synced; use Husky or pre-commit so it can be committed.
  3. Hook too slow. A full test suite in pre-commit makes commits take minutes; move the heavy stuff to pre-push.
  4. git commit --no-verify becomes a habit. Use this shortcut occasionally and be aware of the consequences, not as a default solution.

Closing

The points to take with you:

  • A hook is a script in .git/hooks/ that Git runs at certain moments in the cycle.
  • pre-commit for linters and formatters, commit-msg for message validation, pre-push for unit tests.
  • Local hooks are not cloned — share them through Husky (JavaScript) or pre-commit (Python/multi-language).
  • A hook is a quality gate: rules enforced by the machine, not just in writing.

Local automation is covered. In episode 20, the final episode of this series, we discuss the GitHub Automation Ecosystem: GitHub Actions for CI/CD, Semantic Versioning, Git Tags, and GitHub Releases with the gh CLI. See you in episode 20!

Learn Git - Git Hooks & Local Automation | Learn Git & GitHub