Git Hooks: Introduction
Git hooks are executable scripts that Git runs automatically at specific points in your workflow — before a commit is saved, after a push lands, when a merge completes, and many other moments. They let you enforce rules, automate housekeeping, and connect Git to the rest of your toolchain without any external services.
Where hooks live
# Every Git repo has a hooks directory ls .git/hooks/
Default hook samples
applypatch-msg.sample pre-applypatch.sample commit-msg.sample pre-commit.sample fsmonitor-watchman.sample pre-merge-commit.sample post-update.sample pre-push.sample pre-applypatch.sample pre-rebase.sample prepare-commit-msg.sample update.sample
Every hook ships as a .sample file. Git ignores any file that ends in .sample. To activate a hook, remove the .sample extension and make the file executable.
# Activate the pre-commit hook cp .git/hooks/pre-commit.sample .git/hooks/pre-commit chmod +x .git/hooks/pre-commit
Two categories of hooks
Category | Where they run | Who can use them | Examples |
|---|---|---|---|
Client-side | Developer's local machine | Every contributor (opt-in per machine) | pre-commit, commit-msg, pre-push |
Server-side | Remote Git server (bare repo) | Repository admins only | pre-receive, update, post-receive |
Complete hooks reference
Hook | Trigger point | Can abort? | Category |
|---|---|---|---|
| Before the commit message editor opens | Yes (exit non-zero) | Client |
| After the default message is prepared, before the editor opens | Yes | Client |
| After the message is saved, before the commit is finalised | Yes | Client |
| After the commit is fully created | No (informational) | Client |
| Before a rebase starts | Yes | Client |
| After | No | Client |
| After a successful | No | Client |
| Before objects are transferred to the remote | Yes | Client |
| Before any refs are updated on the server | Yes (reject all) | Server |
| Once per ref being updated on the server | Yes (reject that ref) | Server |
| After all refs are updated on the server | No (informational) | Server |
| After | No | Server |
| Before | Yes | Client |
| After patch message is loaded by | Yes | Client |
| After | No | Client |
| On a non-bare repo when pushed to the checked-out branch | Yes | Server |
| Before a merge commit is created | Yes | Client |
| Before | Yes | Client |
| Called by filesystem monitors to speed up status queries | N/A | Client |
How exit codes control Git
Every hook is a program. Git interprets its exit code:
Exit code | Effect |
|---|---|
| Hook passed — Git proceeds with the operation. |
Non-zero (e.g., | Hook failed — Git aborts the operation and shows the hook's stdout/stderr. |
| Tells |
Minimal hook that always aborts
#!/usr/bin/env bash echo "This hook prevents all commits." exit 1
Minimal hook that always allows
#!/usr/bin/env bash exit 0
Writing your first hook
.git/hooks/pre-commit — prevent committing to main
#!/usr/bin/env bash branch=$(git symbolic-ref --short HEAD) if [ "$branch" = "main" ]; then echo "⛔ Direct commits to 'main' are not allowed." echo " Create a feature branch: git checkout -b feature/my-change" exit 1 fi exit 0
chmod +x .git/hooks/pre-commit
What the developer sees on a blocked commit
$ git commit -m "quick fix"
⛔ Direct commits to 'main' are not allowed.
Create a feature branch: git checkout -b feature/my-changeWhat language can hooks be written in?
Any language whose interpreter is available on the machine — the shebang line (#!) on the first line controls execution:
Language | Shebang |
|---|---|
Bash (most common) |
|
Python |
|
Node.js |
|
Ruby |
|
Perl |
|
Any compiled binary | (no shebang needed — just make it executable) |
Hooks are NOT committed by default
The .git/ directory is never committed — which means hooks you write locally are invisible to your teammates. There are three popular patterns for sharing hooks:
Approach | How it works | Pros | Cons |
|---|---|---|---|
Husky (Node.js) | Stores hooks in | Easy setup, widely adopted in JS projects. | Requires Node.js; setup step needed. |
pre-commit framework (Python) | YAML config in | Language-agnostic, huge plugin ecosystem. | Requires Python; hooks are sandboxed. |
Symlinks into a committed directory | Keep hooks in | No extra tooling needed. | Manual setup per clone; platform-specific symlinks. |
|
| Native Git support (Git 2.9+). | Must be set in local config; can't be committed. |
Using core.hooksPath
# 1. Create a committed directory for hooks mkdir -p scripts/git-hooks # 2. Write your hooks there cat > scripts/git-hooks/pre-commit << 'EOF' #!/usr/bin/env bash npm run lint --silent || exit 1 EOF chmod +x scripts/git-hooks/pre-commit # 3. Commit the directory git add scripts/git-hooks git commit -m "chore: add shared git hooks" # 4. Each developer configures their local Git to use this directory git config core.hooksPath scripts/git-hooks # Or set it globally git config --global core.hooksPath scripts/git-hooks
Husky setup (Node.js projects)
# Install husky npm install --save-dev husky # Initialise (creates .husky/ directory and adds prepare script) npx husky init # Add a pre-commit hook echo "npm test" > .husky/pre-commit chmod +x .husky/pre-commit # Commit the .husky/ directory git add .husky git commit -m "chore: add husky hooks"
pre-commit framework (Python / polyglot projects)
pip install pre-commit
.pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black# Install the hooks defined in the config pre-commit install # Run all hooks manually on every file pre-commit run --all-files