Skip to content
SP StackPractices
beginner

Set Up Pre-Commit Hooks

How to set up pre-commit hooks with husky, lint-staged, and pre-commit to enforce code quality before commits

Topics: devops

Overview

Pre-commit hooks automatically run checks on your code before every commit. They catch linting errors, formatting issues, failing tests, and security vulnerabilities at the earliest possible moment—before they reach CI or production. This recipe covers setting up hooks with the pre-commit framework (Python), husky + lint-staged (JavaScript), and native Git hooks for Java projects.

When to Use

Use this resource when:

  • Your team repeatedly commits code that fails CI lint or format checks
  • You want to enforce code style without relying solely on PR reviews
  • You need to run secrets scanning or vulnerability checks on every commit
  • You want fast feedback: fix issues locally instead of waiting for CI to fail

Solution

Python

# Install pre-commit framework
# pip install pre-commit

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.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.12.1
    hooks:
      - id: black
        language_version: python3.11

  - repo: https://github.com/PyCQA/flake8
    rev: 7.0.0
    hooks:
      - id: flake8
        args: ['--max-line-length=100']

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.7.1
    hooks:
      - id: mypy

# Install hooks into .git/hooks/
# pre-commit install

# Run manually on all files
# pre-commit run --all-files

JavaScript

// package.json scripts + husky + lint-staged
// npm install --save-dev husky lint-staged prettier eslint

// package.json
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yml}": ["prettier --write"]
  },
  "scripts": {
    "prepare": "husky install",
    "lint": "eslint .",
    "format": "prettier --write ."
  }
}

// .husky/pre-commit (generated by npx husky add .husky/pre-commit)
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

// Or with the newer husky v9+ syntax:
// echo "npx lint-staged" > .husky/pre-commit

// .lintstagedrc.js
module.exports = {
  '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
  '*.{json,md,yaml}': ['prettier --write'],
};

Java

// Java projects typically use Maven or Gradle hooks, not husky.
// Option 1: Maven git hook plugin (com.rudikershaw.gitbuildhook)
// pom.xml:
/*
<plugin>
    <groupId>com.rudikershaw.gitbuildhook</groupId>
    <artifactId>git-build-hook-maven-plugin</artifactId>
    <version>3.5.0</version>
    <configuration>
        <installHooks>${project.basedir}/git-hooks</installHooks>
    </configuration>
</plugin>
*/

// Option 2: Gradle + Spotless + custom Git hook
// build.gradle:
plugins {
    id 'com.diffplug.spotless' version '6.23.0'
}
spotless {
    java {
        googleJavaFormat()
    }
}

// git-hooks/pre-commit (chmod +x)
#!/bin/sh
./gradlew spotlessCheck
if [ $? -ne 0 ]; then
    echo "Spotless check failed. Run './gradlew spotlessApply' to fix."
    exit 1
fi

Explanation

Git hooks are executable scripts in .git/hooks/ that run at specific lifecycle events. The pre-commit hook runs after git commit is invoked but before the commit is created. If the hook exits with a non-zero status, the commit is aborted.

How the tools work:

  • pre-commit (Python framework): Manages hook installation and execution across languages. Defined in .pre-commit-config.yaml.
  • husky + lint-staged: Husky installs the Git hook; lint-staged filters file paths so only staged files are checked, making commits fast.
  • Native Git hooks: Any executable script works. Use Maven/Gradle plugins to distribute hooks across the team.

Trade-offs:

  • Hooks add commit latency (seconds to tens of seconds)
  • Team members can bypass hooks with git commit --no-verify
  • Hooks must be installed per clone; CI is still the ultimate gate

Variants

TechnologyToolingNotes
Pythonpre-commit frameworkMature ecosystem; 200+ community hooks available
JavaScript / TypeScripthusky + lint-stagedIndustry standard for Node.js; fast because only staged files are checked
JavaMaven git-build-hook-plugin or Gradle spotlessRun formatters as part of build; hooks call ./gradlew spotlessCheck
Gopre-commit + golangci-lintUse the pre-commit framework with Go-specific hooks
Rustpre-commit + rustfmt / clippySame framework; community hooks available
Secrets scanninggitleaks, trufflehogPre-commit hooks prevent API keys and passwords from entering history

Best Practices

  1. Keep hooks fast: lint only staged files, not the entire codebase
  2. Auto-fix when possible: formatters should rewrite files, not just report errors
  3. Include a prepare or postinstall script so hooks are auto-installed on npm install or pip install
  4. Run the same checks in CI; hooks are a convenience, not a replacement for CI gates
  5. Document bypass procedures (--no-verify) for emergencies, but require PR review when used

Common Mistakes

  1. Checking the entire repo on every commit — lint-staged and pre-commit’s files filter ensure only changed files are checked
  2. Not auto-installing hooks — new clones skip hooks unless a prepare script installs them
  3. Conflicting formatters — ensure Prettier and ESLint rules agree; use eslint-config-prettier to disable conflicting ESLint format rules
  4. Hooks that modify files but don’t re-stage — if a hook reformats code, it must add the file back to the index or the commit will use the old version
  5. Relying only on hooks — developers can use --no-verify; CI must enforce the same rules

Frequently Asked Questions

Can I skip hooks for a specific commit?

Yes: git commit --no-verify (or -n). Use this sparingly and always follow up with a cleanup commit. Some teams require manager approval for --no-verify usage.

Should I run tests in pre-commit hooks?

Unit tests: sometimes, if they complete in under 10 seconds. Integration or E2E tests: never; they belong in CI. Slow hooks train developers to bypass them.

How do I share hooks across my team?

Use pre-commit (cross-language) or husky (Node.js). Both store hook configuration in the repo. For Java, use a Maven/Gradle plugin that installs hooks from a tracked git-hooks/ directory during build. Never commit files directly to .git/hooks/—that directory is not tracked.