Skip to content

Pre-commit Hooks Collection

pre-commit logo

What are Git hooks?

Git hooks are scripts that run automatically at some stage in the git-lifecycle. Most commonly, pre-commit hooks are used, running before a commit goes through. They act as a first line of defense for code quality by:

  • Catching formatting issues
  • Finding potential security risks
  • Validating configurations
  • Running quick tests
  • Enforcing team standards

Why use pre-commit?

Pre-commit is a framework that makes these Git hooks:

  1. Easy to share - Hooks are defined in a single YAML file
  2. Language-agnostic - Works with Python, JavaScript, and more
  3. Fast - Only runs on staged files and are much quicker than CI/CD
  4. Forgettable - Team members don't need to memorize QA tools; hooks run automatically
  5. Extendable - Large ecosystem of ready-to-use hooks

Pre-commit helps maintain code quality without slowing down development. While CI/CD pipelines might take minutes to run, pre-commit hooks provide instant feedback right when you commit. Despite the name, Pre-commit can install hooks at any stage (ex: Use a pre-push hook as a slightly more time-intensive pre-commit and push multiple commits at once.)

Common Use Cases

Pre-commit can be as strict as you want depending on your project's quality-time tradeoff. Here are cases where commit-level checks make more sense than pull-request level:

  1. Linting/formatting code and data files
  2. Re-building code or documentation
  3. Making database migrations
  4. Preventing secrets or large files from being committed
  5. Requiring commit messages to follow a standard (Like Commitizen)
  6. Running fast tests
Alternatives (Husky)

Husky is an alternative to pre-commit that's primarily designed for the NodeJS ecosystem. To my knowledge, while both tools handle Git hooks effectively, pre-commit offers broader multi-language support and has become standard in the Python community.

Installation

  1. The repository comes with a .pre-commit-config.yaml file already configured
  2. Install the hooks with:
pre-commit install

You'll see hooks run automatically on every commit: Pre-commit Example

Useful Commands:

# Update hooks to their latest versions
pre-commit autoupdate

# Reinstall hooks (needed after config changes)
pre-commit install

# Test hooks without committing
pre-commit run --all-files

Hooks

This collection prioritizes best-in-class tools without redundancy. Rather than using multiple overlapping tools, we've selected the most effective option for each task. For example:

  • Python linting uses only Ruff instead of multiple separate linters
  • JSON/YAML/TOML validation uses specialized schema validators
  • Security scanning uses a single comprehensive tool

01 ๐Ÿ”’ Security

GitLeaks is a fast, lightweight scanner that prevents secrets (passwords, API keys, tokens) from being committed to your repository.

- repo: https://github.com/gitleaks/gitleaks
  rev: v8.22.1
  hooks:
    - id: gitleaks
      name: "๐Ÿ”’ security ยท Detect hardcoded secrets"
Alternatives to GitLeaks (TruffleHog)

TruffleHog offers more comprehensive and continuous security scanning across a variety of platforms (not just files). However, it requires more setup time and resources than GitLeaks. Consider TruffleHog for expansive projects with strict security requirements.

02 ๐Ÿ” Code Quality

This section covers tools for code formatting, linting, type checking, and schema validation across different languages and file types. Best-in-class tools were chosen, avoiding redundant functionality. I opted for remote hook downloads over local commands to make the file more portable and self-updating.

๐Ÿ python

Ruff is a fast, comprehensive Python formatter and linter that replaces multiple traditional tools (Black, Flake8, isort, pyupgrade, bandit, pydoclint, mccabe complexity, and more.) While it's not yet at 100% parity with all these tools, its speed and broad coverage make it an excellent choice as this project's only Python linter/formatter:

- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.8.6
  hooks:
    - id: ruff-format
      name: "๐Ÿ python ยท Format with Ruff"

- repo: https://github.com/pre-commit/mirrors-mypy
  rev: "v1.14.1"
  hooks:
    - id: mypy
      name: "๐Ÿ python ยท Check types"

