Featured image of post Git Hooks for Automation: Beyond Code Quality Featured image of post Git Hooks for Automation: Beyond Code Quality

Git Hooks for Automation: Beyond Code Quality

Explore Git hooks for automation covering pre-commit, pre-push, commit-msg hooks, husky, lint-staged, custom scripts, shared hooks, and CI equivalence.

Introduction

Git hooks are executable scripts that run automatically at specific points in the Git lifecycle. While many developers associate them solely with code formatting and linting, their potential extends far beyond—into workflow automation, project management, and team collaboration. This article explores how to leverage Git hooks for comprehensive automation strategies.

Understanding Git Hooks

Git hooks reside in the .git/hooks/ directory of every repository. They are standard executable scripts written in any language—bash, Python, Node.js, or Ruby. Hooks fall into two categories: client-side hooks (pre-commit, pre-push, commit-msg) that run on a developer’s machine, and server-side hooks (pre-receive, update, post-receive) that execute on the remote repository.

The hook lifecycle follows Git operations chronologically. During a commit, hooks execute in this order: pre-commitcommit-msgpost-commit. For a push, the sequence is pre-push (client) → pre-receiveupdatepost-receive (server).

Setting Up Hooks with Husky

Managing hooks manually by placing scripts in .git/hooks/ is impractical for teams because that directory is not tracked by Git. Husky solves this by allowing hooks to be configured within the repository and shared across the team.

Installation with Husky v9+ is straightforward:

npx husky init

This creates a .husky/ directory where hook scripts are plain text files:

# .husky/pre-commit
npx lint-staged

For CI environments, hooks can be skipped by setting HUSKY=0:

HUSKY=0 git push

Integrating lint-staged

Lint-staged runs linters only on staged files, keeping pre-commit checks fast:

{
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{css,scss}": ["stylelint --fix", "prettier --write"]
  }
}

Pre-Commit Hooks: The First Line of Defense

Pre-commit hooks execute before each commit and are ideal for fast, localized checks.

Check TypeToolsPurpose
LintingESLint, stylelintEnforce code style standards
FormattingPrettier, dprintAuto-format code consistently
Type checkingTypeScript --noEmitCatch type errors early
Secret detectionsecretlint, truffleHogPrevent credential leaks
File validationCustom scriptsBlock large files or TODO commits

A comprehensive pre-commit configuration:

# .husky/pre-commit
npx lint-staged
npx tsc --noEmit
npx secretlint "**/*"

Performance is critical—pre-commit hooks should complete in under five seconds to avoid disrupting the development flow.


Pre-Push Hooks: The CI Safety Net

Pre-push hooks run before code reaches the remote repository, making them ideal for longer-running validations:

# .husky/pre-push
npm test
npm run build
npm audit

Branch naming validation ensures consistency across the team:

#!/bin/sh
branch=$(git rev-parse --abbrev-ref HEAD)
if ! echo "$branch" | grep -qE '^(feature|bugfix|hotfix)/[A-Z]+-[0-9]+-'; then
  echo "Error: Branch name must match feature/ABC-123-description"
  exit 1
fi

Commit-Message Hooks with commitlint

Enforcing conventional commit messages ensures a consistent, parseable history. Using commitlint with Husky:

# .husky/commit-msg
npx --no -- commitlint --edit $1

Configure commitlint in commitlint.config.js:

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'subject-max-length': [2, 'always', 50],
    'body-max-line-length': [2, 'always', 72],
  },
};

Valid commit messages follow the <type>(<scope>): <description> format:

feat(auth): implement OAuth 2.0 refresh token rotation
fix(api): handle null pointer in user deserialization
docs(readme): update installation instructions

Post-Commit and Post-Merge Hooks

Post-merge hooks are particularly useful for dependency management. Automatically install updated dependencies when lock files change:

# .husky/post-merge
changed=$(git diff HEAD~1 --name-only)
if echo "$changed" | grep -q "pnpm-lock.yaml"; then
  pnpm install
fi

CI Equivalence Strategy

The most robust pattern ensures hooks and CI run identical checks. Extract shared scripts that both environments call:

#!/bin/sh
# scripts/validate.sh - called by both hooks and CI
npm run lint
npm run typecheck
npm test

Local hooks should warn or auto-fix where possible, while CI enforces strictly. This creates a safety net: developers catch issues before push, and CI catches anything that slips through.


Conclusion

Git hooks are a powerful automation layer that extends far beyond code formatting. Combining Husky with lint-staged provides the recommended baseline for most projects. Pre-push hooks add a safety net before code reaches CI, and commitlint ensures a clean, parseable commit history. The ultimate goal is a seamless developer experience where automation handles repetitive checks, allowing teams to focus on logic and design.