Some mistakes are so easy to make, and so easy to catch, that it seems silly to rely on memory. A console.log left in the code, a commit message that just says "stuff", a push that breaks the tests everyone else depends on. Wouldn't it be better if Git checked for you?
Git hooks make that possible. A hook is a script that Git runs automatically at a particular moment: before a commit is created, after a commit message is written, before a push leaves your computer, and more. If a "pre" hook exits with an error, Git stops the action, so a bad commit or push never happens.
In this lesson you'll write a pre-commit hook that blocks forgotten debug code, a commit-msg hook that enforces a message format, and a pre-push hook that runs tests. You'll then see how teams share hooks through the repository, and the popular tools that set them up automatically.
Where hooks live
Every repository has a .git/hooks folder. Git fills it with examples ending in .sample, such as pre-commit.sample and commit-msg.sample. To turn one on, create a file with the hook's exact name and no extension, and make it executable:
nano .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit- Hooks can be written in any language the computer can run: shell, Python, Node.js. The first line, such as
#!/bin/sh, says which. - On Windows, Git for Windows runs shell hooks through its bundled Bash, so the same scripts work.
.git/hooksis not committed or pushed. Each clone starts without your hooks, which is why teams use the sharing methods below.
The most useful hooks
| Hook | Runs | Typical use |
|---|---|---|
pre-commit | Before the commit is created | Lint and format staged files, block secrets or debug code |
commit-msg | After the message is written | Check the message format |
pre-push | Before anything is sent to the remote | Run the test suite |
post-merge | After a merge or pull | Reinstall dependencies if the lock file changed |
post-checkout | After switching branches | Clean or rebuild generated files |
Hooks whose names start with pre- can stop the action by exiting with a non-zero status; post- hooks run afterwards and can't undo anything.
Exit codes and skipping
A hook's exit code decides what happens. exit 0 (or reaching the end normally) means "carry on"; any other number, such as exit 1, means "stop". Print a helpful message before exiting so the person knows how to fix the problem.
For commit-msg, Git passes the path of a temporary file containing the message as the first argument, $1. The hook reads it and can reject it.
git commit --no-verify (or -n) skips the pre-commit and commit-msg hooks, and git push --no-verify skips pre-push. That's occasionally useful, for example to save a quick work-in-progress commit, but it's also why hooks can't enforce rules on their own. Checks that must never be bypassed belong in CI, such as GitHub Actions.
Sharing hooks with a team
Because .git/hooks isn't shared, teams use one of these approaches:
- core.hooksPath: keep hooks in a committed folder such as
.githooks/, and each person runsgit config core.hooksPath .githooksonce. - Husky (JavaScript projects): installed with npm; hooks live in a committed
.husky/folder and are set up automatically when someone runsnpm install. Often paired with lint-staged, which runs linters only on the staged files. - pre-commit (any language, written in Python): a
.pre-commit-config.yamlfile lists ready-made checks, such as trailing whitespace, large files and leaked keys.
Git Hooks Syntax
Hook basics
ls .git/hooks # sample hooks
chmod +x .git/hooks/pre-commit # make a hook executable
git config core.hooksPath .githooks # use a committed hooks folder
git commit --no-verify # skip pre-commit and commit-msg
git push --no-verify # skip pre-pushGit Hooks Summary
- Hooks are scripts in .git/hooks that Git runs automatically at certain moments.
- pre-commit, commit-msg and pre-push can stop an action by exiting with a non-zero code.
- commit-msg receives the message file path as $1.
- --no-verify skips hooks, so rules that must always hold belong in CI as well.
- Share hooks with core.hooksPath, Husky or the pre-commit framework; keep them fast.
Git Hooks Examples
Each example shows the code, the output it produces and what each part does.
A pre-commit hook that blocks debug code
#!/bin/sh
# .git/hooks/pre-commit
if git diff --cached | grep -q "console.log"; then
echo "Commit blocked: remove console.log before committing."
exit 1
fi$ git add app.js
$ git commit -m "Add app"
Commit blocked: remove console.log before committing.
$ # remove the console.log line, then:
$ git add app.js
$ git commit -m "Add app"
[main 2b5d380] Add app
1 file changed, 1 insertion(+)Line-by-line explanation
- Line 3git diff --cached shows exactly what's about to be committed, so only staged changes are checked.
- Line 4Explain the problem clearly.
- Line 5Exiting with 1 cancels the commit; nothing is saved.
Enforcing a commit message format
#!/bin/sh
# .git/hooks/commit-msg
if ! grep -qE "^(feat|fix|docs|refactor|test|chore): .+" "$1"; then
echo "Commit message must look like: feat: add search box"
exit 1
fi$ git commit -am "stuff"
Commit message must look like: feat: add search box
$ git commit -am "fix: handle empty input"
[main b24e614] fix: handle empty input
1 file changed, 1 insertion(+)Line-by-line explanation
- Line 3$1 is the path of the file holding the message. grep -E checks it starts with a type, a colon and a description, following Conventional Commits.
- Line 5A vague message is rejected before the commit is made.
Running tests before pushing, shared with the team
mkdir .githooks
cat > .githooks/pre-push <<'EOF'
#!/bin/sh
echo "Running tests before push..."
npm test || { echo "Tests failed: push cancelled."; exit 1; }
EOF
chmod +x .githooks/pre-push
git config core.hooksPath .githooks
git add .githooks && git commit -m "chore: add shared pre-push hook"$ git push
Running tests before push...
✓ 42 tests passed
To https://github.com/samlee/shop.git
7e1c0d2..91fe2c3 main -> mainLine-by-line explanation
- Lines 1 to 6The hook lives in a committed .githooks folder, so everyone gets the same file.
- Line 5If npm test fails, the hook prints a message and exits with 1, so nothing is pushed.
- Line 8Each developer runs this once to tell Git to use the shared folder.
Common Git Hooks Mistakes
Forgetting to make the hook executable
nano .git/hooks/pre-commit # saved, but Git silently ignores itnano .git/hooks/pre-commit
chmod +x .git/hooks/pre-commitExpecting hooks in .git/hooks to be shared
git add .git/hooks/pre-commit # .git is never committedmkdir .githooks && mv .git/hooks/pre-commit .githooks/
git config core.hooksPath .githooks
git add .githooksA slow pre-commit hook
#!/bin/sh
npm run test:all # five minutes on every commit#!/bin/sh
npx lint-staged # checks only the staged files, in secondsGit Hooks in Real Projects
Hooks in real projects
Many JavaScript projects use Husky and lint-staged so every commit is automatically formatted with Prettier and checked with ESLint. Python projects commonly use the pre-commit framework to run formatters like Black and to catch accidentally committed keys. Teams combine local hooks for fast feedback with CI checks on GitHub that can't be skipped.
Git Hooks FAQ
Can I write Git hooks in Python or Node.js?
Yes. A hook is any executable file with the right name, so the first line just needs to point at the interpreter, such as #!/usr/bin/env python3 or #!/usr/bin/env node. Shell scripts are most common because they need nothing extra installed.
Do my local Git hooks run on GitHub?
No. Hooks run only in the repository where they're installed, and GitHub.com doesn't run hooks you write. To enforce checks for everyone, use GitHub Actions and branch protection, and treat local hooks as fast early warnings.
How does a commit-msg hook read the commit message?
Git passes the path of a temporary file holding the message as the first argument, $1, usually .git/COMMIT_EDITMSG. The hook can read it, check it, and even rewrite it. Exiting with a non-zero code rejects the commit.
What information does a pre-push hook receive?
The remote's name and URL arrive as the first two arguments. Then, on standard input, Git sends one line per ref being pushed with the local ref, local hash, remote ref and remote hash. That lets a hook, for example, refuse pushes straight to main.
Do Git hooks work on Windows?
Yes. Git for Windows comes with a Bash shell and runs hooks through it, so a script starting #!/bin/sh works as it does on macOS and Linux. Avoid commands that only exist on one system if your team uses a mix.
Git Hooks Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the command that makes a hook script executable.
+x .git/hooks/pre-commit
Which hook runs before a commit is created?
How does a hook stop a commit?
What does a commit-msg hook receive as $1?
Why should important rules also be checked in CI, not only in hooks?
Git Hooks Quiz
5 questions to check what you learned.
Where does Git look for hooks by default?
- .github/hooks
- hooks/
- ~/.gitconfig
- .git/hooks
Which hook is best for running the full test suite?
- pre-commit
- commit-msg
- pre-push
- post-checkout
What does
git config core.hooksPath .githooksdo?- Deletes the default hooks
- Tells Git to run hooks from the committed .githooks folder
- Copies hooks to GitHub
- Disables hooks
What is Husky?
- A tool that installs shared Git hooks in JavaScript projects
- A Git hosting service
- A merge tool
- A type of branch
Can a post-commit hook prevent the commit?
- No, it runs after the commit already exists
- Yes, by exiting with 1
- Only with --no-verify
- Only on main