DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

A Practical Guide to Git Hooks for Real-World Workflows

A Practical Guide to Git Hooks for Real-World Workflows

A Practical Guide to Git Hooks for Real-World Workflows

Git hooks are small, executable scripts that run at certain points in your Git lifecycle. When used well, they automate quality checks, enforce conventions, and speed up collaboration without slowing down developers. This guide walks you through practical, real-world uses of Git hooks, with concrete examples you can adapt to your projects.

Why use Git hooks

  • Catch problems early: prevent committing bad code, misspelled messages, or forgotten tests.
  • Enforce standards: ensure commit messages follow a format, run linters, or check licenses.
  • Automate chores: run tests, build artifacts, or update version numbers automatically.
  • Improve team consistency: provide predictable, repeatable Git processes.

Note: Git hooks live in the .git/hooks directory of your repository and are not versioned by default. They’re typically shared via template hooks or by distributing scripts through your project’s tooling.

Common hook types to consider

  • pre-commit: run before commit. Ideal for linting, formatting, and basic tests.
  • commit-msg: validate or standardize commit messages.
  • prepare-commit-msg: pre-fill or suggest a commit message template.
  • pre-push: run tests or checks before pushing.
  • post-merge: run actions after a merge, such as re-running tests.
  • post-checkout: run actions after checkout (e.g., install deps for a new branch). ### Getting started: setting up local hooks

1) Create reusable hooks in your repo

  • Create a scripts/hooks/ directory to store hook logic.
  • Make hooks executable and reference them from .git/hooks.

2) Use a hook runner tool (optional but helpful)

  • Husky (for Node.js projects) or Lefthook (language-agnostic) can simplify cross-team hook management.
  • These tools let you define hooks in package.json or config files and share them with teammates.

3) Share hooks with the team

  • Commit a hooks/ directory (or a template) and document how to install the runner.
  • Provide a small bootstrap script to install dependencies and set up executables. ### Practical hook examples

Below are practical, ready-to-adapt hooks you can drop into your project. Each example includes the script and a short rationale.

1) Pre-commit: lint, format, and test quick checks

  • Goal: ensure code quality before every commit.
  • Stack: Node.js (but concepts apply broadly).

Script: .git/hooks/pre-commit (or via a runner)

  • If using a runner like Lefthook or Husky, place this in your config instead of the .git/hooks file.

Bash version (simple):
-#!/usr/bin/env bash

- set -euo pipefail

- echo "Running pre-commit checks..."

  • # Stop on any failure
  • npx eslint . ext .js,.ts,.tsx || exit 1
  • npx prettier check "*/.{js,ts,tsx,json,css,md}" || exit 1

- npm test silent || exit 1

  • echo "Pre-commit checks passed."
  • exit 0

Rationale:

  • Ensures formatting and linting are consistent.
  • Catches failing tests before they enter the history.

2) Commit message linting: enforce conventional commits

  • Goal: require messages to follow a standard like Conventional Commits (feat:, fix:, docs:, etc.).

Script: .git/hooks/commit-msg
-#!/usr/bin/env node

  • const fs = require('fs');
  • const msgFile = process.argv || '.git/COMMIT_EDITMSG';
  • const msg = fs.readFileSync(msgFile, 'utf8').trim();
  • const conventional = /^(feat|fix|docs|style|refactor|test|perf|chore|ci|build)($$[^$$]+$$)?: .+/.test(msg);
  • if (!conventional) {
  • console.error("Commit message must follow Conventional Commits: type(scope?): subject");
  • process.exit(1);
  • }
  • console.log("Commit message looks good.");
  • process.exit(0);

Rationale:

  • Improves changelog readability and automation downstream (semver bumps, release notes).

3) Pre-push: run full test suite on push

  • Goal: avoid pushing code that breaks the build.
  • Script: .git/hooks/pre-push

Bash version:
-#!/usr/bin/env bash

- set -euo pipefail

  • echo "Running full test suite before push..."
  • npm ci silent
  • npm test silent
  • echo "All tests passed. Proceeding with push."
  • exit 0

Rationale:

  • Detects issues before collaborators pull changes.

4) Post-merge: reinstall dependencies and run tests

  • Goal: ensure post-merge branches have correct dependencies.

Script: .git/hooks/post-merge
-#!/usr/bin/env bash

  • set -euo pipefail
  • echo "Post-merge: installing dependencies..."
  • if [ -f package.json ]; then npm ci silent; fi
  • if [ -f package.json ]; then npm test silent || true; fi
  • echo "Post-merge steps complete."

Rationale:

  • Keeps environment aligned after merges, especially in mono-repos.

5) Pre-rebase: prevent rebasing public/shared branches

  • Goal: discourage rebasing branches that others rely on.

Script: .git/hooks/pre-rebase
-#!/usr/bin/env bash

  • if git rev-parse verify quiet refs/heads/main >/dev/null; then
  • echo "Rebasing the main branch is discouraged on shared repos."
  • # You can block rebases on protected branches
  • if [ "$(git symbolic-ref short HEAD)" = "main" ]; then
  • echo "Refusing to rebase main on a shared repo."
  • exit 1
  • fi
  • fi
  • exit 0

