Skip to main content
Git FAQ
Frequently asked questions around Git and Version Control.
Git FAQ featured image

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 .git directory.
  • GIT_INDEX_FILE — Path to the index (staging area) file. Note: if this variable is set during a post-commit hook, 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.

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.

Bypassing Pre-Commit Hooks in Tower

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

Bypassing Push Hooks in Tower

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.