Git Hooks
Git Hooks
Git hooks are scripts that run automatically at specific points in your workflow — before a commit is written, before a push is sent, after a merge completes, and so on. They let you enforce quality rules locally, without waiting for CI to catch problems.
Where Hooks Live
By default, Git looks for hooks in the .git/hooks/ directory of your repository. Each hook is a plain executable file named after the event it listens to. Git ships with sample files (e.g. pre-commit.sample) — rename them by removing the .sample extension to activate them.
The Three Most Useful Hooks
pre-commit
Runs before Git opens the commit message editor. This is the right place to run linters, formatters, and static analysis on the code you are about to commit. If the script exits with a non-zero status, the commit is aborted.
Bash example — run a linter on staged files only:
#!/bin/sh
# .git/hooks/pre-commit
# Get the list of staged files
staged_files=$(git diff --cached --name-only --diff-filter=ACMR)
if [ -z "$staged_files" ]; then
exit 0
fi
# Run ESLint on staged JS/TS files
echo "$staged_files" | grep -E '\.(js|ts|tsx)$' | xargs npx eslint --no-error-on-unmatched-pattern
if [ $? -ne 0 ]; then
echo "Lint errors found. Please fix them before committing."
exit 1
fi
exit 0
Node.js example — run Prettier and exit on failure:
// .git/hooks/pre-commit (Node.js)
#!/usr/bin/env node
const { execSync } = require('child_process');
try {
execSync('npx prettier --check "src/**/*.{js,ts,css}"', { stdio: 'inherit' });
} catch (error) {
console.error('Prettier check failed. Run "npx prettier --write" to fix formatting.');
process.exit(1);
}
commit-msg
Runs after you have typed your commit message but before Git saves it. Use this hook to validate that the message conforms to your team's conventions — for example, enforcing Conventional Commits format or a minimum length.
Bash example — enforce Conventional Commits:
#!/bin/sh
# .git/hooks/commit-msg
commit_msg=$(cat "$1")
# Pattern: type(optional-scope): description
pattern='^(feat|fix|docs|style|refactor|test|chore|perf|ci)(\(.+\))?\!?:\s.+'
if ! echo "$commit_msg" | grep -qE "$pattern"; then
echo "Commit message does not follow Conventional Commits format."
echo "Expected: <type>(scope): <description>"
echo "Example: feat(auth): add password reset flow"
exit 1
fi
exit 0
Node.js example — enforce minimum length:
#!/usr/bin/env node
// .git/hooks/commit-msg
const fs = require('fs');
const msg = fs.readFileSync(process.argv[2], 'utf8').trim();
if (msg.length < 10) {
console.error('Commit message is too short. Write at least 10 characters.');
process.exit(1);
}
pre-push
Runs before Git sends commits to the remote. This is a good place to run your full test suite so that you never push broken code. Be aware that it can slow down pushes — keep the check fast or make it opt-out.
Bash example — run the test suite:
#!/bin/sh
# .git/hooks/pre-push
echo "Running tests before push..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Fix them before pushing."
exit 1
fi
exit 0
Sharing Hooks with Your Team
Hooks in .git/hooks/ are not tracked by Git, so each developer has to set them up manually. Two popular tools solve this problem.
Husky
Husky is a Node.js package that manages Git hooks via your package.json. It stores hook scripts in a .husky/ directory that is committed to the repository, so every developer picks them up automatically when they clone.
npm install --save-dev husky
npx husky install
npx husky add .husky/pre-commit "npm run lint"
npx husky add .husky/pre-push "npm test"
The resulting .husky/pre-commit file is a simple shell script that runs npm run lint. Commit the .husky/ directory and every teammate inherits the same hooks.
lint-staged
lint-staged pairs naturally with Husky. Instead of linting your entire codebase on every commit, it runs linters only on the files that are staged — making the pre-commit hook fast regardless of project size.
Add a lint-staged section to your package.json:
{
"scripts": {
"lint-staged": "npx lint-staged"
},
"lint-staged": {
"*.{js,ts,tsx}": ["eslint --fix"],
"*.{js,ts,css,html}": ["prettier --write"]
}
}
Then wire it up with Husky:
npx husky add .husky/pre-commit "npx lint-staged"
Now every git commit runs ESLint and Prettier on only the files you changed. If either tool exits with an error, the commit stops and you fix the problem before it enters your history.
Bypassing Hooks
If you genuinely need to skip a hook — for example, when committing a work-in-progress checkpoint that does not pass lint — you can bypass it with the --no-verify flag:
git commit --no-verify -m "wip: checkpoint before refactor"
Use this sparingly. The hooks exist to protect your history; bypassing them routinely defeats their purpose.