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.
📖 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.onlyor 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.
| Hook | Runs | Can block the action | Typical use |
|---|---|---|---|
pre-commit | Before the commit is created | ✅ | Lint, format, quick checks |
prepare-commit-msg | Before the message editor opens | ✅ | Add a ticket number from the branch name |
commit-msg | After the message is written | ✅ | Validate the message format |
post-commit | After the commit | ❌ | Notifications |
pre-push | Before pushing | ✅ | Run tests |
post-checkout, post-merge | After 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
- Write a
pre-commithook in.git/hooksthat blocks commits containing the wordconsole.log, and test it. - Bypass it once with
--no-verifyand explain why CI is still needed. - Move the hook to a committed
.githooksfolder and usecore.hooksPath. - In a Node or Playwright project, install Husky and lint-staged and run Prettier on staged files.
- Add commitlint and try to commit a badly formatted message.
- Add a
pre-pushhook that runs your@smoketests.
❓ 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”