- repo: https://github.com/abravalheri/validate-pyproject
  rev: v0.23
  hooks:
    - id: validate-pyproject
      name: "๐Ÿ python ยท Validate pyproject.toml"
      additional_dependencies: ["validate-pyproject-schema-store[all]"]
Alternatives to Ruff (Too Many to Name)

ruff

Before Ruff, a typical Python project might use several separate tools:

While these tools are battle-tested and highly configurable, using Ruff provides several advantages:

  1. Speed: Ruff is 10-100x faster as it's written in Rust
  2. Simplicity: Single configuration file and consistent interface
  3. Active Development: Rapidly adding features and reaching feature parity
  4. Modern Defaults: Better handling of new Python features

Consider using individual tools if you need specific features not yet supported by Ruff or have complex existing configurations you need to maintain.


While Ruff does many things, type checking it does not... yet.

MyPy handles Python type checking:

- repo: https://github.com/pre-commit/mirrors-mypy
  hooks:
    - id: mypy
Alternatives to MyPy (Pyright)

Microsoft's Pyright is a faster and more featureful alternative to MyPy. While it's the preferred choice for type checking, there isn't currently a maintained pre-commit hook available. Consider using Pyright through its Git hook or as a local tool until a pre-commit hook is developed.

๐ŸŸจ JavaScript & Web Tools

Biome is a modern, fast formatter and linter for JS/TS ecosystems (JS[X], TS[X], JSON[C], CSS, GraphQL). It provides better defaults than ESLint and comes with a helpful VSCode Extension:

- repo: https://github.com/biomejs/pre-commit
  rev: "v0.6.1"
  hooks:
    - id: biome-ci
      name: "๐ŸŸจ javascript ยท Lint and format with Biome"
      additional_dependencies: ["@biomejs/biome@1.9.4"]
Alternatives to Biome (ESLint & Prettier)

ESLint and Prettier are more established alternatives with broader plugin ecosystems. While Prettier supports many file types, it can be notably slow, sometimes produces unexpected formatting, and sometimes breaks code (which I find annoying). Since this is primarily a Python-focused project template and Biome handles our JavaScript needs efficiently, we prefer it over the traditional ESLint/Prettier setup. Consider ESLint and Prettier if you need plugins, support for specific JS frameworks, or formatting for languages unsupported elsewhere. (More linters here as well)

โœ… Data & Config Validation

check-jsonschema validates various configuration files using JSON Schema. It supports JSON, YAML, and TOML files, and includes specialized validators like the TaskFile and GitHub Actions checker:

- repo: https://github.com/python-jsonschema/check-jsonschema
  rev: 0.30.0
  hooks:
    - id: check-github-workflows
      name: "๐Ÿ™ github-actions ยท Validate gh workflow files"
      args: ["--verbose"]
    - id: check-taskfile
      name: "โœ… taskfile ยท Validate Task configuration"

Additional json schema available on the Schema Store

validate-pyproject specifically handles pyproject.toml validation. In the future, I may have check-jsonschema do this as well.

- repo: https://github.com/abravalheri/validate-pyproject
  rev: v0.23
  hooks:
    - id: validate-pyproject
      name: "๐Ÿ python ยท Validate pyproject.toml"
      additional_dependencies: ["validate-pyproject-schema-store[all]"]

๐Ÿ“ Markdown

mdformat for Markdown formatting with additional plugins for GitHub-Flavored Markdown, Ruff-style code formatting, and frontmatter support:

- repo: https://github.com/hukkin/mdformat
  rev: 0.7.21
  hooks:
    - id: mdformat
      name: "๐Ÿ“ markdown ยท Format documentation"
      additional_dependencies:
        - mdformat-gfm           # GitHub-Flavored Markdown support
        - mdformat-ruff         # Python code formatting
        - mdformat-frontmatter  # YAML frontmatter support
        - ruff                  # Required for mdformat-ruff

๐Ÿ““ Notebooks

nbQA for Jupyter notebook quality assurance, allowing us to use our standard Python tools on notebooks:

