Git Hooks for Automation: Catch Mistakes Before They Embarrass You
Learn how to use Git hooks (pre-commit, commit-msg, pre-push) to automatically validate code quality, enforce commit standards, and prevent broken code from reaching your repository.
This post is part of our Git workflow series. For the complete branching strategy and team workflow, see Git Branching Strategy for Small Teams.
Why Use Git Hooks?
Remember that time you committed code with a console.log, pushed it, and CI failed? Or forgot to run tests locally and broke the build? Or wrote a commit message like “fix stuff” and your team lead gave you that look?
Git hooks prevent all of that. They’re automated scripts that run at specific points in your Git workflow - before commits, before pushes, after merges, etc. Think of them as your personal code review bouncer that catches mistakes before they embarrass you.
What hooks actually prevent:
Bad code hitting the repo
- Linting errors (trailing whitespace, unused imports, formatting issues)
- Failing tests (unit tests, integration tests)
- Security issues (hardcoded secrets, vulnerable dependencies)
- Build failures (syntax errors, type errors)
Messy commit history
- Invalid commit message formats (
fix stuff→fix: resolve login timeout) - Missing issue references (enforces
feat: add dashboard (#123)) - Commits to protected branches (prevents direct commits to
main)
- Invalid commit message formats (
Wasted CI/CD minutes
- If hooks catch issues locally, CI doesn’t even run
- Saves time, money, and embarrassment
- Faster feedback loop (seconds vs minutes)
Team inconsistency
- Everyone runs the same checks automatically
- No “it works on my machine” when you skipped linting
- Enforces team standards without manual reminders
The hooks we actually use:
| Hook | When It Runs | What It Checks | Skip It? |
|---|---|---|---|
pre-commit | Before commit is saved | Linting, formatting, tests | git commit --no-verify |
commit-msg | After commit message written | Message format, length | git commit --no-verify |
pre-push | Before pushing to remote | Full test suite, build | git push --no-verify |
Pro tip: You can skip hooks with --no-verify, but only do it if you know what you’re doing. Like when fixing a typo in documentation and you don’t want to wait for the full test suite.
Here’s how to set them up:
Pre-commit Hook
Automatically check code quality before commits:
#!/bin/bash
# .git/hooks/pre-commit
echo "Running pre-commit checks..."
# 1. Lint code
npm run lint
if [ $? -ne 0 ]; then
echo "❌ Linting failed. Please fix errors before committing."
exit 1
fi
# 2. Run tests
npm test
if [ $? -ne 0 ]; then
echo "❌ Tests failed. Please fix failing tests before committing."
exit 1
fi
# 3. Check commit message format (if using commitlint)
npm run commitlint --edit $1
if [ $? -ne 0 ]; then
echo "❌ Commit message doesn't follow convention."
exit 1
fi
echo "✅ Pre-commit checks passed!"
exit 0Commit Message Hook
Validate commit message format:
#!/bin/bash
# .git/hooks/commit-msg
commit_msg=$(cat "$1")
# Check format: <type>(<scope>): <subject>
if ! echo "$commit_msg" | grep -qE "^(feat|fix|docs|style|refactor|perf|test|chore|ci|build)(\(.+\))?: .{1,50}"; then
echo "❌ Invalid commit message format!"
echo ""
echo "Format: <type>(<scope>): <subject>"
echo ""
echo "Types: feat, fix, docs, style, refactor, perf, test, chore, ci, build"
echo ""
echo "Example: feat(auth): add JWT authentication"
exit 1
fi
echo "✅ Commit message format is valid!"
exit 0Using Husky for Git Hooks
Instead of manually creating hook files in .git/hooks/, use Husky for easier hook management across your team:
# Install husky
npm install --save-dev husky
# Initialize husky
npx husky install
# Add pre-commit hook
npx husky add .husky/pre-commit "npm test"
# Add commit-msg hook
npx husky add .husky/commit-msg "npx commitlint --edit $1"Why Husky?
- Shareable hooks -
.git/hooks/is not tracked by Git, but.husky/is - Easy installation - New team members get hooks automatically with
npm install - Cross-platform - Works on Windows, Mac, and Linux
- Simple syntax - Just add scripts, no bash knowledge needed
Add this to your package.json to install hooks automatically:
{
"scripts": {
"prepare": "husky install"
}
}Now npm install automatically sets up hooks for new developers.
Integrating Hooks with CI/CD
Git hooks work best when paired with CI/CD. The hooks catch issues locally (fast feedback), while CI/CD catches anything that slipped through (final validation).
The strategy:
- Pre-commit hook → Quick checks (linting, fast unit tests)
- Pre-push hook → Slower checks (full test suite)
- CI/CD pipeline → Everything + deployment
This way, you catch most issues in seconds locally, but still have CI/CD as a safety net.
Example GitLab CI/CD pipeline:
# .gitlab-ci.yml
stages:
- lint
- test
- build
- deploy
# Run on all branches
lint:
stage: lint
script:
- npm run lint
only:
- branches
# Run on all branches
test:
stage: test
script:
- npm test
only:
- branches
# Build on all branches
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
only:
- branches
# Deploy develop → dev environment
deploy_dev:
stage: deploy
script:
- npm run deploy:dev
environment:
name: development
url: https://dev.example.com
only:
- develop
# Deploy staging → staging environment
deploy_staging:
stage: deploy
script:
- npm run deploy:staging
environment:
name: staging
url: https://staging.example.com
only:
- staging
# Deploy main → production (manual)
deploy_production:
stage: deploy
script:
- npm run deploy:prod
environment:
name: production
url: https://example.com
only:
- main
when: manualThe workflow:
- Developer makes changes
- Pre-commit hook runs → Catches linting errors in 5 seconds
- Developer fixes issues, commits again
- Pre-push hook runs → Catches failing tests in 30 seconds
- Developer pushes to remote
- CI/CD pipeline runs → Full validation + deployment
If your pre-commit hook already runs linting and tests, your CI/CD should still run them too (safety net), but consider making local checks faster by only running changed files:
# Pre-commit: Only lint changed files (fast)
npx lint-staged
# CI/CD: Lint entire codebase (thorough)
npm run lintCommon Hook Patterns
1. Prevent Commits to Protected Branches
Stop developers from accidentally committing directly to main:
#!/bin/bash
# .git/hooks/pre-commit
branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')
if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then
echo "❌ Direct commits to $branch are not allowed!"
echo "Create a feature branch instead:"
echo " git checkout -b feature/my-feature"
exit 1
fi2. Check for Hardcoded Secrets
Prevent accidentally committing API keys or passwords:
#!/bin/bash
# .git/hooks/pre-commit
# Check for common secret patterns
if git diff --cached | grep -qE '(api_key|API_KEY|password|PASSWORD|secret|SECRET)\s*='; then
echo "❌ Possible hardcoded secret detected!"
echo "Use environment variables instead."
exit 1
fi3. Auto-Format Code Before Commit
Automatically format code with Prettier before committing:
#!/bin/bash
# .git/hooks/pre-commit
# Format staged files
npx prettier --write $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx|css|md)$')
# Re-stage formatted files
git add $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx|css|md)$')4. Require Issue Number in Commit Message
Enforce linking commits to issue tracker:
#!/bin/bash
# .git/hooks/commit-msg
commit_msg=$(cat "$1")
# Check for issue number (#123)
if ! echo "$commit_msg" | grep -qE '\(#[0-9]+\)'; then
echo "❌ Commit message must reference an issue number!"
echo "Example: feat: add user login (#123)"
exit 1
fiTroubleshooting Hooks
Hook Not Running
Problem: Created hook but it’s not executing.
Solutions:
- Make hook executable:
chmod +x .git/hooks/pre-commit - Check shebang line:
#!/bin/bashmust be first line - Verify hook name:
pre-commitnotpre-commit.sh
Hook Runs But Always Fails
Problem: Hook script has errors.
Debug:
# Run hook manually to see errors
bash .git/hooks/pre-commit
# Add debug output
echo "Debug: Current directory is $(pwd)"
echo "Debug: Git status:"
git statusNeed to Skip Hook Temporarily
Solution:
# Skip pre-commit and commit-msg hooks
git commit --no-verify -m "fix: emergency hotfix"
# Skip pre-push hook
git push --no-verifyWarning: Only use --no-verify when absolutely necessary. You’re bypassing safety checks.
Hooks Work Locally But Not for Team
Problem: Hooks in .git/hooks/ aren’t tracked by Git.
Solution: Use Husky (see Using Husky section above). Husky stores hooks in .husky/ which IS tracked by Git.
Conclusion
Git hooks are your first line of defense against bad code reaching your repository. Set them up once, and they’ll save you countless hours of debugging CI failures and fixing messy commits.
Quick setup checklist:
- Install Husky:
npm install --save-dev husky - Initialize hooks:
npx husky install - Add pre-commit hook for linting:
npx husky add .husky/pre-commit "npm run lint" - Add commit-msg validation:
npx husky add .husky/commit-msg "npx commitlint --edit $1" - Add auto-install to package.json:
"prepare": "husky install" - Test hooks: Make a commit and verify they run
- Commit
.husky/directory to share with team
Now your hooks will catch mistakes locally before they embarrass you in code review or break CI. You’re welcome.
Hands-On Implementation Guide
Want to see a real-world implementation of these concepts? Check out our practical guide that walks through setting up Husky, lint-staged, and commitlint in an actual Astro blog project:
Implementing Git Hooks with Husky: A Real-World Example →
The guide includes:
- Step-by-step Husky installation with actual terminal output
- Configuring commitlint for conventional commits
- Setting up lint-staged for faster pre-commit checks (6-10x speedup!)
- Integrating hooks with GitLab CI/CD pipeline
- Troubleshooting based on real issues we encountered
- Before/after metrics showing time savings
This is the implementation we use on blog.stack101 - battle-tested and production-ready.
Related reading:
- Git Branching Strategy for Small Teams - Complete workflow guide this post is part of
- How to Fix: Accidentally Committed to Main Branch - Recover from common Git mistakes
Share this post
Found this helpful? Share it with your network!
