DEV Community

arasosman
arasosman

Posted on • Originally published at mycuriosity.blog

Git Hooks for Automated Code Quality Checks Guide 2025

Introduction

Every development team struggles with maintaining consistent code quality. Code reviews catch many issues, but what if you could prevent problematic code from even being committed? Git hooks provide the perfect solution—automated scripts that run at specific points in your Git workflow.

Git hooks act as your first line of defense against code quality issues. They can automatically format code, run tests, check for security vulnerabilities, and enforce coding standards before changes ever leave a developer's machine. This proactive approach saves time, reduces review cycles, and maintains high code quality standards across your entire team.

In this comprehensive guide, you'll learn how to implement Git hooks that automatically enforce code quality checks, making your development workflow more efficient and your codebase more maintainable.

Understanding Git Hooks

What Are Git Hooks?

Git hooks are scripts that Git executes before or after events such as commit, push, and receive. They're stored in the .git/hooks directory of every Git repository and can be written in any scripting language—Bash, Python, Node.js, or any executable script.

Types of Git Hooks

Git provides two types of hooks:

Client-Side Hooks:

  • pre-commit: Runs before a commit is created
  • prepare-commit-msg: Runs before the commit message editor is opened
  • commit-msg: Validates the commit message
  • post-commit: Runs after a commit is created
  • pre-push: Runs before pushing to a remote repository
  • pre-rebase: Runs before a rebase operation

Server-Side Hooks:

  • pre-receive: Runs before accepting pushed commits
  • update: Similar to pre-receive but runs once per branch
  • post-receive: Runs after push is complete

How Git Hooks Work

When you initialize a Git repository, Git creates sample hook scripts in .git/hooks/:

ls .git/hooks/ # applypatch-msg.sample # commit-msg.sample # pre-commit.sample # pre-push.sample # pre-rebase.sample # ... 
Enter fullscreen mode Exit fullscreen mode

To activate a hook, remove the .sample extension and ensure the script is executable:

mv .git/hooks/pre-commit.sample .git/hooks/pre-commit chmod +x .git/hooks/pre-commit 
Enter fullscreen mode Exit fullscreen mode

Setting Up Your First Git Hook

Creating a Simple Pre-Commit Hook

Let's create a basic pre-commit hook that checks for debugging statements:

#!/bin/sh # .git/hooks/pre-commit # Check for console.log statements in JavaScript files if git diff --cached --name-only | grep -E '\.js$' | xargs grep -n 'console\.log'; then echo "Error: Found console.log statements in JavaScript files" echo "Please remove them before committing" exit 1 fi exit 0 
Enter fullscreen mode Exit fullscreen mode

Making Hooks Executable

chmod +x .git/hooks/pre-commit 
Enter fullscreen mode Exit fullscreen mode

Testing Your Hook

# Add a file with console.log echo "console.log('debug');" > test.js git add test.js git commit -m "Test commit" # Error: Found console.log statements in JavaScript files 
Enter fullscreen mode Exit fullscreen mode

Common Code Quality Checks

Linting and Code Style

ESLint for JavaScript/TypeScript

#!/bin/sh # .git/hooks/pre-commit # Run ESLint on staged JavaScript/TypeScript files STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx)$') if [ -n "$STAGED_FILES" ]; then echo "Running ESLint..." npx eslint $STAGED_FILES --fix # Add fixed files back to staging git add $STAGED_FILES # Re-run to check if issues remain if ! npx eslint $STAGED_FILES; then echo "ESLint failed. Please fix errors before committing." exit 1 fi fi 
Enter fullscreen mode Exit fullscreen mode

Python Black Formatter

#!/bin/sh # .git/hooks/pre-commit # Format Python files with Black STAGED_PY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$') if [ -n "$STAGED_PY_FILES" ]; then echo "Running Black formatter..." black $STAGED_PY_FILES git add $STAGED_PY_FILES fi 
Enter fullscreen mode Exit fullscreen mode

Running Tests

Pre-Push Test Runner

