Fix: pre-commit Not Working — Hooks Not Running, Install Failures, and CI Issues
Quick Answer
How to fix pre-commit errors — hooks not triggering on commit, pre-commit install failed, repo local hook not found, autoupdate not working, CI environment cache issues, and skip specific hooks.
The Error
You configure pre-commit but the hooks don’t run on commit:
$ git commit -m "changes"
[main abc1234] changes
# No hooks ran — code committed with lint errors still inOr installing pre-commit fails:
An error has occurred: FatalError: git failed. Is it installed, and are you in a Git repository directory?Or a repo’s local hook can’t be found:
- id: my-custom-check
An unexpected error has occurred: FatalError:
Expected hook id `my-custom-check` to be in `repo: local`.Or autoupdate silently fails in CI:
pre-commit autoupdate
# Output suggests success but versions don't changeOr a hook passes locally but fails in CI with confusing Python version errors:
InstallationError: Command errored out with exit status 1
ERROR: Package 'ruff' requires a different Python: 3.9.0 not in '>=3.10'pre-commit is the standard framework for managing Git hooks across Python projects — and increasingly across JS, Go, Rust, and more. It abstracts away hook installation, dependency management, and multi-language environments. But configuration has a specific grammar, and the interaction with Git hooks, CI caches, and language environments produces specific failure modes. This guide covers each.
Why This Happens
pre-commit installs itself as a Git hook by writing to .git/hooks/pre-commit. If you clone a repo fresh, the hooks aren’t installed automatically — you need pre-commit install per clone. If the .git/hooks/pre-commit file already exists (from another tool), pre-commit refuses to overwrite it without --force.
The .pre-commit-config.yaml config specifies repos (hook providers) and hooks to run. Each hook runs in its own isolated environment that pre-commit caches in ~/.cache/pre-commit/. Changes to that config invalidate parts of the cache.
Fix 1: Installing pre-commit and Hooks
pip install pre-commit
# Or with uv
uv tool install pre-commitInstall hooks into the current repo:
cd my-repo
pre-commit install
# pre-commit installed at .git/hooks/pre-commitCommon cause of “hooks not running”:
# Check: does the Git hook actually exist?
cat .git/hooks/pre-commit
# If the file is missing or has different content, install didn't happenpre-commit install must run once per clone. Add a README note:
## Setup
```bash
git clone ...
cd my-repo
pip install pre-commit
pre-commit install
**Install for multiple stages:**
```bash
pre-commit install --hook-type pre-commit --hook-type commit-msg --hook-type pre-pushEach stage runs different hooks from your config.
Common Mistake: Running pre-commit run --all-files once, seeing it work, and assuming hooks are installed. pre-commit run works without installing the Git hook — it’s the pre-commit install step that makes hooks fire on git commit. Confusion between “runs manually” and “fires on commit” is the single most common pre-commit issue.
Fix 2: Configuration Format
# .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
args: ["--maxkb=500"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies:
- pydantic
- types-requestsrev is a git ref — typically a version tag, but can be a branch or commit SHA:
rev: v4.5.0 # Tag (most common)
rev: main # Branch (risky — breaks reproducibility)
rev: abc1234 # Full commit SHA (reproducible but unclear)autoupdate pins to the latest tag:
pre-commit autoupdate
# Updates all rev: fields to latestRun this periodically to pick up bug fixes and new rules.
Skip autoupdate for specific repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
- id: ruff-format
# Exclude from autoupdate via CLIpre-commit autoupdate --exclude https://github.com/astral-sh/ruff-pre-commitFix 3: Local Hooks vs Remote Hooks
Remote hooks pull from a pre-commit repo (like the ones above). Most hooks are remote.
Local hooks run scripts from your own repo without a separate pre-commit hook repo:
repos:
- repo: local
hooks:
- id: custom-check
name: Run custom validation script
entry: python scripts/validate.py
language: system
files: ^src/.*\.py$
- id: pytest-quick
name: Run fast tests
entry: pytest -x -m "not slow"
language: system
pass_filenames: false
stages: [pre-push]
- id: check-secrets
name: Detect secrets in commits
entry: detect-secrets-hook
language: python
additional_dependencies:
- detect-secrets==1.4.0Language options for local hooks:
| Language | Behavior |
|---|---|
system | Run arbitrary shell command (must be in PATH) |
python | Create an isolated Python env with additional_dependencies |
node | Create Node env |
docker_image | Run in a Docker image |
fail | Always fail; use for “this rule always fails on matching files” |
Common fields:
- id: my-hook
name: Human-readable name
entry: command to run
language: system
files: ^src/.*\.py$ # Run only on matching files
exclude: ^tests/.* # Skip these
types: [python] # Run on Python files (language-aware)
types_or: [python, yaml] # Either type
pass_filenames: true # Pass matched files as args (default)
stages: [pre-commit] # Or pre-push, commit-msg, manual
require_serial: false # Run in parallel by defaultPro Tip: For quick custom validation, use language: system with a shell or Python command. Only reach for language: python with additional_dependencies if the hook needs packages that aren’t guaranteed on the developer’s machine. system keeps the hook fast and debuggable.
Fix 4: Running and Skipping Hooks
# Run all hooks on all files
pre-commit run --all-files
# Run a specific hook
pre-commit run ruff --all-files
# Run on staged files only (what happens at commit time)
pre-commit run
# Run on specific files
pre-commit run --files src/main.py src/utils.py
# Show hook IDs
pre-commit run --helpSkip hooks for a single commit (use sparingly):
SKIP=ruff,mypy git commit -m "WIP"
# Skip all hooks
git commit --no-verify -m "emergency fix"Don’t use --no-verify in team workflows — it bypasses quality gates. Prefer fixing the issue or configuring exclude for specific files that should skip checks.
Manual-only stages — hooks that only run when explicitly invoked:
- id: slow-integration-tests
stages: [manual]
entry: pytest tests/integration
language: system# Normal commit doesn't run these
git commit -m "changes"
# Explicit invocation
pre-commit run slow-integration-tests --hook-stage manualFix 5: additional_dependencies for Python Hooks
Hooks like mypy need to know your project’s dependencies to type-check correctly:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies:
- pydantic>=2.0
- fastapi
- types-requests
- sqlalchemy[mypy]Without these, mypy can’t resolve types from third-party libraries and reports false positives.
Pin versions in additional_dependencies to keep hooks reproducible:
additional_dependencies:
- pydantic==2.7.0
- types-requests==2.31.0.6Caveat: pre-commit maintains a separate virtual env per hook. Your project’s pyproject.toml dependencies don’t automatically flow into the hook. If your code uses pydantic==2.7.0 and the hook env has pydantic==1.x, type-checking produces wrong results.
For mypy-specific errors that come up in pre-commit hooks, see Python mypy type error.
For Ruff, the hook bundles its own linting rules — no additional_dependencies needed:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
- id: ruff-formatFor Ruff-specific configuration and rule selection, see Ruff not working.
Fix 6: CI Integration
GitHub Actions:
# .github/workflows/pre-commit.yml
name: pre-commit
on:
pull_request:
push:
branches: [main]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: pre-commit/[email protected]Cache pre-commit environments (makes CI 2-5x faster):
- uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}The key includes a hash of the config — cache invalidates when you bump hook versions.
Common CI failure — different Python version than local:
default_language_version:
python: python3.12 # Force a specific Python for all Python hooksWithout this, pre-commit uses the default python3 on the CI runner, which may differ from the developer’s local version.
pre-commit.ci — free hosted service that auto-updates config and fixes lint issues:
# .pre-commit-config.yaml
ci:
autofix_commit_msg: 'style: [pre-commit.ci] auto fixes'
autoupdate_schedule: weekly
skip: [mypy] # Hooks to skip in pre-commit.ci (e.g., those needing secrets)Sign up at pre-commit.ci and it integrates with GitHub PRs automatically.
Fix 7: Performance Tuning
Hooks run sequentially on matched files. Slow hooks compound — running mypy on 1000 files takes minutes.
Limit files per run:
- id: mypy
entry: mypy
language: system
types: [python]
files: ^src/ # Only src/, not tests/
exclude: ^src/migrations/ # Skip auto-generated
require_serial: true # mypy needs the whole module graphRun on changed files only (default behavior of git commit):
git commit # pre-commit only runs on staged files
pre-commit run # Same — only staged files
pre-commit run --all-files # Full scan (slow)Parallel vs serial:
- id: my-fast-check
require_serial: false # Default — runs in parallel across files
- id: my-slow-check-with-state
require_serial: true # Runs once with all filesCache cleanup (if the cache gets stale):
pre-commit clean # Remove cached environments
pre-commit install-hooks # Re-download freshCommon Mistake: Configuring hooks to scan **/*.py including node_modules/, .venv/, build/, or auto-generated files. Every hook run then takes minutes. Always set exclude: for dirs that shouldn’t be checked.
Fix 8: Writing Custom Hook Scripts
For project-specific checks, write a custom hook:
# scripts/check_no_debug_prints.py
import sys
import re
def main(files):
errors = []
pattern = re.compile(r'\b(print|breakpoint)\s*\(')
for filename in files:
with open(filename) as f:
for line_no, line in enumerate(f, 1):
if pattern.search(line) and not line.strip().startswith("#"):
errors.append(f"{filename}:{line_no}: {line.strip()}")
if errors:
print("Found debug statements:")
for err in errors:
print(err)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))# .pre-commit-config.yaml
- repo: local
hooks:
- id: no-debug-prints
name: Check for debug print statements
entry: python scripts/check_no_debug_prints.py
language: system
types: [python]
exclude: ^tests/Test your custom hook locally:
pre-commit run no-debug-prints --all-filesMake it a shared hook by publishing to a separate repo — other projects can then reference it:
- repo: https://github.com/yourorg/custom-pre-commit-hooks
rev: v1.0.0
hooks:
- id: no-debug-printsStill Not Working?
Pre-commit vs husky vs lefthook
- pre-commit — Python-focused, multi-language, widespread in Python community. Best for Python monorepos.
- husky — Node.js-based, uses npm scripts. Best for JS/TS projects where npm is already the workflow center.
- lefthook — Go-based, language-agnostic, fast. Good for multi-language monorepos.
Integration with Git Workflow
For Git hook concepts, commit workflow, and how .git/hooks/ integrates with pre-commit, see Git hooks not running.
Debugging Failed Hooks
# Show full output
pre-commit run --verbose
# Show the exact commands being executed
pre-commit run --verbose --all-files 2>&1 | tee pre-commit-debug.logView a specific hook’s environment:
# Find the cached env
ls ~/.cache/pre-commit/
# repo*-*-...
# Activate it and test manually
source ~/.cache/pre-commit/repo.../py_env-python3.12/bin/activate
mypy your_file.pyCommit Message Hooks
Validate commit messages with commit-msg stage hooks:
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: []Enforces Conventional Commits format (feat:, fix:, chore:, etc.). Install for this stage:
pre-commit install --hook-type commit-msgpre-push vs pre-commit Hooks
Fast hooks go in pre-commit (run on every commit). Slow hooks (full test suite, security scans) go in pre-push:
- id: pytest-full
stages: [pre-push]
entry: pytest
language: system
pass_filenames: falsepre-commit install --hook-type pre-pushThis keeps commits fast (<5 seconds) while still catching failures before they reach the remote.
Ruff + mypy in the Same Config
Run fast linters first (they fail quickly), slow ones last:
repos:
# Fast — runs first
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
# Slower — runs last
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: [pydantic, types-requests]pre-commit runs hooks in the order they appear in the config. Fast-fail order means you catch 90% of issues in the first few seconds.
For general pytest-related checks that developers often add to pre-commit, see pytest fixture not found.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Ruff Not Working — Configuration Errors, Rule Selection, and Format vs Lint Confusion
How to fix Ruff errors — pyproject.toml configuration not applied, rule code unknown, ruff format vs ruff check confusion, ignore not working, per-file-ignores, line-length conflicts, and migrating from Flake8 Black isort.
Fix: aiohttp Not Working — Session Leaks, ClientTimeout, and Connector Errors
How to fix aiohttp errors — RuntimeError session is closed, ClientConnectorError connection refused, SSL verify failure, Unclosed client session warning, server websocket disconnect, and connector pool exhausted.
Fix: Apache Airflow Not Working — DAG Not Found, Task Failures, and Scheduler Issues
How to fix Apache Airflow errors — DAG not appearing in UI, ImportError preventing DAG load, task stuck in running or queued, scheduler not scheduling, XCom too large, connection not found, and database migration errors.
Fix: BeautifulSoup Not Working — Parser Errors, Encoding Issues, and find_all Returns Empty
How to fix BeautifulSoup errors — bs4.FeatureNotFound install lxml, find_all returns empty list, Unicode decode error, JavaScript-rendered content not found, select vs find_all confusion, and slow parsing on large HTML.