Git Hooks and Husky: Automate Checks with pre-commit, commit-msg and pre-push

Git hooks run your checks automatically at the right moment, before a commit or before a push. In this lesson you will write your first hook, share hooks with your team using Husky, run linters on staged files and validate commit messages.

Advertisement

📖 What You’ll Learn: what hooks are, the main client-side hooks, writing a pre-commit hook, bypassing hooks and why CI still matters, sharing hooks with core.hooksPath and Husky, lint-staged, commitlint, the Python pre-commit framework, and server-side alternatives on GitHub.


🪝 What Are Git Hooks?

Git hooks are scripts that Git runs automatically at certain moments: before a commit, after a checkout, before a push and more. Teams use them to catch problems before they reach the repository:

  • Run the linter and formatter on staged files before each commit.
  • Reject commit messages that do not follow Conventional Commits.
  • Run fast tests before pushing.
  • Block commits that contain test.only or secrets.

📂 Where Hooks Live

Every repository has a .git/hooks folder with sample files such as pre-commit.sample. To enable a hook, create a file with the exact hook name (no extension) and make it executable.

HookRunsCan block the actionTypical use
pre-commitBefore the commit is created✅Lint, format, quick checks
prepare-commit-msgBefore the message editor opens✅Add a ticket number from the branch name
commit-msgAfter the message is written✅Validate the message format
post-commitAfter the commit❌Notifications
pre-pushBefore pushing✅Run tests
post-checkout, post-mergeAfter switching branches or merging❌Reinstall dependencies

A hook blocks the action when it exits with a non-zero code.


✍️ Write Your First Hook

Block commits that contain test.only (a Playwright focus left in by mistake):

#!/bin/sh
# .git/hooks/pre-commit
if git diff --cached --name-only | grep -E '\.(ts|js)$' | xargs -r grep -n "test\.only" 2>/dev/null; then
  echo "❌ Remove test.only before committing."
  exit 1
fi
exit 0
chmod +x .git/hooks/pre-commit

Now git commit fails if a staged TypeScript or JavaScript file contains test.only. On Windows, hooks run in Git Bash, so shell scripts work.

📝 Note: Anyone can skip client-side hooks with git commit --no-verify (or -n). Hooks are a convenience and safety net; the real enforcement belongs in CI and branch protection.


🤝 Sharing Hooks with the Team

Files in .git/hooks are not committed, so teammates do not get them. Two ways to share hooks:

Option 1: core.hooksPath

Commit a folder such as .githooks/ and point Git at it:

git config core.hooksPath .githooks

Each developer runs this once (or a setup script does it).

Option 2: Husky (JavaScript and TypeScript projects)

Husky installs hooks automatically when teammates run npm install:

npm install -D husky
npx husky init

husky init creates a .husky/ folder with a pre-commit hook (initially running npm test) and adds a prepare script to package.json so hooks are set up on every install. Edit the hook files as plain shell commands:

# .husky/pre-commit
npx lint-staged
# .husky/pre-push
npx playwright test --grep @smoke

🎯 lint-staged: Check Only What You Changed

Running the linter on the whole project before every commit is slow. lint-staged runs commands only on staged files:

npm install -D lint-staged
{
  "lint-staged": {
    "*.{ts,js}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yml}": "prettier --write"
  }
}

Put this in package.json. With npx lint-staged in the pre-commit hook, every commit is automatically linted and formatted.


✅ Validate Commit Messages

Combine Husky with commitlint:

npm install -D @commitlint/cli @commitlint/config-conventional
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.mjs
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

Now git commit -m "stuff" is rejected with a clear explanation.


🐍 Python Projects: pre-commit Framework

Python teams often use the pre-commit framework instead of Husky:

pip install pre-commit
pre-commit install          # installs the hook defined in .pre-commit-config.yaml
pre-commit run --all-files

It manages hooks such as Black, Ruff, trailing whitespace and secret detection from a YAML config committed to the repository.


🖥️ Server-Side Hooks

Hooks like pre-receive run on the server when someone pushes. GitHub.com does not let you install custom server-side hooks; use rulesets and branch protection, required status checks in GitHub Actions, and push protection for secrets (security) instead.


📝 Assignment

  1. Write a pre-commit hook in .git/hooks that blocks commits containing the word console.log, and test it.
  2. Bypass it once with --no-verify and explain why CI is still needed.
  3. Move the hook to a committed .githooks folder and use core.hooksPath.
  4. In a Node or Playwright project, install Husky and lint-staged and run Prettier on staged files.
  5. Add commitlint and try to commit a badly formatted message.
  6. Add a pre-push hook that runs your @smoke tests.

❓ FAQ

🤔 What are Git hooks?

Scripts that Git runs automatically at points such as before a commit, after a commit or before a push, used to automate checks.

🤔 What is a pre-commit hook?

A hook that runs before a commit is created; if it exits with a non-zero code, the commit is cancelled. It is commonly used for linting and formatting.

🤔 What is Husky?

A tool for JavaScript and TypeScript projects that stores Git hooks in the repository and installs them automatically for every developer via npm.

🤔 How do I skip Git hooks?

git commit --no-verify (or -n) skips pre-commit and commit-msg hooks; git push --no-verify skips pre-push.

🤔 Are Git hooks shared when I push?

Not automatically. Share them with core.hooksPath, Husky or the pre-commit framework.


🎯 Next Steps

🎬 Learn Git and GitHub step by step: watch the series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

1 thought on “Git Hooks and Husky: Automate Checks with pre-commit, commit-msg and pre-push”

Leave a Comment