What are Git Hooks?
Git hooks allow you to run custom scripts before or after important events occur in the Git lifecycle, like committing, merging, and pushing. Each hook is simply an executable file that Git calls at a specific point during a command's execution.
The core mechanism is simple: if a hook script exits with a non-zero status, the Git operation is aborted. This is what makes hooks powerful — they can enforce rules, run checks, and block actions that don't meet your criteria. Hooks that run after an operation (like post-commit) are informational only and cannot affect the outcome.
At its most basic level, hooks can be useful for simple actions, such as enforcing naming conventions for commits or branches. However, since hook scripts are fully customizable, any Git operation can be tweaked, enabling highly complex development workflows.
By running these custom scripts, the end goal is to assure consistency among teams, increase productivity, and save time; if your project is running a Continuous Integration pipeline, the number of failures can also be reduced by running some of the pipeline's tasks on the local machine.
Getting Started with Git Hooks
Hooks are a built-in feature in Git — you don't need to install anything. After initializing or cloning a repository, have a look at the hooks folder, which should be inside the hidden .git folder.
If we run the ls command, this is what we get:
$ cd .git/hooks
$ ls
applypatch-msg.sample pre-applypatch.sample pre-rebase.sample
commit-msg.sample pre-commit.sample prepare-commit-msg.sample
post-update.sample pre-push.sample update.sample
By default, these example scripts are disabled — this can be altered by removing the desired hook's .sample extension.
While hooks are usually shell scripts, you can use whatever language you're most comfortable with, like Ruby or Python — just make sure you change the shebang line (e.g. #!/usr/bin/env python3) at the top of the script.
To create a hook from scratch, create a new file in .git/hooks/ (the file name must match one of the hooks), write the code, and make the file executable by running the following command (it will be ignored otherwise):
chmod +x .git/hooks/pre-commit
That's it! From that point forward, the hook will be called.
Important
Hooks are NOT cloned or pushed
The .git/hooks/ directory is local only. When you clone a repository, you get the sample hooks — not any custom hooks that someone else may have set up. This means hooks must be set up individually on each developer's machine, or shared using one of the strategies described in the sharing hooks section below.
The Git Cheat Sheet
No need to remember all those commands and parameters: get our popular "Git Cheat Sheet" - for free!
Git Hooks Quick Reference
The following table summarizes all major hooks at a glance:
| Hook | Triggered by | Scope | Arguments | Can abort? | Common use |
|---|---|---|---|---|---|
| pre-commit | git commit |
Client | none | Yes | Lint, format, secret scan |
| prepare-commit-msg | git commit |
Client | msg file, source, [SHA] | Yes | Auto-prefix messages |
| commit-msg | git commit |
Client | msg file | Yes | Validate message format |
| post-commit | git commit |
Client | none | No | Notifications, logging |
| pre-rebase | git rebase |
Client | upstream, branch | Yes | Block rebasing published work |
| post-checkout | git checkout, git switch, git clone |
Client | prev HEAD, new HEAD, branch flag | No | Rebuild deps, clean generated files |
| post-merge | git merge, git pull |
Client | squash flag | No | Restore permissions, rebuild deps |
| pre-push | git push |
Client | remote name, URL + stdin refs | Yes | Run tests, protect branches |
| pre-receive | git push (server) |
Server | stdin (old, new, ref per line) | Yes | Enforce push policies |
| update | git push (server) |
Server | refname, old SHA, new SHA | Yes | Per-branch validation |
| post-receive | git push (server) |
Server | stdin (old, new, ref per line) | No | CI triggers, deploy, notify |
Hook Execution Lifecycle
During a typical commit workflow, hooks fire in this order:
git add → pre-commit → prepare-commit-msg → editor → commit-msg → commit created → post-commit
During a push workflow:
git push → pre-push → transfer → pre-receive → update (per branch) → refs updated → post-receive
During a checkout/switch:
git checkout/switch → working tree updated → post-checkout
Client-Side Hooks
pre-commit
When it fires: Before the commit is created, after you run git commit.
Arguments: None.
Exit code: Non-zero aborts the commit.
The pre-commit hook is the most commonly used hook. It runs before Git even asks you for a commit message, making it the ideal place to run linters, formatters, static analysis, or secret detection. If the script exits with a non-zero status, the commit is aborted.
prepare-commit-msg
When it fires: After the default commit message is created, but before the editor opens.
Arguments: Path to the file containing the commit message, the source of the message (message, template, merge, squash, or commit), and optionally the SHA of the relevant commit.
Exit code: Non-zero aborts the commit.
This hook is useful for auto-populating commit messages — for example, prefixing a ticket number extracted from the branch name.
commit-msg
When it fires: After the user writes the commit message and saves the editor.
Arguments: Path to the file containing the commit message.
Exit code: Non-zero aborts the commit.
Use this hook to validate commit message format — for example, enforcing Conventional Commits or requiring a minimum message length.
post-commit
When it fires: After the commit has been created.
Arguments: None.
Exit code: Cannot affect the commit (informational only).
Commonly used for notifications (e.g. sending a Slack message or logging to an external system). Since it runs after the commit is already recorded, it cannot abort or change anything.
post-checkout
When it fires: After git checkout, git switch, or at the end of git clone.
Arguments: The previous HEAD ref, the new HEAD ref, and a flag (1 = branch checkout, 0 = file checkout).
Exit code: Cannot affect the checkout (informational only).
A practical use case is rebuilding dependencies when switching branches — for example, running npm install automatically if package.json changed between the two branches.
post-merge
When it fires: After a successful git merge (including git pull, which performs a merge internally).
Arguments: A squash flag (1 if a squash merge, 0 otherwise).
Exit code: Cannot affect the merge (informational only).
Similar to post-checkout, this hook is useful for restoring file permissions, rebuilding dependencies, or cleaning up generated files after a merge changes relevant source files.
pre-rebase
When it fires: Before git rebase begins replaying commits.
Arguments: The upstream branch and the branch being rebased (blank if rebasing the current branch).
Exit code: Non-zero aborts the rebase.
This hook can prevent rebasing commits that have already been pushed — a common source of problems in shared repositories.
pre-push
When it fires: Before git push transfers data to the remote.
Arguments: The remote name and URL. Additionally, lines are provided on stdin in the format: <local ref> <local SHA> <remote ref> <remote SHA> for each ref being pushed.
Exit code: Non-zero aborts the push.
Use this to run tests before pushing or to prevent pushing to protected branches (e.g. main or production).
Server-Side Hooks
Server-side hooks run on the remote repository that receives a push. They are typically used to enforce project policies.
pre-receive
When it fires: Once when a push is received, before any refs are updated.
Arguments: Receives lines on stdin in the format <old SHA> <new SHA> <refname> for each ref being pushed.
Exit code: Non-zero rejects the entire push.
This is the most powerful server-side hook. Use it to enforce branch protection rules, commit message policies, file size limits, or code review requirements.
update
When it fires: Like pre-receive, but once per branch being updated.
Arguments: The ref name, the old SHA, and the new SHA.
Exit code: Non-zero rejects the update for that specific branch (other branches in the same push can still succeed).
The update hook allows more granular control than pre-receive because you can accept pushes to some branches while rejecting others.
post-receive
When it fires: After all refs have been updated successfully.
Arguments: Same stdin format as pre-receive.
Exit code: Cannot reject the push (informational only).
This hook is commonly used to trigger CI/CD pipelines, deploy code, or send notifications.
Practical Git Hook Examples
Below are copy-paste-ready hook scripts. Remember: each script must be saved in .git/hooks/ with the exact hook name (no extension) and made executable with chmod +x.
Example 1: Block Commits to main/master (pre-commit)
This simple hook prevents accidental commits directly to protected branches:
Shell version:
#!/bin/sh
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then
echo "ERROR: Direct commits to $branch are not allowed."
echo "Create a feature branch instead."
exit 1
fi
Python version (to demonstrate that hooks can be written in any language):
#!/usr/bin/env python3
import subprocess, sys
branch = subprocess.check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
text=True
).strip()
if branch in ("main", "master"):
print(f"ERROR: Direct commits to {branch} are not allowed.")
print("Create a feature branch instead.")
sys.exit(1)
Save as .git/hooks/pre-commit and run chmod +x .git/hooks/pre-commit.
Example 2: Lint Staged Files and Check for Whitespace Errors (pre-commit)
Run Git's built-in whitespace check on staged files:
#!/bin/sh
# Check for whitespace errors in staged files
if ! git diff-index --check --cached HEAD -- ; then
echo "ERROR: Whitespace errors found. Fix them before committing."
exit 1
fi
Example 3: Auto-Prefix Commit Message with Issue Number (prepare-commit-msg)
Automatically extract an issue number from the branch name (e.g. feature/PROJ-123-add-login) and prepend it to the commit message:
#!/usr/bin/env python3
import sys, re
commit_msg_file = sys.argv[1]
# Skip merge commits, amends, and other automated messages
if len(sys.argv) > 2 and sys.argv[2] in ("merge", "squash", "commit"):
sys.exit(0)
# Read the current branch name
import subprocess
branch = subprocess.check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
text=True
).strip()
# Extract issue number (e.g. PROJ-123) from the branch name
match = re.search(r"([A-Z]+-\d+)", branch)
if match:
issue = match.group(1)
with open(commit_msg_file, "r+") as f:
msg = f.read()
if issue not in msg: # Avoid duplicating the prefix
f.seek(0)
f.write(f"{issue} {msg}")
Save as .git/hooks/prepare-commit-msg and run chmod +x .git/hooks/prepare-commit-msg.
Example 4: Validate Commit Message Format (commit-msg)
Reject commit messages that don't follow a specific pattern (e.g. starting with feat:, fix:, docs:, etc.):
#!/bin/sh
commit_msg=$(cat "$1")
pattern="^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?: .{1,}"
if ! echo "$commit_msg" | grep -qE "$pattern"; then
echo "ERROR: Commit message does not match the required format."
echo "Expected: <type>(<scope>): <description>"
echo "Example: feat(auth): add OAuth2 login flow"
exit 1
fi
Save as .git/hooks/commit-msg and run chmod +x .git/hooks/commit-msg.
Example 5: Rebuild Dependencies on Branch Switch (post-checkout)
Automatically run npm install if package.json changed between branches:
#!/bin/sh
prev_head=$1
new_head=$2
branch_checkout=$3
# Only run on branch checkouts, not file checkouts
if [ "$branch_checkout" != "1" ]; then
exit 0
fi
# Check if package.json changed between the two HEADs
if git diff --name-only "$prev_head" "$new_head" | grep -q "package.json"; then
echo "package.json changed — running npm install..."
npm install
fi
Save as .git/hooks/post-checkout and run chmod +x .git/hooks/post-checkout.
Example 6: Deploy on Push to main (post-receive)
A server-side hook that deploys code when changes are pushed to main:
#!/bin/sh
while read oldrev newrev refname; do
branch=$(echo "$refname" | sed 's|refs/heads/||')
if [ "$branch" = "main" ]; then
echo "Deploying main branch..."
git --work-tree=/var/www/html --git-dir=/path/to/repo.git checkout -f main
echo "Deployment complete."
fi
done
This script is placed on the server in the bare repository's hooks/ directory.
Environment Variables Available in Hooks
Git sets several environment variables that your hook scripts can use:
GIT_DIR— Path to the.gitdirectory.GIT_INDEX_FILE— Path to the index (staging area) file. Note: if this variable is set during apost-commithook, it may cause unexpected behavior — unset it if you run Git commands inside that hook.GIT_AUTHOR_NAME,GIT_AUTHOR_EMAIL,GIT_AUTHOR_DATE— The commit author's details (available in commit-related hooks).GIT_COMMITTER_NAME,GIT_COMMITTER_EMAIL— The committer's details.
Sharing Git Hooks with Your Team
Since .git/hooks/ is not tracked by version control, you need a strategy to share hooks across your team.
core.hooksPath (Git 2.9+)
The recommended modern approach. Store your hooks in a version-controlled directory (e.g. .githooks/) and point Git to it:
git config core.hooksPath .githooks
This can be set per-repository or globally. Every developer runs this command once (or you add it to your project's setup instructions), and hooks are automatically shared through version control.
Symlinks
Create a setup script that symlinks from a versioned directory to .git/hooks/:
#!/bin/sh
# setup-hooks.sh — run once after cloning
ln -sf ../../.githooks/pre-commit .git/hooks/pre-commit
ln -sf ../../.githooks/commit-msg .git/hooks/commit-msg
echo "Hooks installed."
Template Directory
Configure a global template so hooks are automatically copied into every new clone:
git config --global init.templateDir /path/to/git-templates
Any files in /path/to/git-templates/hooks/ will be copied to .git/hooks/ on git init or git clone.
Hook Management Tools
Several tools automate hook setup and management:
- Husky — Popular in JavaScript projects; integrates with
package.json. - pre-commit — Language-agnostic framework with a large library of ready-made hooks.
- Lefthook — Fast, polyglot hook manager with parallel execution support.
Advanced Tips
Interactive Prompts in Hooks
By default, hooks don't have access to the terminal for user input. To enable interactive prompts (e.g. "Tests failed. Continue anyway?"), redirect from /dev/tty:
exec < /dev/tty
read -p "Tests failed. Continue anyway? [y/N] " answer
if [ "$answer" != "y" ]; then
exit 1
fi
Background Long-Running Tasks
If a hook triggers a slow operation (like notifying an external service), you can run it in the background so it doesn't block the Git command:
# Send notification without making the user wait
curl -s -X POST https://example.com/webhook -d '{"event":"push"}' &>/dev/null & disown
Branch-Specific Logic
Run hook logic only for specific branches:
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "main" ]; then
# Run extra checks only on main
npm test
fi
Bypassing Hooks
There are times when you may need to skip the execution of hooks (e.g. to bypass linters or security checks) and quickly get a commit out there. This is where the --no-verify option can come in handy.
Let's add a commit and bypass the pre-commit and commit-msg hooks:
$ git commit --no-verify -m "your commit message"
In this case, you could simply type -n for a shorter option. The following command would perform the same action:
$ git commit -n -m "your commit message"
However, please note that -n won't always work. One example would be the git push command, where -n performs a dry run. In these cases, adding --no-verify is the only option available.
$ git push --no-verify
Tip
Bypassing Hooks in Tower
If you are using the Tower Git GUI, a "Skip Hooks" option will automatically be displayed in the "Commit Composing" window if a pre-commit hook is being used in the repository.

You can also skip the execution of hooks for push, pull, merge, rebase, and apply operations by accessing the list of additional options.

Get our popular Git Cheat Sheet for free!
You'll find the most important commands on the front and helpful best practice tips on the back. Over 100,000 developers have downloaded it to make Git a little bit easier.
About Us
As the makers of Tower, the best Git client for Mac and Windows, we help over 100,000 users in companies like Apple, Google, Amazon, Twitter, and Ebay get the most out of Git.
Just like with Tower, our mission with this platform is to help people become better professionals.
That's why we provide our guides, videos, and cheat sheets (about version control with Git and lots of other topics) for free.