
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Broken builds, leaked secrets, and non-compliant commit messages waste engineering time and erode trust in production systems. Implementing Git Hooks: Automate Checks Before Commit and Push moves validation left, catching errors on the developer's machine before they ever reach the remote repository or CI pipeline. This guide covers the practical configuration of client-side hooks using modern tooling like Husky and pre-commit, ensuring your team maintains high standards without relying solely on server-side rejections.
What Are Git Hooks and Why Automate Checks Before Commit?
Git hooks are executable scripts triggered automatically by Git events. They reside in the .git/hooks directory but are not version-controlled by default, which is why we use wrapper tools to share them across teams. When you understand Git Hooks: Automate Checks Before Commit and Push, you effectively create a local quality gate that mirrors your CI/CD pipeline. For teams adopting CI/CD best practices for small teams, this reduces feedback loops from minutes to seconds.
In my experience auditing infrastructure for SOC 2 compliance, organizations that rely exclusively on server-side checks often face "compliance fatigue." Developers push code, wait for CI to fail, fix it, and push again. By enforcing standards locally, you reduce noise in your audit logs and ensure that every commit landing in main already meets baseline security and formatting criteria. This is especially relevant when managing secrets management with HashiCorp Vault, where preventing accidental credential commits is far superior to rotating keys after a leak.
How Do You Configure Pre-Commit Hooks for Code Quality?
The pre-commit hook runs after you execute git commit but before the commit object is created. If the script exits with a non-zero status, the commit is aborted. This is your primary defense against style violations, unformatted code, and basic syntax errors.
Setting Up Husky for Node.js Projects
For JavaScript and TypeScript ecosystems, Husky is the industry standard. It simplifies hook installation and ensures hooks persist across clones.
# Install Husky as a dev dependency
npm install -D husky
# Initialize Husky (creates .husky/ directory and updates package.json)
npx husky init
# Add a lint-staged configuration to package.json
# This ensures only staged files are checked, improving performance
{
"lint-staged": {
"*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.css": ["stylelint --fix", "prettier --write"]
}
} A common mistake I see in Nepal’s growing tech scene is running linters against the entire codebase on every commit. Always scope checks to staged files using tools like lint-staged. On legacy monorepos, full-suite linting can take minutes; scoped checks take seconds. This distinction determines whether developers embrace or bypass your automation.
Using Pre-Commit Framework for Polyglot Repos
If your repository contains Python, Terraform, Go, or shell scripts alongside application code, the language-agnostic pre-commit framework is superior. Define a .pre-commit-config.yaml:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: detect-private-key # Critical for security compliance
- repo: https://github.com/terraform-linters/tflint
rev: v0.50.3
hooks:
- id: terraform_fmt
- id: terraform_validate This configuration enforces whitespace consistency and validates Terraform syntax before any infrastructure-as-code change is committed. When practicing infrastructure as code with Terraform, catching HCL formatting drift locally prevents state file inconsistencies during collaborative deployments.
Which Git Hook Enforces Commit Message Standards?
While pre-commit validates file content, the commit-msg hook validates the commit message itself. Consistent messaging is vital for automated changelogs, semantic versioning, and audit trails. In regulated environments, traceability between code changes and ticket IDs is often a mandatory control.
I recommend commitlint paired with Conventional Commits. Create a commitlint.config.js:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'build', 'ci', 'chore', 'revert'
]],
'subject-empty': [2, 'never'],
'header-max-length': [2, 'always', 72]
}
}; Add the hook via Husky: npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1". This enforces structured messages like feat(auth): add OAuth2 provider support. For teams in Nepal working with international clients, adhering to global conventions like this signals professionalism and makes your repository compatible with automated release tools like Semantic Release or Changesets.
When Should You Use Pre-Push Hooks for Security Scanning?
The pre-push hook executes after all commits are ready but before data transfers to the remote. This is your last line of defense. Use it for checks that are too slow for pre-commit or require broader context than individual files.
- Full test suites: Running unit tests on every commit is fast; running integration tests is not. Reserve heavier test batches for pre-push.
- Secret scanning: Tools like
gitleaksortrufflehogscan history and diffs for credentials. Whiledetect-private-keycatches obvious patterns in pre-commit, deeper entropy analysis belongs here. - Branch protection validation: Prevent direct pushes to
mainorproductionbranches locally before wasting bandwidth.
#!/bin/sh
# .husky/pre-push
# Block direct pushes to protected branches
protected_branches="^(main|master|production)$"
current_branch=$(git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,')
if echo "$current_branch" | grep -qE "$protected_branches"; then
echo "❌ Direct push to '$current_branch' is blocked."
echo " Please use a feature branch and pull request."
exit 1
fi
# Run gitleaks on outgoing commits
echo "