Git and GitHub course menu
Git Hooks Tutorial: Run Checks Before Every Commit
Automate checks with Git hooks: write a pre-commit hook, check commit messages with commit-msg, share hooks with your team using core.hooksPath, Husky or pre-commit, and bypass hooks when needed.
On this page
A hook is a script that Git runs automatically at certain moments, such as before a commit. Teams use hooks to run linters and tests, and to check commit messages, before a mistake leaves your computer.
Hooks live in .git/hooks/. Git ships sample files there, ending in .sample, which do nothing until renamed.
Write a pre-commit hook#
If a hook exits with a non-zero code, Git cancels the action. This hook refuses a commit containing the word TODO:
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
if git diff --cached | grep -q '^+.*TODO'; then
echo "Commit blocked: remove TODO first."
exit 1
fi
EOF
chmod +x .git/hooks/pre-commitgit diff --cached shows what is staged (it is another name for --staged). chmod +x makes the file executable, which is required on macOS and Linux.
Try it:
echo "TODO: finish this" >> app.txt
git add app.txt
git commit -m "Add task"
git log --onelineCommit blocked: remove TODO first.
617d59f StartThe hook printed its message, and no commit was created. Fix the line and try again:
sed -i 's/TODO: finish this/Finished/' app.txt
git add app.txt
git commit -q -m "Add task"
git log --oneline73bc778 Add task
617d59f StartCheck commit messages#
commit-msg receives the path of the message file as its first argument. This hook enforces a Conventional Commits prefix:
cat > .git/hooks/commit-msg <<'EOF'
#!/bin/sh
if ! grep -qE '^(feat|fix|docs|chore|test|refactor)(\(.+\))?!?: ' "$1"; then
echo "Bad message. Use a prefix, for example: feat: add login"
exit 1
fi
EOF
chmod +x .git/hooks/commit-msg
echo "more" >> app.txt
git add app.txt
git commit -m "add stuff"
git commit -q -m "feat: add more text"
git log --oneline -n 1Bad message. Use a prefix, for example: feat: add login
52cb096 feat: add more textSkipping hooks#
--no-verify (or -n) skips pre-commit and commit-msg:
git commit --no-verify -m "Emergency fix"This is why hooks are only a convenience. Anyone can skip them, so important checks should also run in CI, where they cannot be avoided.
Sharing hooks with a team#
The .git folder is not committed, so hooks there are not shared. There are three common solutions.
1. A hooks folder in the repository:
mkdir .githooks
mv .git/hooks/pre-commit .githooks/
git config core.hooksPath .githooks
git config core.hooksPath.githooksEach developer needs to run that git config command once.
2. Husky, for JavaScript projects. It installs hooks when people run npm install:
npm install --save-dev husky
npx husky init3. pre-commit, a tool written in Python that works for any language. You list ready-made checks in a .pre-commit-config.yaml file:
pip install pre-commit
pre-commit installKeep hooks fast#
Developers will skip a hook that takes a minute. Run quick checks such as linting on changed files in pre-commit, and leave the full test suite for pre-push or CI.
Common mistakes#
Practice#
Practice exercise
Write a pre-commit hook that blocks any commit that adds a file ending in .env. Test it by trying to commit prod.env.
Show hint
git diff --cached --name-only lists the staged files.
Show solution
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
if git diff --cached --name-only | grep -q '\.env$'; then
echo "Blocked: do not commit .env files."
exit 1
fi
EOF
chmod +x .git/hooks/pre-commit
echo "KEY=abc" > prod.env
git add prod.env
git commit -m "Add settings"Blocked: do not commit .env files.Key takeaway#
Next: Secrets and Large Files.
Finished this lesson?