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
Output
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

HookRunsA non-zero exit…
pre-commitBefore the commit message is asked forStops the commit
prepare-commit-msgBefore the editor opensStops the commit
commit-msgAfter the message is written; receives the message fileStops the commit
post-commitAfter a commit is madeNothing (informational)
pre-pushBefore anything is sent to a remoteStops the push
post-checkout, post-mergeAfter switching branches or mergingNothing — 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"
Output
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"
Output
[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"
Output
commit-msg: summary is 63 characters; keep it to 50
$
git commit -am "Add shipping price constant"
Output
[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 run git config core.hooksPath .githooks once.
  • 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 via package.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"
Output
[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.