Git Hooks
Hooks are scripts that Git runs automatically at certain moments — before a commit is made, after a checkout, before a push. They're used to check code formatting, run quick tests, enforce commit message rules, or block secrets from being committed.
Where hooks live
Every repository has a .git/hooks folder, filled with disabled examples ending in .sample:
ls .git/hooks
applypatch-msg.sample commit-msg.sample fsmonitor-watchman.sample post-update.sample pre-applypatch.sample pre-commit.sample pre-merge-commit.sample pre-push.sample pre-rebase.sample pre-receive.sample prepare-commit-msg.sample push-to-checkout.sample update.sample
To enable a hook, create an executable file named exactly after the hook — no extension — in that folder. It can be written in any language with a #! line at the top: shell, Python, Node.js…
Common hooks
| Hook | Runs | A non-zero exit… |
|---|---|---|
pre-commit | Before the commit message is asked for | Stops the commit |
prepare-commit-msg | Before the editor opens | Stops the commit |
commit-msg | After the message is written; receives the message file | Stops the commit |
post-commit | After a commit is made | Nothing (informational) |
pre-push | Before anything is sent to a remote | Stops the push |
post-checkout, post-merge | After switching branches or merging | Nothing — used e.g. to reinstall dependencies |
A pre-commit hook
Sam keeps forgetting to remove debugging console.log lines. This hook checks the staged changes and refuses the commit if it finds one:
cat > .git/hooks/pre-commit <<'EOF' #!/bin/sh # Block commits that add console.log to JavaScript files if git diff --cached -U0 -- '*.js' | grep -q '^+.*console\.log'; then echo "pre-commit: remove console.log before committing" >&2 exit 1 fi EOF chmod +x .git/hooks/pre-commit
echo "console.log('debug');" >> basket.js git commit -am "Debug basket"
pre-commit: remove console.log before committing
The commit was blocked. After removing the line, the commit goes through:
git restore basket.js echo "export const CURRENCY = 'EUR';" >> basket.js git commit -am "Add currency constant"
[main a70a6fd] Add currency constant 1 file changed, 1 insertion(+)
Note the hook checks the staged content (--cached), which is what will actually be committed.
A commit-msg hook
The commit-msg hook receives the path of a file containing the message. This one rejects summaries over 50 characters:
cat > .git/hooks/commit-msg <<'EOF' #!/bin/sh summary=$(head -n 1 "$1") if [ ${#summary} -gt 50 ]; then echo "commit-msg: summary is ${#summary} characters; keep it to 50" >&2 exit 1 fi EOF chmod +x .git/hooks/commit-msg echo "export const SHIPPING = 4.99;" >> basket.js git commit -am "Add a constant for the standard shipping price used at checkout"
commit-msg: summary is 63 characters; keep it to 50
git commit -am "Add shipping price constant"
[main 2605f46] Add shipping price constant 1 file changed, 1 insertion(+)
Skipping hooks
--no-verify (or -n) skips the pre-commit and commit-msg hooks for one commit; git push --no-verify skips pre-push. That's why client-side hooks are a convenience, not a guarantee — real enforcement belongs on the server, in CI checks and branch protection.
Sharing hooks with a team
.git/hooks isn't part of the repository, so hooks you create there aren't shared. Options:
core.hooksPath— commit your hooks in a normal folder such as.githooks/, and have everyone rungit config core.hooksPath .githooksonce.- Hook managers — tools that install hooks from a config file in the repository: pre-commit (language-agnostic, configured in
.pre-commit-config.yaml), Husky (common in JavaScript projects, set up viapackage.json), and Lefthook, among others.
mkdir .githooks
mv .git/hooks/pre-commit .git/hooks/commit-msg .githooks/
git config core.hooksPath .githooks
git add .githooks
git commit -m "Share pre-commit and commit-msg hooks"
[main 6825dc5] Share pre-commit and commit-msg hooks 2 files changed, 12 insertions(+) create mode 100755 .githooks/commit-msg create mode 100755 .githooks/pre-commit
Keep hooks fast
A pre-commit hook that takes a minute will get skipped. Check only the staged files, run the formatter and linter and maybe a few quick tests; leave the full test suite to pre-push or CI.
Server-side hooks
Servers you run yourself support pre-receive, update and post-receive hooks that can reject pushes or trigger deployments. Hosted services like GitHub don't let you run your own server hooks; they offer the same results through branch protection rules, required status checks and webhooks.
Try it yourself
Write a pre-commit hook that refuses to commit any file larger than 1 MB, or any staged file named .env. Test that it blocks a bad commit, allows a good one, and can be bypassed with --no-verify.