Git Hooks
Learn how Git hooks automate checks before commits and pushes, and how tools like Husky make hooks shareable across teams.
What Are Git Hooks?
Git hooks are scripts that run automatically when Git reaches specific events. They help you automate repetitive checks so problems are caught before code is committed or pushed.
Hooks are usually grouped into two categories:
- client-side hooks, which run on a developer's machine
- server-side hooks, which run on the Git server when a push is received
Tip: Hooks are best for checks people frequently forget, such as formatting, test commands, or commit message rules.
Client-Side vs Server-Side Hooks
Client-Side Hooks
Client-side hooks run inside a local repository during actions like commit, merge, and push. Common examples include:
pre-commitcommit-msgpre-push
Server-Side Hooks
Server-side hooks run on a remote repository and can reject pushes or enforce policy. On hosted platforms, CI and branch protection often serve a similar role.
Note: Local hooks improve developer experience, but CI is still the shared source of truth because local hooks can be skipped or may differ across machines.
Three Hooks You Will Use Often
pre-commit
pre-commit runs before the commit is created. If the script exits with a non-zero status, Git stops the commit.
Typical uses:
- lint checks
- formatting checks
- secret scanning
- basic file validation
Example:
#!/bin/sh
npm run lint
npm run format:check
commit-msg
commit-msg validates the commit message itself. It is the usual place to enforce Conventional Commits.
#!/bin/sh
message_file="$1"
grep -Eq '^(feat|fix|docs|chore): .+' "$message_file" || {
echo "Commit message must start with feat:, fix:, docs:, or chore:"
exit 1
}
pre-push
pre-push runs right before Git sends commits to the remote. It is useful for checks that are important but slightly heavier than commit-time checks.
#!/bin/sh
npm test
Where Hooks Live
A repository's built-in hooks live in:
.git/hooks/
Git often creates sample files there, such as pre-commit.sample. To install a hook manually, create a file with the exact hook name and make it executable.
Example:
nano .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
If the file is not executable, Git ignores it.
Installing Hooks Manually
Here is a small pre-commit hook that runs linting:
cat > .git/hooks/pre-commit <<'HOOK'
#!/bin/sh
npm run lint
HOOK
chmod +x .git/hooks/pre-commit
And a commit-msg hook that blocks non-standard messages:
cat > .git/hooks/commit-msg <<'HOOK'
#!/bin/sh
message_file="$1"
grep -Eq '^(feat|fix|docs|chore): .+' "$message_file" || exit 1
HOOK
chmod +x .git/hooks/commit-msg
The Main Limitation of Raw Hooks
Plain .git/hooks/ scripts are local only. They are not shared automatically when another developer clones the repository. That leads to three common problems:
- teammates do not get the same hooks
- hook updates drift over time
- onboarding becomes manual
Using Husky for Team-Wide Hooks
Husky is a popular solution in JavaScript projects because it stores hook definitions inside the repository.
Basic setup:
npm install --save-dev husky
npx husky init
That creates a .husky/ directory. A typical pre-commit hook might look like this:
#!/bin/sh
. "./_/husky.sh"
npm run lint
npm run format:check
A commit-msg hook can call commitlint:
#!/bin/sh
. "./_/husky.sh"
npx commitlint --edit "$1"
Best Practices
Keep Hooks Fast
Fast hooks get used. Slow hooks get bypassed. Put quick checks in pre-commit and slightly heavier ones in pre-push.
Pair Hooks with CI
Hooks provide local convenience, but CI should still verify important rules centrally.
Write Useful Errors
If a hook blocks work, explain what failed and which command fixes it.
What You Should Remember
Git hooks automate work around commits and pushes. pre-commit is great for lint or format checks, commit-msg enforces message rules, and pre-push can run tests before code is shared. Manual hooks live in .git/hooks/ and need chmod +x, while Husky makes hooks versioned and team-friendly.
Test Your Understanding
Exercise 1: Choosing the right hook
Which hook should you use to reject commit messages that do not follow Conventional Commits?
Exercise 2: Installing a local hook
What step is required after creating a script in `.git/hooks/pre-commit` so Git can run it?
Exercise 3: Sharing hooks with a team
Why do many JavaScript teams use Husky instead of relying only on `.git/hooks/`?