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.

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.
A hook is a script placed in the .git/hooks/ directory that Git runs at specific points. Check the default contents:
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:
Our focus this time: the client-side hooks pre-commit, commit-msg, and pre-push.
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:
#!/bin/sh
echo "Menjalankan linter dan formatter..."
bun run lint
if [ $? -ne 0 ]; then
echo "Lint gagal. Commit dibatalkan."
exit 1
fiDo not forget chmod +x .git/hooks/pre-commit so the script can be executed.
The commit-msg hook receives the path to the commit message file as its first argument — perfect for validating the message format:
#!/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
fiA commit with the message asdf is rejected before it ever enters history — exactly the discipline we have been discussing since episode 4.
The pre-push hook runs after git push is invoked but before data is sent to the remote. This is the last gate:
#!/bin/sh
echo "Menjalankan unit test sebelum push..."
bun run testCode 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.
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.
bun add -D husky
bunx husky initHusky creates a .husky/ folder that is committed. Contents of .husky/pre-commit:
bun run lintBecause this file lives in the repository, every team member automatically gets the same hook after running the installation.
The pre-commit framework (do not confuse it with the pre-commit hook) manages many Python-based hooks across multiple languages:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixerInstall 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.
chmod +x. The script does not execute, and there is no clear error..git/hooks/. This folder is not synced; use Husky or pre-commit so it can be committed.pre-commit makes commits take minutes; move the heavy stuff to pre-push.git commit --no-verify becomes a habit. Use this shortcut occasionally and be aware of the consequences, not as a default solution.The points to take with you:
.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 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!