Track
Git hooks let you automate tasks in your workflow by running scripts before or after Git actions like commits or pushes. They help catch issues early, enforce standards, and even fix problems automatically.
Let’s say you’ve written a script to enforce code styling standards. You can add that script to a hook that runs before a commit. Now, every time someone tries to commit, the hook runs. If the code passes the checks in the hook, the commit goes through. If it doesn’t, the commit is blocked.
And it’s not just about flagging problems. You can also customize the script to fix those issues before the commit happens. In this article, I’ll cover everything you need to know about Git hooks, what they are, which ones matter, and how to implement them effectively.
What Are Git Hooks?
Git hooks automatically run custom scripts when certain Git actions occur, such as before or after a commit, push, or merge. They come built-in with Git, so there’s no need to install any external libraries.
When you initialize a Git repository, Git creates the hooks by default. You’ll find them inside the .git/hooks
directory. These come with .sample
extensions, which prevent them from running by default. If you want them to run by default, just remove the .sample
extension.
You can write the hooks in any executable scripting language. For example, inside the pre-commit hook file, you can add a script that checks for "To-do" comments in staged files and blocks the commit if it finds any. That means every time you try to commit, Git scans for comments starting with “To-do” and fails if the pending tasks exist. Like this, you can create custom hooks for different git actions like post-commit, pre-merge, pre-push, and more.
Types of Git Hooks
Before we dive into practical examples, let’s understand the two types of Git hooks.
Client-side hooks: categories and common examples
Client-side hooks run on local machines during Git operations, like committing, rebasing, and pushing. Here are a few popular client hooks and their specific use cases.
- pre-commit: Runs before you even type the commit message. You can use it to enforce code styles, run tests, or check for syntax errors before committing.
- prepare-commit-msg: Runs after Git creates the default commit message, but before the commit message editor pops up. This hook can modify or replace the default message based on custom script logic.
- commit-msg: This hook runs right after the commit message is saved, but before Git completes the commit. It’s your last checkpoint to enforce commit message rules like formatting, structure, or tagging.
- post-commit: Runs after a commit is created. It’s often used for notifications (like sending a Slack message after a commit) or logging (storing commit metadata).
- post-checkout: Runs after a successful
git checkout
command. It kicks off when you change branches or check out a specific commit or path. You can use it to rebuild project dependencies, delete temp files, or notify about a new branch. - post-merge: Runs after a successful merge operation. It’s commonly used for updating dependencies, printing merge messages, or clearing caches.
- pre-rebase: Runs before a rebase operation. You can use it to prevent rebasing protected branches, run tests before allowing the rebase, or stop a rebase if there are uncommitted changes.
- pre-push: Runs before pushing changes. It helps prevent pushes to protected branches or run pre-push tests.
- post-rewrite: Runs after Git successfully rewrites commits. It triggers on operations like
git commit --amend
(which rewrites the last commit) orgit rebase
(which rewrites a series of commits). Use it to update references or trigger tests and CI steps after rewriting. - pre-applypatch: Invoked by the
git am
command, this hook runs after a patch has been applied to the working tree but before a new commit is created. If the checks in this hook fail, it can exit with a non-zero status and prevent the patch from being committed. - post-applypatch: Git runs this after applying the patch and creating the commit. It cannot abort the commit but works well for logging or sending notifications.
- applypatch-msg: Git runs this before
git am
applies the patch. Use it to check or edit the proposed commit message for the patch.
Server-side hooks: categories and use cases
Server-side hooks run on remote Git servers and trigger when remote events occur, such as before or after pushing to a repository.
- pre-receive: The remote runs this before it accepts a push. You can use it to enforce a consistent code style across users or run security checks before accepting changes from a client.
- update: While pre-receive runs once per push, the update hook runs for every reference (branch or tag) that’s being changed in a single push.
- post-receive: Runs after the git push operation is complete. It’s often used for sending notifications, comprehensive logging, or triggering CI/CD pipelines.
- post-update: Runs after Git updates all references in a push. You can use it for simple notifications because, unlike post-receive, it cannot access both the old and new logs of every reference update.
- reference-transaction: It fires when a reference transaction is prepared, committed, or aborted so that it may run multiple times. Use it to validate or reject updates across multiple references in a single transaction, or to log and audit ref changes.
- push-to-checkout: Triggers when you git push to a branch that’s currently checked out on the remote. With this hook, you can update files on the server after a push, log the changes, or block unsafe pushes directly to the working directory.
- pre-auto-gc: Runs just before Git triggers automatic garbage collection. You can use it to log the start of garbage collection, block it during resource-heavy operations, or handle any necessary pre-garbage collection tasks.
- proc-receive: Runs when the git-receive-pack command processes an incoming push. It updates references (branches and tags) in the remote repository and reports the results back to the client.
Setting Up Git Hooks
In this section, you will find the steps to install and set up the git hooks. You'll also see how to create custom hooks from scratch.
Installation and configuration mechanics
When you initialize a Git repository, Git automatically creates a set of hooks inside the .git/hooks
folder. To see them, open your terminal and go to the root of the repository where you ran git init
. From there, open the hidden .git
directory and then the hooks subdirectory. A simple cd
command does all this for you and provides access to the hooks subdirectory.
cd .git/hooks
Once you’re inside, you’ll see default hooks with the .sample
extension. To enable a default hook, remove the .sample
extension for that hook and make it executable.
For example, use the below command to remove the .sample
extension from the pre-commit hook.
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
Next, make it executable using the command below:
chmod +x .git/hooks/pre-commit
That covers editing and enabling the default hooks. To create custom hooks from scratch, follow these steps.
To open a new file in .git/hooks/
, run:
nano .git/hooks/commit-msg
Create the script in this file. Example:
#!/bin/sh
echo "✅ Commit message hook triggered"
Save and exit, then make it executable:
chmod +x .git/hooks/commit-msg
In the above example, whenever the commit goes through, the hooks print a message " Commit message hook triggered"
There’s another way to set up custom hooks. When you initialize a repository, Git includes default hooks. You can edit these defaults using template directories.
Here’s how it works: When you run git init
, Git copies the contents of the global template directory into the new repository’s .git/
directory, including hooks. If you add custom hook scripts to the global template directory, every new repository you create will inherit those custom hooks automatically.
Creating and customizing hook scripts
First, you need to figure out what you want to achieve. Is it catching vague commit messages, preventing accidental pushes, or clearing caches? Defining the objective upfront is important because different git hooks serve different purposes, and you should know which one to use.
For example, if you want to check the commit message style, you will use the commit-msg hook. If you want to avoid certain pushes to the main branch, you should do it with a pre-push hook.
Once you’ve picked the hook, the next decision is language. On macOS and Linux, a small Bash (or plain sh) script is often enough. On Windows, some teams lean on PowerShell.
If your repo is polyglot, you might prefer a universal interpreter like Python or Node.js so everyone can run the same logic. Therefore, the key is including a shebang (#!/bin/sh, #!/usr/bin/env python3, etc.) at the top so Git knows how to execute it.
A simple pre-commit hook example that prevents committing .env
files. This avoids accidentally pushing sensitive API keys.
#!/bin/sh
# Block committing secrets
if git diff --cached --name-only | grep -q ".env"; then
echo "❌ .env file detected in commit! Remove it before committing."
exit 1
fi
exit 0
When you want to disable hooks, the easiest way is to temporarily rename them (change pre-commit to pre-commit.off) or remove their execute permission.
However, if you only need to bypass it once, use git commit --no-verify
to skip commit hooks and git push --no-verify
to skip push hooks. You can even globally disable hooks by pointing Git to an empty folder with git config core.hooksPath /dev/null
.
Git Hooks Common Use Cases and Examples
Git hooks are especially useful for automating testing, security checks, and enforcing quality standards in your workflow.
Practical automation scenarios
Enforce code style or linting: The pre-commit hook is often used to automate code formatting and catch potential errors before the commit goes through.
For example, in the following pre-commit hook, prettier handles the formatting automatically and stages the changes before committing. A linter can then run right after, and the commit only succeeds if the linting passes. If it fails, the commit is stopped.
#!/bin/sh
# Collect staged JS/TS files
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|tsx)$')
[ -z "$FILES" ] && exit 0
echo "Formatting with Prettier..."
npx --yes prettier --write $FILES
# Re-stage files that Prettier changed
echo "$FILES" | xargs git add
echo "Linting with ESLint..."
npx --yes eslint $FILES
STATUS=$?
if [ $STATUS -ne 0 ]; then
echo "ESLint found issues. Fix them or commit with --no-verify if needed."
exit $STATUS
fi
echo "Pre-commit checks passed."
exit 0
Run automated tests: You can set the hooks to run automated tests before pushing any changes. For example, the following pre-push hook automatically discovers and runs available pytests in your repo before pushing the changes.
#!/bin/sh
echo "Running tests before push..."
pytest -q
STATUS=$?
[ $STATUS -eq 0 ] || { echo "Tests failed. Push aborted."; exit $STATUS;
Automatically block committing sensitive data: Sensitive files like .env
, .pem
, or .key
often contain secrets. The following pre-commit hook example checks for such patterns and blocks the commit if it contains them.
#!/bin/sh
set -e
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
# Block dotenv and private keys
echo "$STAGED" | grep -E '\.env(\.|$)|(^|/)\.env$|\.pem$|\.key$|id_rsa$' >/dev/null 2>&1 && {
echo "Sensitive file detected in staged changes. Remove it from the commit."
exit 1
}
Tips for Writing Effective Git Hooks
Here, I’ve included some tips I’ve found to be helpful when writing Git hooks.
Keep hooks fast
Hooks run every time you trigger certain Git actions, so keep them lightweight. If you have large test suites, run them in CI instead of in local hooks for better performance. Running linters, formatters, or checks across the entire repository on every commit will slow you down, so limit checks to the files that actually changed.
Maintain cross-platform compatibility
Ensure your hooks work smoothly on different operating systems. Use portable shebangs, handle line endings consistently (use LF over CRLF), and do not hardcode absolute paths.
Instead, rely on PATH
and local project scripts like npm run lint
or python -m
so it runs across multiple environments.
Debugging strategies
Add verbose logging or a debug flag (for example, HOOK_DEBUG=1
) to trace how the script runs. Print details like the current directory, branch, or files being staged. This makes it easier to follow the execution flow and spot where things go wrong.
Test hooks with different inputs and simulate failure scenarios locally before pushing changes. If a hook blocks progress unexpectedly, you can bypass it temporarily with Git’s --no-verify
flag or by using environment variables designed to skip hooks.
To maintain consistent behavior across operating systems, avoid OS-specific commands, and prefer portable scripting languages.
Document hooks clearly
Good documentation improves team understanding and eases the onboarding of new members. Maintain a central README detailing the purpose, behavior, and usage of each hook. Include how to run, prerequisites, troubleshooting, and bypassing tips. Also, provide installation/setup instructions.
Advanced Implementation Techniques and Best Practices for Git Hooks
If you’re a more advanced user, these tips can help:
Performance optimization
To optimize performance in complex workflows, reduce external dependencies by avoiding heavy libraries. This lowers startup time and cuts down on failure points.
Cache the results of expensive processes like linting so you can reuse them when files haven’t changed.
If hooks contain multiple checks, parallelize them so they run concurrently. Also, modularize hook scripts. That means structure hooks as reusable, small components that only execute relevant parts based on context.
Hook chaining and orchestration
Use hook chaining to run multiple hooks in a single git event. For example, in a pre-commit hook, you might want to run linting, then run tests, and then format code, all chained together. Each command runs one after another, and if any fail (exit with non-zero status), subsequent commands can be skipped to prevent a bad commit.
When the hook actions are independent, you can also run them in parallel to save time. That's where you use orchestration to manage task execution in the correct order.
Config management
Every repo has its own .git/hooks
directory. That means if you want the same checks (say, linting) in multiple projects, you have to copy-paste them everywhere. A better way is to manage hooks centrally so all repositories can access and run them.
You can do this with a single command: git config --global core.hooksPath ~/.githooks
. This tells Git to look for hooks in the ~/.githooks
folder. Any hooks you create there are automatically shared across all your repositories.
You can also make hook logic more dynamic with config files (JSON or YAML). Instead of hardcoding different logic for each repository, your hook script can read that repository’s config file and apply the correct rules automatically.
Conditional execution
Inside the hook scripts, you can add simple conditions so they only run when relevant. That could mean checking the current branch name, looking at which files actually changed, or reading an environment variable that tells them to skip.
If you want more control, use a framework like pre-commit, where you declare in a config file which hooks run at which stage. You can also add conditional logic, such as running hooks only on certain branches, skipping hooks with variables, or even adding time-based conditions. This makes your hook scripts dynamic and tailored to real workflows.
Collaboration and sharing strategies
There are two ways you can centralize git hooks and share across repositories and teams:
Git templates
When you initialize a repository, Git adds some default hooks under the git/hooks
directory. These come from Git’s default template.
To create custom hooks centrally, you can create a Git template directory, write your hooks there, and globally set Git to use this new directory as the default template.
git config --global init.templateDir /path/to/hooks-template
But this method lacks version control for the hooks. This is especially useful if your hooks change rarely. If you update hooks often and need version control, you’ll want to use the next method instead.
Central git repo
Create a dedicated, version-controlled, central repository and store all your custom hook scripts in that repo, separate from project repos. So you can make updates to the hooks independently of the project codebase.
These hooks should be standardized, meaning avoid hardcoding paths or values tied to a single repository.
This setup is especially useful for teams, since everyone can pull updates regularly and sync them into their projects.
A more efficient approach is to use a setup script that clones the central hooks repo and copies the hook scripts into the .git/hooks
directory of each project. Example setup script:
#!/usr/bin/env bash
set -euo pipefail
# Location of the shared hooks template repo
HOOKS_TEMPLATE_REPO="git@github.com:your-org/git-hooks-template.git"
HOOKS_TEMPLATE_DIR="$HOME/.git-hooks-template"
# Clone or update the template repo
echo "Cloning hooks template repo..."
git clone "$HOOKS_TEMPLATE_REPO" "$HOOKS_TEMPLATE_DIR"
# The repo where you want hooks installed
TARGET_REPO="${1:-$(pwd)}"
TARGET_HOOKS_DIR="$TARGET_REPO/.git/hooks"
# --- Copy strategy ---
echo "Copying hooks into $TARGET_HOOKS_DIR..."
cp -r "$HOOKS_TEMPLATE_DIR/"* "$TARGET_HOOKS_DIR/"
# 4. Make sure hooks are executable
chmod +x "$TARGET_HOOKS_DIR"/* || true
echo "✅ Hooks installed successfully into $TARGET_REPO"
Another option is to use symlinks. You can link the .git/hooks
directory of each project to the scripts in your version-controlled hooks repo. So whenever you update the central repo, the hooks automatically update because the symlink points to it.
To keep this system reliable, set up a clear process for maintaining hooks. Team members should propose hooks changes with pull requests, merge only after review, and keep the documentation updated as hooks evolve.
Integrating Git Hooks with Development Workflows
Git hooks play a big role in continuous integration (CI). For context, CI means merging changes frequently, then running builds and tests to catch issues early. By connecting hooks with CI/CD tools like Jenkins, GitHub Actions, or GitLab CI, you can trigger builds, run tests, and even deploy applications automatically based on Git events.
For example, a server-side pre-receive hook runs a custom script on the Git server before it accepts a push. If the script fails, the server rejects the push. You can use this to check whether the latest CI build on that branch passed before allowing the push. That CI run might include code coverage checks, unit or integration tests, or even security scans.
Hooks can also automate deployment tasks. A post-receive hook can trigger a webhook that starts a CI/CD pipeline. When code is merged into main, it automatically kicks off the build → deploy → release cycle.
Security Considerations and Best Practices
If you maintain central hooks for easier collaboration and shared truth, ensure the hooks directory has secure file permissions to prevent unauthorized access. Only trusted users should have write access.
You always have to validate the inputs or parameters your hook scripts receive to avoid code injection attacks. For example, validate inputs against strict patterns like only allowing branch names with alphanumerics and dashes, or tags that match vMAJOR.MINOR.PATCH
.
Never hardcode sensitive data in hook scripts. Instead, use environment variables or dedicated secret management tools to supply credentials securely at runtime. You can integrate tools like Gitleaks with pre-commit hooks to run security scans for sensitive information in the changes you’re about to commit.
Someone can redirect your hook to send the code or information to a different server. So limit which domains your hooks can reach. Use a proxy or firewall to block unknown destination servers. Also, ensure your logs don’t show confidential info in the display messages.
Conclusion
Git hooks are a powerful way to automate tasks and enforce standards directly in your development workflow. They help with everything from code style and testing to deployment and security checks.
By using them well, you reduce errors, save time, and make collaboration smoother across teams.
Start with simple pre-commit checks, then experiment with more advanced setups like orchestration, central management, or CI/CD integrations. If you are a Git user and want to advance your skills, check out this advanced Git course.
Git Hooks FAQs
Where do I find Git hooks?
The git default path for hooks is . git/hooks
and this folder is usually a hidden folder, so you can only access it on bash or terminal.
What are best practices for setting up pre-commit hooks?
Keep hooks fast and run them on staged files for higher performance. Do not use pre-commit hooks for time-consuming tasks like running a full test suite; use your CI/CD pipeline for those. And document their purpose clearly.
How do server-side Git hooks differ from client-side hooks?
Client-side hooks run on developer machines for feedback and auto-fixes. Examples: pre-commit, pre-push.
Server-side hooks run on the Git remote server and critical checks for seamless production pipelines and effective team collaboration. Examples: pre-receive, update. If a server hook rejects a push, it does not land.
Can I use Git hooks to enforce coding standards?
Use pre-commit to format and lint, commit-msg to enforce message conventions, pre-push for fast tests, and server pre-receive to block secrets from committing to remote.
Srujana is a freelance tech writer with the four-year degree in Computer Science. Writing about various topics, including data science, cloud computing, development, programming, security, and many others comes naturally to her. She has a love for classic literature and exploring new destinations.