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 hooks in simple terms:
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.

Pre-commit hook example (Bash):
#!/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.

Commit-msg hook example (Bash):
#!/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.

Pre-push hook example (Bash):
#!/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.

Pre-receive hook example (Bash):
#!/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.yaml file stored in the repository. Developers install the framework and hooks run automatically.
  • Symlinks: Store hooks in a committed hooks directory and use a script to symlink them to .git/hooks during 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.
Pre-commit framework configuration (.pre-commit-config.yaml):
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.

  1. Navigate to your repository's .git/hooks directory.
  2. Create a new file with the name of the hook you want to use (e.g., pre-commit).
  3. Add your script code (starting with a shebang line like #!/bin/bash).
  4. Make the script executable with chmod +x pre-commit.
  5. Test the hook by performing the Git operation that triggers it.
Quick hook creation example:
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-verify flag for emergency situations, but log when it is used.

Frequently Asked Questions

  1. Are Git hooks shared when I push to a remote?
    No. The .git/hooks directory 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.
  2. Can I skip Git hooks?
    Yes. Most Git commands that trigger hooks support a --no-verify flag. For example, git commit --no-verify skips pre-commit and commit-msg hooks. Use this sparingly, and consider logging when hooks are bypassed.
  3. 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.
  4. How do I debug a Git hook that is not working?
    Add logging statements to your hook script. You can also run Git with GIT_TRACE=1 to see detailed execution information. Check that the hook file is executable and has the correct name without the .sample extension.
  5. 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.
  6. 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.