#!/bin/sh # .git/hooks/pre-push echo "Running tests before push..." # Run different test suites based on changed files if git diff --name-only @{u}... | grep -E '\.(js|ts)$' > /dev/null; then echo "Running JavaScript tests..." if ! npm test; then echo "Tests failed. Push aborted." exit 1 fi fi if git diff --name-only @{u}... | grep '\.py$' > /dev/null; then echo "Running Python tests..." if ! python -m pytest; then echo "Tests failed. Push aborted." exit 1 fi fi echo "All tests passed!" 
Enter fullscreen mode Exit fullscreen mode

Security Scanning

Detecting Secrets

#!/bin/sh # .git/hooks/pre-commit # Check for potential secrets PATTERNS=( 'password\s*=\s*["\'][^"\']+["\']' 'api[_-]?key\s*=\s*["\'][^"\']+["\']' 'secret\s*=\s*["\'][^"\']+["\']' 'token\s*=\s*["\'][^"\']+["\']' 'BEGIN RSA PRIVATE KEY' 'BEGIN DSA PRIVATE KEY' 'BEGIN EC PRIVATE KEY' 'BEGIN PGP PRIVATE KEY' ) STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM) for pattern in "${PATTERNS[@]}"; do if echo "$STAGED_FILES" | xargs grep -E -i "$pattern" 2>/dev/null; then echo "Error: Potential secret detected!" echo "Please remove sensitive information before committing." exit 1 fi done 
Enter fullscreen mode Exit fullscreen mode

Commit Message Validation

#!/bin/sh # .git/hooks/commit-msg # Enforce conventional commit format commit_regex='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,50}' if ! grep -qE "$commit_regex" "$1"; then echo "Invalid commit message format!" echo "Format: <type>(<scope>): <subject>" echo "Example: feat(auth): add login functionality" echo "" echo "Types: feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert" exit 1 fi # Check message length if [ $(head -1 "$1" | wc -c) -gt 72 ]; then echo "Commit message too long. Keep under 72 characters." exit 1 fi 
Enter fullscreen mode Exit fullscreen mode

Advanced Hook Implementation

Multi-Language Support Hook

#!/bin/bash # .git/hooks/pre-commit # Color output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color echo -e "${YELLOW}Running pre-commit checks...${NC}" # Track overall status PASS=true # JavaScript/TypeScript checks JS_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|jsx|ts|tsx)$') if [ -n "$JS_FILES" ]; then echo -e "${YELLOW}Checking JavaScript/TypeScript files...${NC}" # ESLint if ! npx eslint $JS_FILES; then PASS=false fi # Prettier npx prettier --write $JS_FILES git add $JS_FILES fi # Python checks PY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$') if [ -n "$PY_FILES" ]; then echo -e "${YELLOW}Checking Python files...${NC}" # Flake8 if ! flake8 $PY_FILES; then PASS=false fi # Black black $PY_FILES git add $PY_FILES fi # Go checks GO_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$') if [ -n "$GO_FILES" ]; then echo -e "${YELLOW}Checking Go files...${NC}" # gofmt UNFORMATTED=$(gofmt -l $GO_FILES) if [ -n "$UNFORMATTED" ]; then echo "Go files must be formatted with gofmt:" echo "$UNFORMATTED" PASS=false fi # go vet if ! go vet ./...; then PASS=false fi fi # Final status if [ "$PASS" = true ]; then echo -e "${GREEN}All checks passed!${NC}" exit 0 else echo -e "${RED}Some checks failed. Please fix issues before committing.${NC}" exit 1 fi 
Enter fullscreen mode Exit fullscreen mode

Performance Optimization