- repo: https://github.com/nbQA-dev/nbQA
  rev: 1.9.1
  hooks:
    - id: nbqa-mypy
      name: "๐Ÿ““ notebook ยท Type-check cells"
    - id: nbqa
      entry: nbqa mdformat
      name: "๐Ÿ““ notebook ยท Format markdown cells"
      args: ["--nbqa-md"]
      types: [jupyter]
      additional_dependencies:  # Same dependencies as mdformat
        - mdformat
        - mdformat-gfm
        - mdformat-ruff
        - mdformat-frontmatter
        - ruff
ruff supports notebooks by default

Ruff has built-in support for Jupyter Notebooks, so this has been excluded from nbQA since it would be redundant. nbQA has nbqa-ruff-format and nbqa-ruff-check hooks, but these appear to be redundant.

โœจ Additional File Types

Prettier handles formatting for various file types not covered by other tools (HTML, CSS, YAML, etc.). While it can be slow and sometimes produces code-breaking formatting, it remains the standard for these file types:

- repo: https://github.com/pre-commit/mirrors-prettier
  rev: v4.0.0-alpha.8
  hooks:
    - id: prettier
      name: "โœจ misc-files ยท Format misc web files"
      types_or: [yaml, html, scss]
      additional_dependencies:
        - prettier@3.4.2
Future Improvements

