Git Hooks: Automate Tasks Before and After Commits
Git hooks are scripts that run automatically before or after events like commits.
Git Hooks: Automate Tasks Before and After Commits
Git hooks are scripts that run automatically before or after specific Git events, such as committing, pushing, or merging. They are one of Git's most powerful yet underutilized features, enabling you to automate tasks like running tests, enforcing code standards, checking commit message formats, and preventing bad commits from ever reaching your repository. Hooks give you the ability to customize Git's behavior to fit your team's workflow and quality standards.
Every Git repository contains a hidden .git/hooks directory with sample hook scripts. These samples are disabled by default but serve as templates for creating your own hooks. To enable a hook, you simply remove the .sample extension from the filename and make the script executable. Hooks can be written in any scripting language, including Bash, Python, Ruby, or JavaScript, making them accessible to developers regardless of their preferred language. To understand hooks properly, it is helpful to be familiar with Git core concepts, basic Git workflow, and Git best practices.
Git event (commit, push, etc.)
│
▼
Git checks for hook script
│
▼
If hook exists and is executable, Git runs it
│
▼
Hook can allow, reject, or modify the operation
Example: Pre-commit hook runs tests before allowing a commit to proceed
What Are Git Hooks
Git hooks are user-defined scripts that Git executes at specific points in the Git lifecycle. They are stored in the .git/hooks directory of your repository and are not cloned when you push or pull. This means each developer must set up their own hooks, or you can use tools like pre-commit (the framework) to share hooks across your team through configuration files stored in the repository.
Hooks can be divided into two main categories: client-side hooks and server-side hooks. Client-side hooks run on your local machine during operations like committing and merging. Server-side hooks run on the remote server during operations like receiving pushed commits. Client-side hooks are useful for enforcing code quality and preventing mistakes locally, while server-side hooks are essential for enforcing team policies on shared branches.
Why Git Hooks Matter
Git hooks automate repetitive tasks and enforce quality standards without requiring developers to remember extra steps. They catch problems early, before they become part of your project history.
- Prevent Bad Commits: Run tests and linting before commits to ensure bad code never enters the repository.
- Enforce Commit Message Standards: Validate that commit messages follow your team's format, such as Conventional Commits.
- Automate Code Formatting: Run formatters like Prettier or Black automatically before commits.
- Check for Secrets: Scan for accidentally committed API keys, passwords, or tokens and reject the commit.
- Run Tests Automatically: Ensure all tests pass before allowing a push to a shared branch.
- Deploy After Push: Trigger deployments or notifications after successful pushes to specific branches.
- Enforce Branch Naming Conventions: Validate that branch names follow team standards before allowing commits.
Types of Git Hooks
| Hook Name | When It Runs | Use Case |
|---|---|---|
| pre-commit | Before the commit is created | Run linters, format code, check for secrets |
| prepare-commit-msg | After the default commit message is created, before editor opens | Modify commit message template, add issue numbers |
| commit-msg | After the commit message is written, before the commit is finalized | Validate commit message format |
| post-commit | After the commit is created | Send notifications, trigger backup, update documentation |
| pre-rebase | Before a rebase operation | Prevent rebasing on certain branches |
| post-checkout | After a successful checkout or clone | Set up environment, install dependencies |
| post-merge | After a successful merge | Reinstall dependencies, run migrations |
| pre-push | Before a push operation | Run full test suite, prevent pushing to protected branches |
| pre-receive | On the server before any refs are updated | Enforce team policies, reject commits that break standards |
| update | On the server for each branch being pushed | Enforce branch-specific rules, prevent force push to main |
| post-receive | On the server after refs are updated | Trigger deployments, send notifications, update issue trackers |
Client-Side Hooks
Client-side hooks run on your local machine and are the most commonly used hooks for enforcing code quality and preventing mistakes before commits reach the remote repository.
Pre-Commit Hook
The pre-commit hook runs before the commit is created. It is the most popular hook for running linters, formatters, and tests. If the script exits with a non-zero status, the commit is aborted. This prevents bad code from ever being committed, even locally.
#!/bin/bash
# .git/hooks/pre-commit
# Run linter on staged JavaScript files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.js$')
if [ -n "$STAGED_FILES" ]; then
echo "Running ESLint on staged files..."
npx eslint $STAGED_FILES
if [ $? -ne 0 ]; then
echo "ESLint failed. Commit aborted."
exit 1
fi
fi
# Check for debugging statements
if git diff --cached | grep -E "^\+.*(console\.log|debugger)"; then
echo "Found console.log or debugger in staged changes. Commit aborted."
exit 1
fi
echo "Pre-commit checks passed."
exit 0
Commit-Msg Hook
The commit-msg hook runs after the user writes a commit message but before the commit is created. It is ideal for validating that commit messages follow a specific format, such as Conventional Commits or your team's custom rules.
#!/bin/bash
# .git/hooks/commit-msg
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat $COMMIT_MSG_FILE)
# Conventional Commits format: type(scope): subject
PATTERN="^(feat|fix|docs|style|refactor|test|chore)(\([a-z]+\))?: .{1,72}$"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "ERROR: Commit message does not follow Conventional Commits format"
echo ""
echo "Valid format: type(scope): subject"
echo "Example: feat(auth): add login functionality"
echo ""
echo "Your message: $COMMIT_MSG"
exit 1
fi
echo "Commit message format valid."
exit 0
Pre-Push Hook
The pre-push hook runs before a push operation. It is useful for running more expensive tests that you do not want to run on every commit, such as the full test suite or integration tests. If the hook fails, the push is aborted.
#!/bin/bash
# .git/hooks/pre-push
echo "Running full test suite before push..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Push aborted."
exit 1
fi
# Prevent force push to main branch
while read local_ref local_sha remote_ref remote_sha; do
if [ "$remote_ref" = "refs/heads/main" ]; then
echo "Force push to main branch is not allowed."
exit 1
fi
done
echo "Pre-push checks passed."
exit 0
Server-Side Hooks
Server-side hooks run on the remote repository server during push operations. They are essential for enforcing team policies that cannot be bypassed by individual developers, such as preventing commits that break the build or enforcing branch protection rules.
Pre-Receive Hook
The pre-receive hook runs on the server before any references are updated. It receives a list of all references being pushed and can reject the entire push if any validation fails. This hook is commonly used to enforce code quality standards, run tests, or prevent pushes to protected branches.
#!/bin/bash
# hooks/pre-receive (on server)
# Read each ref being updated
while read old_sha new_sha ref; do
# Prevent direct push to main branch
if [ "$ref" = "refs/heads/main" ]; then
echo "Direct push to main branch is not allowed. Use pull requests."
exit 1
fi
# Check for large files
for file in $(git diff --name-only $old_sha $new_sha); do
size=$(git cat-file -s "$new_sha:$file")
if [ $size -gt 1048576 ]; then # 1MB limit
echo "File $file exceeds size limit (1MB)."
exit 1
fi
done
done
exit 0
Sharing Hooks Across Your Team
By default, hooks are stored in the .git/hooks directory, which is not committed to the repository. This means each developer must set up hooks manually, leading to inconsistency. Several solutions exist for sharing hooks across a team.
- Pre-commit Framework: The most popular solution. Hooks are defined in a
.pre-commit-config.yamlfile stored in the repository. Developers install the framework and hooks run automatically. - Symlinks: Store hooks in a committed
hooksdirectory and use a script to symlink them to.git/hooksduring project setup. - Husky (JavaScript): Popular for Node.js projects, Husky manages Git hooks through package.json and installs automatically with npm install.
- Lefthook: Fast, cross-platform hook manager written in Rust, supports multiple languages and parallel execution.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.40.0
hooks:
- id: eslint
Creating Your Own Git Hook
Creating a Git hook is straightforward. Follow these steps to create a custom hook for your repository.
- Navigate to your repository's
.git/hooksdirectory. - Create a new file with the name of the hook you want to use (e.g.,
pre-commit). - Add your script code (starting with a shebang line like
#!/bin/bash). - Make the script executable with
chmod +x pre-commit. - Test the hook by performing the Git operation that triggers it.
cd my-repo
cd .git/hooks
echo '#!/bin/bash' > pre-commit
echo 'echo "Running pre-commit checks..."' >> pre-commit
echo 'exit 0' >> pre-commit
chmod +x pre-commit
Common Git Hooks Mistakes to Avoid
- Slow Hooks: Running expensive operations in pre-commit hooks frustrates developers and leads to bypassing hooks. Keep hooks fast or use pre-push for slower operations.
- Not Making Hooks Executable: A hook script will not run if it does not have execute permissions. Always run
chmod +x hook-name. - No Error Handling: Hooks that fail silently defeat their purpose. Always provide clear error messages when a hook rejects an operation.
- Inconsistent Team Hooks: Relying on manual hook setup leads to inconsistency. Use a hook management framework like pre-commit or Husky.
- Modifying Untracked Files: Hooks that modify tracked files without staging them can lead to confusion. Stage changes explicitly if needed.
- Ignoring Hook Exit Codes: A hook must exit with 0 to allow the operation to proceed. Any non-zero exit aborts the operation.
Git Hooks Best Practices
- Keep Hooks Fast: Pre-commit hooks should complete in under a second. Move expensive checks to pre-push hooks.
- Provide Clear Error Messages: When a hook fails, tell the developer exactly what went wrong and how to fix it.
- Use Hook Management Tools: Tools like pre-commit, Husky, or Lefthook make it easy to share hooks across your team.
- Version Control Hook Configurations: Store hook configuration files in your repository so all team members use the same hooks.
- Test Your Hooks: Test hooks thoroughly to ensure they do not produce false positives or block legitimate operations.
- Allow Bypassing When Necessary: Consider supporting a
--no-verifyflag for emergency situations, but log when it is used.
Frequently Asked Questions
- Are Git hooks shared when I push to a remote?
No. The.git/hooksdirectory is not part of the repository and is not cloned or pushed. Each developer must set up their own hooks, or you must use a hook management framework that stores configuration in the repository. - Can I skip Git hooks?
Yes. Most Git commands that trigger hooks support a--no-verifyflag. For example,git commit --no-verifyskips pre-commit and commit-msg hooks. Use this sparingly, and consider logging when hooks are bypassed. - What languages can I use to write Git hooks?
Any language that can be executed as a script. Bash is the most common, but you can use Python, Ruby, Node.js, or any other language. The key is to include the correct shebang line at the top of the script. - How do I debug a Git hook that is not working?
Add logging statements to your hook script. You can also run Git withGIT_TRACE=1to see detailed execution information. Check that the hook file is executable and has the correct name without the.sampleextension. - What is the difference between pre-receive and update hooks?
Pre-receive runs once for the entire push operation and receives a list of all references being updated. Update runs once for each reference being pushed and can reject individual branches. Pre-receive is better for rejecting an entire push; update is better for branch-specific rules. - What should I learn next after understanding Git hooks?
After mastering Git hooks, explore Git workflows, Git best practices, CI/CD pipelines, and advanced Git commands for comprehensive version control mastery.
Conclusion
Git hooks are a powerful feature that allows you to automate tasks, enforce quality standards, and prevent mistakes at critical points in the Git workflow. Whether you are running linters before commits, validating commit message formats, or running tests before pushes, hooks help you maintain a clean and reliable repository.
Client-side hooks like pre-commit and pre-push catch problems early, before they reach the remote repository. Server-side hooks like pre-receive enforce team policies that cannot be bypassed by individual developers. By sharing hook configurations using tools like pre-commit or Husky, your entire team benefits from consistent automation.
The key to successful hook usage is keeping them fast, providing clear error messages, and testing them thoroughly. Start with simple hooks like trailing whitespace removal and commit message validation, then gradually add more sophisticated checks as your team's needs evolve.
To deepen your understanding, explore related topics like Git workflows, Git best practices, CI/CD pipelines, and advanced Git commands. Together, these skills form a complete foundation for professional version control.