#!/bin/bash # .git/hooks/pre-commit # Only check modified files get_staged_files() { git diff --cached --name-only --diff-filter=ACM | grep -E "$1" } # Run checks in parallel run_parallel_checks() { local pids=() # JavaScript lint in background if [ -n "$(get_staged_files '\.(js|jsx|ts|tsx)$')" ]; then npx eslint $(get_staged_files '\.(js|jsx|ts|tsx)$') & pids+=($!) fi # Python lint in background if [ -n "$(get_staged_files '\.py$')" ]; then flake8 $(get_staged_files '\.py$') & pids+=($!) fi # Wait for all background jobs local failed=0 for pid in "${pids[@]}"; do if ! wait $pid; then failed=1 fi done return $failed } # Execute checks if ! run_parallel_checks; then echo "Pre-commit checks failed!" exit 1 fi 
Enter fullscreen mode Exit fullscreen mode

Using Git Hook Managers

Husky (JavaScript/Node.js)

Installation

npm install --save-dev husky npx husky install npm pkg set scripts.prepare="husky install" 
Enter fullscreen mode Exit fullscreen mode

Configuration

# Add pre-commit hook npx husky add .husky/pre-commit "npm run lint" # Add pre-push hook npx husky add .husky/pre-push "npm test" 
Enter fullscreen mode Exit fullscreen mode

Package.json Integration