Rationale:

  • Preserves published history and reduces disruption.

    Advanced patterns: automation and safety

  • Auto-update version numbers on tagged releases

    • Hook idea: pre-push or post-commit that nudges a version bump in a dedicated file (e.g., package.json) when a release tag is created.
    • Approach: use a small script that increments a patch version in a controlled way and prompts for confirmation in multi-developer teams.
  • Enforce license headers on new files

    • Hook checks the first 10 lines of new files for a license header and rejects noncompliant files.
    • Script outline: scan git diff name-status to identify added files, then grep for header.
  • Protect sensitive data in commits

    • Hook scans staged changes for secrets or credentials (e.g., AWS keys, API keys) and rejects the commit.
    • Tools: git-secrets, detect-secrets, or custom regex checks.
  • Ensure README or docs are updated when code changes

    • Hook checks that related docs state changes exist in docs/ when certain code paths are modified.
    • Example: if you modify package.json scripts, require updating docs accordingly. ### Cross-project sharing and consistency
  • Use a standard hook template across repositories

    • Provide a shared repository with a set of ready-made hooks and a bootstrap script.
    • Onboarding: scripts/install-hooks.sh to symlink template hooks into .git/hooks.
  • Use a common linting/formatting/CI baseline

    • Align ESLint rules, Prettier configs, and CI pipelines so hooks produce consistent results across teams.
  • Document each hook

    • Create a HOOKS.md in the repo with each hook’s purpose, activation steps, and how to override locally if needed. ### Implementation tips
  • Start small

    • Pick one or two hooks that deliver the most immediate value (pre-commit for linting and commit-msg for formatting).
  • Prefer idempotent hooks

    • Hooks should be safe to run multiple times and not cause unnecessary side effects. If a hook fails, it should fail fast and give a clear error message.
  • Make hooks fast

    • Long-running hooks slow down development. Run quick checks in pre-commit and reserve heavier checks for pre-push or CI.
  • Use environment-neutral scripts

    • Avoid relying on user-specific paths. Use npx, npm run, or cross-platform tools where possible.
  • Consider contributors’ environments

    • Some developers may not have a full Node environment. Provide non-invasive defaults and optional dependencies. ### A minimal starter setup you can copy

If you want a quick starter, here’s a compact approach using a local script and a simple runner.

1) Create a hooks runner (optional)

  • mkdir -p scripts/hooks
  • cat > scripts/hooks/pre-commit.sh <<'SH'
  • #!/usr/bin/env bash
  • set -euo pipefail
  • echo "Running pre-commit: lint and format"
  • npx eslint . ext .js,.ts,.tsx || exit 1
  • npx prettier check "*/.{js,ts,tsx,json,css,md}" || exit 1
  • echo "Pre-commit checks passed"
  • SH
  • chmod +x scripts/hooks/pre-commit.sh

2) Link the hook into Git (for the first time)

  • ln -s ../../scripts/hooks/pre-commit.sh .git/hooks/pre-commit

3) Commit-msg hook example

  • cat > .git/hooks/commit-msg <<'SH'
  • #!/usr/bin/env node
  • const fs = require('fs');
  • const msg = fs.readFileSync(process.argv || '.git/COMMIT_EDITMSG', 'utf8').trim();
  • if (!/^(feat|fix|docs|style|refactor|test|chore|ci|build)($$[^)]+$$)?: .+/.test(msg)) {
  • console.error("Commit message must follow Conventional Commits format");
  • process.exit(1);
  • }
  • SH
  • chmod +x .git/hooks/commit-msg

4) Optional: adopt a hook runner

  • Install Lefthook: npm i -D lefthook
  • Create lefthook.yml to map commands to hooks
  • Example: lefthook: hooks: pre-commit:
    • npm run lint commit-msg:
    • node scripts/validate-commit-msg.js

Rationale:

  • This gives a maintainable, cross-project approach without reinventing the wheel.

    Troubleshooting tips

  • Hooks not firing

    • Ensure the hook file is executable (chmod +x) and located in .git/hooks with the correct name (e.g., pre-commit).
    • If you’re using a runner, verify the config is loaded correctly and that developers have it installed.
  • Hooks running too slowly

    • Move expensive checks to pre-push or CI.
    • Cache dependencies where possible and run only changed paths.
  • Hooks conflicting with IDEs or pre-configured workflows

    • Document how to bypass locally if needed (e.g., GIT_HOOKS_SKIP=true) and encourage teammates to use the standard workflow. ### Wrap-up

Well-chosen Git hooks can transform your development experience by catching issues early, enforcing team conventions, and automating repetitive tasks. Start with a couple of lightweight checks-like pre-commit lint/format and commit-msg style-and expand as your team's needs evolve. The key is to make hooks reliable, fast, and easy to share so everyone on the team benefits without friction.

Would you like help tailoring a hooks setup to a specific tech stack (for example, Python with pytest, or a React/TypeScript monorepo) and generating a ready-to-use set of script templates? If so, tell me your stack and any constraints (CI integration, preferred hook tool, and whether you want a shared template repo).

-

Rizwan Saleem | https://rizwansaleem.co

Top comments (0)