I might replace Prettier with more focused tools in the future (Perhaps HTMLHint for HTML validation but it's hardly a linter.)

However, this would require managing multiple tools and dependencies, so I'm sticking with Prettier for now.

My disatisfaction with prettier is humorously shared by pre-commit, as they themselves no longer support the prettier hook because "prettier made some changes that breaks plugins entirely"

๐Ÿ› ๏ธ Local Tools

For using tools without hooks, you can also run a local command:

- repo: local
  hooks:
    - id: make-lint
      name: Run 'make lint'
      entry: make
      args: ["lint"]
      language: system

Note: If you're using uv, they also have pre-commits available.

03 ๐Ÿ“ Filesystem

These hooks help maintain repository hygiene by preventing common file-related issues:

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v5.0.0
  hooks:
    - id: check-executables-have-shebangs
      name: "๐Ÿ“ filesystem/โš™๏ธ exec ยท Verify shebang presence"
    - id: check-shebang-scripts-are-executable
      name: "๐Ÿ“ filesystem/โš™๏ธ exec ยท Verify script permissions"
    - id: check-case-conflict
      name: "๐Ÿ“ filesystem/๐Ÿ“ names ยท Check case sensitivity"
    - id: check-illegal-windows-names
      name: "๐Ÿ“ filesystem/๐Ÿ“ names ยท Validate Windows filenames"
    - id: check-symlinks
      name: "๐Ÿ“ filesystem/๐Ÿ”— symlink ยท Check symlink validity"
    - id: destroyed-symlinks
      name: "๐Ÿ“ filesystem/๐Ÿ”— symlink ยท Detect broken symlinks"
    # ... More Below ...
  • check-added-large-files - Prevents committing files larger than 8000KB (Git Large File Storage (LFS) or Data Version Control (DVC) should instead be used)
  • check-case-conflict - Prevents issues on case-insensitive filesystems (Windows/MacOS)
  • check-symlinks & destroyed-symlinks - Maintains symlink integrity
  • check-executables-have-shebangs - Ensures scripts are properly configured
  • check-illegal-windows-names - Check for files that cannot be created on Windows.

04 ๐ŸŒณ Git Quality

Branch Protection

- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v5.0.0
  hooks:
    # ... More Above ...
    - id: check-merge-conflict
      name: "๐ŸŒณ git ยท Detect conflict markers"
    - id: forbid-new-submodules
      name: "๐ŸŒณ git ยท Prevent submodule creation"
    - id: no-commit-to-branch
      name: "๐ŸŒณ git ยท Protect main branches"
      args: ["--branch", "main", "--branch", "master"]
    - id: check-added-large-files
      name: "๐ŸŒณ git ยท Block large file commits"
      args: ['--maxkb=1000']
  • forbid-new-submodules - Prevent addition of new git submodules. (I'm mixed on this one since I think this is a confusing paradigm but don't know of better alternatives.)
  • check-merge-conflict - Prevents committing unresolved merge conflicts
  • no-commit-to-branch - Protects main branches from direct commits (GitHub branch protections are for enterprise members only (sad))

For the best experience:

  1. Use cz commit instead of git commit
  2. Consider czg for a better implementation of the cz cli (I'm personally a fan of the AI generated commits it has.)

๐Ÿ—’๏ธ Commit Message Standards

Commitizen enforces standardized commit messages that enable automatic changelog generation and semantic versioning

Additionally, I add cz-conventional-gitmoji, a third-party prompt template that combines the gitmoji and conventional commit standards. (More templates here)

- repo: https://github.com/commitizen-tools/commitizen
  rev: v4.1.0
  hooks:
    - id: commitizen
      name: "๐ŸŒณ git ยท Validate commit message"
      stages: [commit-msg]
Alternatives to Commitizen (Commitlint)

commitlint is a similar project to commitizen. Many articles claim that the difference between the two are that commitizen is more of a tool to generate these fancy commits while commitlint is meant to lint the commits. However, considering cz check is a thing, I'm confused what the difference is. The tools can be used together. Seems like commitizen has better python support than commitlint. Projects equally popular. More research to be done on the differences!

05 ๐Ÿงช Testing

# TODO After completing `tests/`
# - repo: local
#   hooks:
#     - id: fast-tests
#       name: Run Fast Tests
#       entry: pytest
#       language: system
#       types: [python]
#       args: [
#         "tests/unit",  # Only run unit tests
#         "-m", "not slow",  # Skip slow-marked tests
#         "--quiet"
#       ]
#       pass_filenames: false

Conclusion

Putting all these together, we have the overall .pre-commit-config.yaml file:

exclude: |
  (?x)^(
      .*\{\{.*\}\}.*|    # Exclude any files with cookiecutter variables
      docs/site/.*|       # Exclude mkdocs compiled files
      \.history/.*|       # Exclude history files
      .*cache.*/.*|       # Exclude cache directories
      .*venv.*/.*|        # Exclude virtual environment directories
  )$
fail_fast: true
default_install_hook_types:
  - pre-commit
  - commit-msg
repos:
  # ---------------------------------------------------------------------------- #
  #                              ๐Ÿ”„ Pre-Commit Hooks                             #
  # ---------------------------------------------------------------------------- #

  # ----------------------------- ๐Ÿ”’ Security Tools ---------------------------- #

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.22.1
    hooks:
      - id: gitleaks
        name: "๐Ÿ”’ security ยท Detect hardcoded secrets"

  # --------------------------- ๐Ÿ” Code Quality Tools -------------------------- #

  ### Python Tools ###
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.6
    hooks:
      # - id: ruff
      #   args: [ --fix ]
      - id: ruff-format
        name: "๐Ÿ python ยท Format with Ruff"

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: "v1.14.1"
    hooks:
      - id: mypy
        name: "๐Ÿ python ยท Check types"

  - repo: https://github.com/abravalheri/validate-pyproject
    rev: v0.23
    hooks:
      - id: validate-pyproject
        name: "๐Ÿ python ยท Validate pyproject.toml"
        additional_dependencies: ["validate-pyproject-schema-store[all]"]

  ### Javascript & Web Tools ###
  - repo: https://github.com/biomejs/pre-commit
    rev: "v0.6.1"
    hooks:
      - id: biome-ci
        name: "๐ŸŸจ javascript ยท Lint and format with Biome"
        additional_dependencies: ["@biomejs/biome@1.9.4"]

  ### Data & Config Validation ###
  - repo: https://github.com/python-jsonschema/check-jsonschema
    rev: 0.30.0
    hooks:
      - id: check-github-workflows
        name: "๐Ÿ™ github-actions ยท Validate gh workflow files"
        args: ["--verbose"]
      - id: check-taskfile
        name: "โœ… taskfile ยท Validate Task configuration"

  ### Markdown ###
  - repo: https://github.com/hukkin/mdformat
    rev: 0.7.21
    hooks:
      - id: mdformat
        name: "๐Ÿ“ markdown ยท Format documentation"
        additional_dependencies:
          - mdformat-gfm
          - mdformat-ruff
          - mdformat-frontmatter
          - ruff

  ### Notebooks ###
  - repo: https://github.com/nbQA-dev/nbQA
    rev: 1.9.1
    hooks:
      - id: nbqa-mypy
        name: "๐Ÿ““ notebook ยท Type-check cells"
      - id: nbqa
        entry: nbqa mdformat
        name: "๐Ÿ““ notebook ยท Format markdown cells"
        args: ["--nbqa-md"]
        types: [jupyter]
        additional_dependencies:
          - mdformat
          - mdformat-gfm
          - mdformat-ruff
          - mdformat-frontmatter
          - ruff

  ### Additional File Types ###
  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v4.0.0-alpha.8
    hooks:
      - id: prettier
        name: "โœจ misc-files ยท Format misc web files"
        types_or: [yaml, html, scss]
        additional_dependencies:
          - prettier@3.4.2

  # ---------------------------- ๐Ÿ“ Filesystem Tools --------------------------- #

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      # Filesystem Checks
      - id: check-executables-have-shebangs
        name: "๐Ÿ“ filesystem/โš™๏ธ exec ยท Verify shebang presence"
      - id: check-shebang-scripts-are-executable
        name: "๐Ÿ“ filesystem/โš™๏ธ exec ยท Verify script permissions"
      - id: check-case-conflict
        name: "๐Ÿ“ filesystem/๐Ÿ“ names ยท Check case sensitivity"
      - id: check-illegal-windows-names
        name: "๐Ÿ“ filesystem/๐Ÿ“ names ยท Validate Windows filenames"
      - id: check-symlinks
        name: "๐Ÿ“ filesystem/๐Ÿ”— symlink ยท Check symlink validity"
      - id: destroyed-symlinks
        name: "๐Ÿ“ filesystem/๐Ÿ”— symlink ยท Detect broken symlinks"

      # ------------------------------- ๐ŸŒณ Git Tools ------------------------------- #
      - id: check-merge-conflict
        name: "๐ŸŒณ git ยท Detect conflict markers"
      - id: forbid-new-submodules
        name: "๐ŸŒณ git ยท Prevent submodule creation"
      - id: no-commit-to-branch
        name: "๐ŸŒณ git ยท Protect main branches"
        args: ["--branch", "main", "--branch", "master"]
      - id: check-added-large-files
        name: "๐ŸŒณ git ยท Block large file commits"
        args: ["--maxkb=1000"]

  # ------------------------------ ๐Ÿ› ๏ธ Local Tools ----------------------------- #

  # - repo: local
  #   hooks:
  #     - id: make-lint
  #       name: Run 'make lint'
  #       entry: make
  #       args: ["lint"]
  #       language: system

  # ---------------------------------------------------------------------------- #
  #                            ๐Ÿ“ Commit Message Hooks                           #
  # ---------------------------------------------------------------------------- #
  # --------------------------- โœ๏ธ Git Commit Quality -------------------------- #

  # - repo: https://github.com/ljnsn/cz-conventional-gitmoji
  #     rev: 0.2.4
  #     hooks:
  #       - id: conventional-gitmoji

  ### Commit Message Standards ###
  - repo: https://github.com/commitizen-tools/commitizen
    rev: v4.1.0
    hooks:
      - id: commitizen
        name: "๐ŸŒณ git ยท Validate commit message"
        stages: [commit-msg]

With each commit looking a bit like this:

Pre-commit Final Result

Inspiration

Some inspo from this article