|$ curl https://forge-ai.dev/api/markdown?path=docs/git/hooks
$cat docs/git-—-hooks-&-automation.md
updated Recently·12 min read·published

Git — Hooks & Automation

GitIntermediate
Introduction

Git hooks are scripts that run automatically at specific points in Git's workflow. They let you enforce policies, run checks, and automate tasks — from preventing commits with lint errors to deploying code after pushes.

This guide covers both client-side and server-side hooks, writing custom hook scripts, and modern tooling like husky, lint-staged, and commitlint.

Client-Side Hooks

Client-side hooks run on your local machine during Git operations. They are stored in .git/hooks/ and are not shared with the repository by default. The most commonly used hooks are:

HookTriggerCommon Use
pre-commitBefore commit message editorLint code, run tests, check formatting
prepare-commit-msgAfter default message is createdAdd issue numbers, templates
commit-msgAfter commit message is editedValidate commit message format
post-commitAfter commit completesNotifications, CI triggers
pre-pushBefore push to remoteRun full test suite, check secrets
post-mergeAfter merge completesInstall dependencies, run migrations
pre-rebaseBefore rebasingEnsure branch is not shared

Hooks are executable scripts named exactly after the hook event. They receive input via stdin and command-line arguments, and they can stop the Git operation by exiting with a non-zero status.

terminal
Bash
1# Example: Simple pre-commit hook (.git/hooks/pre-commit)
2#!/bin/sh
3echo "Running linter..."
4npm run lint
5if [ $? -ne 0 ]; then
6 echo "Linting failed. Commit aborted."
7 exit 1
8fi
9
10# Make the hook executable (required!)
11chmod +x .git/hooks/pre-commit

info

Hooks must be executable files. If your hook script doesn't run, check its permissions with ls -l .git/hooks/. The file should have -rwxr-xr-x permissions. Run chmod +x .git/hooks/* to fix them.
Server-Side Hooks

Server-side hooks run on the remote repository (e.g., GitHub, GitLab, or your own Git server) when receiving pushes. They are useful for enforcing team-wide policies.

HookTriggerUse Case
pre-receiveBefore any refs are updatedBlock force pushes, check file sizes
updateBefore each ref is updatedPer-branch policies, fast-forward only
post-receiveAfter all refs are updatedDeploy, CI, notifications, changelog
bash
Bash
1# Example: post-receive hook for deployment
2#!/bin/sh
3# Deploy main branch to production
4while read oldrev newrev refname
5do
6 if [ "$refname" = "refs/heads/main" ]; then
7 echo "Deploying main to production..."
8 git --work-tree=/var/www/app checkout -f main
9 cd /var/www/app && npm install && pm2 restart app
10 echo "Deployment complete."
11 fi
12done
13
14# Install as server-side hook:
15# On your Git server, place the script at:
16# /path/to/repo.git/hooks/post-receive
17# Make it executable: chmod +x .../post-receive
Writing Custom Hook Scripts

Hooks can be written in any scripting language (bash, Python, Node.js, Ruby, etc.) as long as the file is executable. Each hook receives specific input that you can use to make decisions.

