Git Hooks — Automate Your Workflow
Learn client-side Git hooks to run linters, validate commit messages, and block bad pushes automatically, plus how to share hooks with your team.
Code
Git Hooks Cheatsheet
Git hooks are executable scripts that Git fires at specific moments in its lifecycle. Client-side hooks live in each repo and run on the developer's machine — perfect for linting, formatting, validating commit messages, and running fast tests before code is shared. This cheatsheet covers the hooks you use most, where they live, how to share them with a team, and the popular managers (husky, lefthook).
Where hooks live
Every repo ships sample hooks in .git/hooks/. They are inactive until you create a file with the exact hook name (no extension) and make it executable.
# list the sample hooks Git created
ls .git/hooks/
# create a hook
touch .git/hooks/pre-commit
# make it executable (required, or Git silently skips it)
chmod +x .git/hooks/pre-commit
Notes:
- The file MUST be named exactly (
pre-commit,commit-msg,pre-push) with no extension. - The
.samplefiles are templates — Git never runs them. - Hooks in
.git/hooks/are NOT committed (the.gitdir is local). To share, seecore.hooksPath.
Client-side hooks at a glance
| Hook | Fires | Exit non-zero | Common use |
|---|---|---|---|
pre-commit |
before the commit is created | aborts commit | lint, format, type-check |
prepare-commit-msg |
before the editor opens | aborts commit | inject template/branch name |
commit-msg |
after message is written | aborts commit | validate message format |
pre-push |
before objects are sent | aborts push | run tests, block protected branch |
post-commit |
after the commit is made | (ignored) | notifications, logging |
A hook that exits with a non-zero status cancels the operation (except informational hooks like post-commit).
pre-commit
Runs before the commit object is created. Use it for fast checks on staged files.
#!/bin/sh
# .git/hooks/pre-commit
# Block commits that contain lint errors.
npm run lint || {
echo "Lint failed. Commit aborted."
exit 1
}
Run only on staged files (faster):
#!/bin/sh
# .git/hooks/pre-commit
# Get staged JS/TS files and run eslint on them.
files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$')
[ -z "$files" ] && exit 0
echo "$files" | xargs npx eslint || exit 1
Bypass when you must:
git commit --no-verify -m "wip"
commit-msg
Receives the path to the file holding the commit message as $1. Use it to enforce a format such as Conventional Commits.
#!/bin/sh
# .git/hooks/commit-msg
# Enforce Conventional Commits: type(scope): subject
msg_file="$1"
pattern='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf)(\(.+\))?: .{1,}'
if ! grep -qE "$pattern" "$msg_file"; then
echo "Commit message must follow Conventional Commits."
echo "Example: feat(auth): add password reset"
exit 1
fi
pre-push
Runs before refs are pushed. Reads <local-ref> <local-sha> <remote-ref> <remote-sha> lines from stdin. Use it for tests or to protect branches.
#!/bin/sh
# .git/hooks/pre-push
# Run the test suite before pushing.
npm test || {
echo "Tests failed. Push aborted."
exit 1
}
Block direct pushes to main:
#!/bin/sh
# .git/hooks/pre-push
# Refuse to push the main branch directly.
protected="main"
while read local_ref local_sha remote_ref remote_sha; do
if [ "$remote_ref" = "refs/heads/$protected" ]; then
echo "Direct push to $protected is blocked. Open a PR."
exit 1
fi
done
Bypass:
git push --no-verify
Sharing hooks with core.hooksPath
Because .git/hooks/ isn't committed, point Git at a tracked directory instead. Commit your hooks to e.g. .githooks/ and have each clone opt in once.
# create a tracked hooks directory
mkdir .githooks
mv .git/hooks/pre-commit .githooks/pre-commit
chmod +x .githooks/pre-commit
git add .githooks
git commit -m "chore: add shared git hooks"
# every teammate runs this once after cloning
git config core.hooksPath .githooks
Automate the opt-in with a setup script teammates run after cloning:
#!/bin/sh
# scripts/setup.sh
git config core.hooksPath .githooks
chmod +x .githooks/*
echo "Git hooks installed."
Reset to the default location:
git config --unset core.hooksPath
Hook managers: husky vs lefthook
Managers wire up core.hooksPath for you, keep config in one file, and make hooks installable as part of npm install.
husky (Node, JS/TS projects)
npm install --save-dev husky
npx husky init # sets core.hooksPath to .husky/ and adds a prepare script
Add a hook by writing a file in .husky/:
# .husky/pre-commit
npx lint-staged
The prepare script in package.json ("prepare": "husky") runs automatically on npm install, so hooks self-install for the whole team.
lefthook (language-agnostic, single YAML config)
npm install --save-dev lefthook # or: brew install lefthook
npx lefthook install # registers hooks via core.hooksPath
# lefthook.yml
pre-commit:
parallel: true
commands:
lint:
glob: "*.{js,ts,jsx,tsx}"
run: npx eslint {staged_files}
format:
glob: "*.{js,ts,css,md}"
run: npx prettier --check {staged_files}
pre-push:
commands:
tests:
run: npm test
Quick comparison:
- husky — minimal, Node-centric, one shell file per hook; pairs with
lint-staged. - lefthook — single YAML, parallel execution,
{staged_files}templating, works for any language (Go, Ruby, Python, etc.).
Common workflows
1. Lint and format only staged files (husky + lint-staged)
npm install --save-dev husky lint-staged
npx husky init
echo "npx lint-staged" > .husky/pre-commit
// package.json
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,md,json}": ["prettier --write"]
}
}
2. Enforce Conventional Commits team-wide (shared hook)
mkdir .githooks
cat > .githooks/commit-msg <<'EOF'
#!/bin/sh
pattern='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf)(\(.+\))?: .{1,}'
grep -qE "$pattern" "$1" || { echo "Use Conventional Commits."; exit 1; }
EOF
chmod +x .githooks/commit-msg
git config core.hooksPath .githooks
git add .githooks && git commit -m "chore: add commit-msg hook"
3. Run tests before every push
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
npm test || exit 1
EOF
chmod +x .git/hooks/pre-push
4. Cross-language checks with lefthook
npx lefthook install
# edit lefthook.yml, then verify a single hook without committing:
npx lefthook run pre-commit
Gotchas / tips
- Hooks must be executable (
chmod +x) — a non-executable hook is silently ignored. - Shebang matters: start with
#!/bin/shor#!/usr/bin/env bash. On Windows, Git Bash runs them, but plain shell is the safest portable choice. - Hooks in
.git/hooks/are not version-controlled — usecore.hooksPathor a manager to share them. --no-verify(-n) skipspre-commit,prepare-commit-msg,commit-msg, andpre-push. Don't rely on hooks as your only line of defense — also enforce in CI.- Keep
pre-commitfast; run on staged files only. Heavy checks (full test suites) belong inpre-pushor CI. commit-msgreceives the message file path as$1;pre-pushreceives ref info on stdin, not arguments.git commit --amendalso triggerspre-commitandcommit-msg.- A merge commit skips
pre-commitby default; usepre-merge-commitif you need to hook merges. - Switching managers? Run the new tool's install command — it rewrites
core.hooksPath, overriding any previous setup.
Git Hooks — Automate Your Workflow
Git hooks are scripts Git runs automatically at key points in your workflow — before a commit, when a commit message is written, or before a push. They let you enforce quality gates (lint, format, tests, message conventions) without relying on anyone to remember.
What this skill covers
- The client-side hooks you actually use:
pre-commit,commit-msg,pre-push - Where hooks live (
.git/hooks) and how to make them executable - Sharing hooks across a team with
core.hooksPath - When to reach for a manager like husky or lefthook
When to use it
Reach for hooks whenever you want a check to run consistently and locally before code leaves a developer’s machine: blocking commits with lint errors, rejecting non-conventional commit messages, or running a quick test suite before a push.
Cheatsheet
The full copy-pasteable reference — commands, hook examples, end-to-end workflows, and gotchas — lives in the Markdown snippet attached to this resource.