{ "scripts": { "prepare": "husky install", "lint": "eslint . --fix", "test": "jest" }, "lint-staged": { "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], "*.{json,md}": ["prettier --write"] } } 
Enter fullscreen mode Exit fullscreen mode

Pre-commit Framework (Python)

Installation

pip install pre-commit 
Enter fullscreen mode Exit fullscreen mode

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 - id: check-merge-conflict - repo: https://github.com/psf/black rev: 23.1.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 rev: 6.0.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-eslint rev: v8.36.0 hooks: - id: eslint files: \.(js|jsx|ts|tsx)$ 
Enter fullscreen mode Exit fullscreen mode

Setup

# Install hooks pre-commit install # Run on all files pre-commit run --all-files # Update hooks pre-commit autoupdate 
Enter fullscreen mode Exit fullscreen mode

Lefthook (Multi-language)

Configuration (lefthook.yml)

pre-commit: parallel: true commands: eslint: glob: "*.{js,jsx,ts,tsx}" run: npx eslint {staged_files} prettier: glob: "*.{js,jsx,ts,tsx,json,md}" run: npx prettier --write {staged_files} && git add {staged_files} python-black: glob: "*.py" run: black {staged_files} && git add {staged_files} go-fmt: glob: "*.go" run: gofmt -w {staged_files} && git add {staged_files} pre-push: commands: test-js: run: npm test test-python: run: python -m pytest audit: run: npm audit commit-msg: commands: commitlint: run: npx commitlint --edit 
Enter fullscreen mode Exit fullscreen mode

Best Practices for Git Hooks

Keep Hooks Fast

#!/bin/bash # Set timeout for long-running checks timeout_check() { timeout 30s "$@" local status=$? if [ $status -eq 124 ]; then echo "Check timed out after 30 seconds" return 1 fi return $status } # Use timeout for potentially slow operations timeout_check npm run lint timeout_check npm test 
Enter fullscreen mode Exit fullscreen mode

Make Hooks Configurable

#!/bin/bash # Allow developers to skip hooks when needed # Check for skip environment variable if [ "$SKIP_HOOKS" = "1" ]; then echo "Skipping pre-commit hooks (SKIP_HOOKS=1)" exit 0 fi # Check for --no-verify flag if [ "$GIT_SKIP_HOOKS" = "1" ]; then exit 0 fi # Run normal hooks... 
Enter fullscreen mode Exit fullscreen mode

Provide Clear Error Messages

#!/bin/bash # Function for formatted output print_error() { echo -e "\033[0;31m✗ $1\033[0m" } print_success() { echo -e "\033[0;32m✓ $1\033[0m" } print_info() { echo -e "\033[0;34mℹ $1\033[0m" } # Example usage if ! npm run lint; then print_error "Linting failed" print_info "Run 'npm run lint:fix' to automatically fix issues" print_info "Or commit with --no-verify to skip hooks" exit 1 fi print_success "All checks passed!" 
Enter fullscreen mode Exit fullscreen mode

Version Control Your Hooks

# Create hooks directory in your project mkdir .githooks # Copy hooks cp .git/hooks/pre-commit .githooks/ # Create setup script cat > setup-hooks.sh << 'EOF' #!/bin/bash echo "Setting up Git hooks..." # Create symlinks ln -sf ../../.githooks/pre-commit .git/hooks/pre-commit ln -sf ../../.githooks/pre-push .git/hooks/pre-push chmod +x .git/hooks/* echo "Git hooks installed successfully!" EOF chmod +x setup-hooks.sh 
Enter fullscreen mode Exit fullscreen mode

Troubleshooting Common Issues

Hook Not Executing

# Check if hook is executable ls -la .git/hooks/pre-commit # Make executable chmod +x .git/hooks/pre-commit # Check shebang line head -1 .git/hooks/pre-commit # Should be: #!/bin/sh or #!/bin/bash 
Enter fullscreen mode Exit fullscreen mode

Hook Failing on CI/CD

# Detect CI environment if [ -n "$CI" ] || [ -n "$CONTINUOUS_INTEGRATION" ]; then echo "Running in CI environment, skipping interactive checks" exit 0 fi 
Enter fullscreen mode Exit fullscreen mode

Windows Compatibility

#!/bin/sh # Handle Windows line endings # Convert CRLF to LF for staged files if [ "$OS" = "Windows_NT" ]; then git config core.autocrlf true fi # Use cross-platform commands # Instead of: grep -E # Use: git grep -E 
Enter fullscreen mode Exit fullscreen mode

Integration with Development Workflows

IDE Integration

VS Code Settings

{ "git.enableCommitSigning": true, "git.confirmSync": false, "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } } 
Enter fullscreen mode Exit fullscreen mode

JetBrains IDE Configuration

<!-- .idea/git_hooks.xml --> <component name="GitHooks"> <option name="preCommitHook" value=".githooks/pre-commit" /> <option name="prePushHook" value=".githooks/pre-push" /> </component> 
Enter fullscreen mode Exit fullscreen mode

CI/CD Pipeline Integration

# GitHub Actions name: Code Quality on: [push, pull_request] jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run pre-commit hooks uses: pre-commit/action@v3.0.0 - name: Run tests run: | npm install npm test 
Enter fullscreen mode Exit fullscreen mode

FAQ Section

Q: Can I skip Git hooks temporarily?

Yes, use git commit --no-verify or git push --no-verify to skip hooks for a single operation. You can also set SKIP_HOOKS=1 environment variable if your hooks support it.

Q: How do I share Git hooks with my team?

Store hooks in a directory like .githooks/ in your repository and provide a setup script. Alternatively, use hook managers like Husky or pre-commit that automatically install hooks.

Q: Do Git hooks work with GUI Git clients?

Most GUI clients respect Git hooks, but some may not show hook output clearly. Test your hooks with the specific GUI tools your team uses.

Q: Can hooks modify staged files?

Yes, hooks can modify files, but you must re-stage them using git add within the hook script for changes to be included in the commit.

Q: What happens if a hook script fails?

If a pre-commit or pre-push hook exits with a non-zero status, Git cancels the operation. Post-hooks don't affect the Git operation since they run after completion.

Q: How do I debug Git hooks?

Add debug output using echo statements, check the hook's exit status with echo $?, and test hooks manually by running them directly from the command line.

Conclusion

Git hooks are powerful tools that automate code quality checks and enforce development standards. By implementing the hooks and practices outlined in this guide, you'll create a more robust development workflow that catches issues early and maintains consistent code quality.

Key takeaways:

  • Start simple with basic linting and formatting hooks
  • Use hook managers to simplify setup and maintenance
  • Keep hooks fast to avoid disrupting developer flow
  • Provide clear error messages and recovery options
  • Share hooks across your team for consistent standards

Begin implementing Git hooks in your projects today. Start with a simple pre-commit hook for linting, then gradually add more sophisticated checks as your team becomes comfortable with the workflow.

Have you implemented Git hooks in your projects? Share your experiences and custom hook scripts in the comments below!

Top comments (0)