pre-push
Bash
1# pre-push hook — run tests before push
2#!/bin/sh
3remote="$1"
4url="$2"
5
6echo "Running tests before push..."
7npm test || exit 1
8
9# Check for sensitive data
10if git diff --cached -S"API_KEY" --quiet; then
11 echo "Error: Found potential API key in staged changes!"
12 exit 1
13fi
14
15exit 0
commit-msg
Bash
1# commit-msg hook — validate commit message format
2#!/bin/sh
3commit_msg_file="$1"
4commit_msg=$(cat "$commit_msg_file")
5
6# Check for conventional commit format
7if ! echo "$commit_msg" | grep -qE "^(feat|fix|chore|docs|style|refactor|perf|test|ci)((.+))?!?: .{1,}$"; then
8 echo "ERROR: Commit message must follow Conventional Commits format"
9 echo "Examples:"
10 echo " feat(auth): add login page"
11 echo " fix(api): handle null response"
12 echo " docs: update README"
13 exit 1
14fi
15
16# Check message length
17if [ ${#commit_msg} -gt 72 ]; then
18 echo "ERROR: Commit message must be 72 characters or less"
19 exit 1
20fi
21
22exit 0
husky — Modern Git Hooks

husky is the most popular tool for managing Git hooks in Node.js projects. It makes hooks configurable via package.json or .husky/ directory, and ensures hooks are shared with everyone who installs the project.

terminal
Bash
1# Install husky (v9+)
2npm install --save-dev husky
3
4# Initialize husky (creates .husky/ directory)
5npx husky init
6
7# This creates .husky/pre-commit with:
8# npx lint-staged
9
10# Add a hook
11npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
12
13# Manually create a hook file (.husky/pre-push)
14#!/bin/sh
15. "$(dirname "$0")/_/husky.sh"
16npm test
17
18# The .husky/ directory structure:
19# .husky/
20# _/ (husky internal scripts)
21# pre-commit (runs before commit)
22# commit-msg (validates commit message)
23# pre-push (runs before push)
24
25# Package.json integration (husky v4 style — older)
26# "husky": {
27# "hooks": {
28# "pre-commit": "lint-staged",
29# "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
30# }
31# }
32
33# Modern approach (husky v9+):
34# Hooks are files in .husky/ directory,
35# committed to the repository and shared with the team.
🔥

pro tip

husky v9+ stores hooks in the .husky/ directory, which you commit to your repository. This ensures all team members automatically get the same hooks without manual setup. Previous versions stored configuration in package.json and required running npm install to activate hooks.
lint-staged — Lint Staged Files

lint-staged runs linters only on files staged for commit. This is dramatically faster than running the linter on the entire project and ensures that only the code you're about to commit meets quality standards.

terminal
Bash
1# Install lint-staged
2npm install --save-dev lint-staged
3
4# Configure in package.json:
5{
6 "lint-staged": {
7 "*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
8 "*.{json,md,css}": ["prettier --write"],
9 "*.py": ["black --check", "flake8"]
10 }
11}
12
13# Works with husky pre-commit hook:
14# .husky/pre-commit:
15# npx lint-staged
16
17# What happens when you commit:
18# 1. lint-staged gets the list of staged files
19# 2. Files matching patterns get the configured commands
20# 3. If any command fails, the commit is aborted
21# 4. Files are re-staged after formatting changes
22
23# Use with git add:
24git add src/app.js src/utils.js
25git commit -m "feat: add user dashboard"
26# lint-staged automatically runs on the two staged files

info

Combine lint-staged with prettier --writeto auto-format code on commit. This eliminates formatting discussions in code reviews — every committed file is automatically formatted to your project's standards.
Commit Conventions & commitlint

Consistent commit messages make history readable and enable automated changelog generation, semantic versioning, and release notes. The most widely adopted convention is Conventional Commits.

conventional-commits.md
TEXT
1# Conventional Commits format:
2<type>[optional scope]: <description>
3
4[optional body]
5
6[optional footer(s)]
7
8# Types:
9feat: A new feature
10fix: A bug fix
11docs: Documentation only changes
12style: Formatting, missing semicolons, etc.
13refactor: Code change that neither fixes a bug nor adds a feature
14perf: Performance improvement
15test: Adding or fixing tests
16chore: Build process, dependencies, etc.
17ci: CI/CD configuration changes
18
19# Examples:
20feat(auth): add OAuth login flow
21fix(api): handle 429 rate limit response
22docs: update API reference for v2
23refactor(core): extract database connection pool
24perf(cache): reduce TTL on user sessions
25ci: add deployment to staging environment
26
27# Breaking changes:
28feat(api)!: remove deprecated /v1/endpoint
29
30BREAKING CHANGE: The /v1/ endpoint has been removed.
31Use /v2/ endpoints instead.

commitlint enforces these conventions by checking commit messages against a configurable set of rules.

terminal
Bash
1# Install commitlint
2npm install --save-dev @commitlint/cli @commitlint/config-conventional
3
4# Configure commitlint:
5echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
6
7# Or use a file-based config (.commitlintrc.js or .commitlintrc.json)
8# {
9# "extends": ["@commitlint/config-conventional"],
10# "rules": {
11# "type-enum": [2, "always", ["feat", "fix", "docs", "chore"]],
12# "subject-case": [2, "always", "sentence-case"]
13# }
14# }
15
16# Use with husky commit-msg hook:
17# .husky/commit-msg:
18npx --no -- commitlint --edit "$1"
19
20# If the commit message doesn't follow conventions:
21# ⧗ input: oops forgot to lint
22# ✖ subject must not be sentence-case [subject-case]
23# ✖ type must be one of [feat, fix, docs, style, ...] [type-enum]
24# ✖ found 2 problems, 0 warnings

best practice

Adopt Conventional Commits as a team. Combined with commitlint and husky, you get automatic enforcement, readable history, and free changelog generation through tools like standard-version or semantic-release.
Shared Hooks Across a Team

Since .git/hooks/ is not committed to the repository, teams need a strategy for sharing hooks. Here are the most common approaches:

husky v9+ (Recommended)

Store hooks in .husky/ and commit them. Team members get hooks automatically when they clone and run npm install. This is the most maintainable approach.

Git Template Directory

Create a template repository with hooks and configure git config --global init.templateDir. Every new clone or git init will automatically include the hooks.

Hook Management Script

Include a setup script (e.g., npm run setup-hooks) that copies hooks from a hooks/ directory in the repo to .git/hooks/ and makes them executable.

Best Practices

Keep Hooks Fast

Developers will disable hooks that take too long. Run fast checks (linting changed files) in pre-commit and leave slow checks (full test suite) for pre-push or CI.

Provide a Bypass Mechanism

Allow developers to skip hooks with git commit --no-verify or git push --no-verify for emergencies. Trust your team — hooks are guardrails, not prison bars.

Use Node.js for Complex Logic

For hooks with complex logic, write them in Node.js instead of bash. They are easier to test, maintain, and debug. husky handles the invocation, and your script can use any npm packages.

Version Your Hooks

Keep hook scripts under version control. Whether you use husky, a hooks/ directory, or a template, ensure hooks are tracked, reviewed, and updated like any other code.

$Blueprint — Engineering Documentation·Section ID: GIT-HOOKS·Revision: 1.0