
Pre Commit
- 47 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Helps with git & pull requests tasks.
About
pre-commit is a Claude Code skill for git & pull requests. It helps solo builders move faster with AI-assisted development.
- pre-commit
- Git & Pull Requests
- AI-coding skill
Pre Commit by the numbers
- 47 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #310 of 733 Git & Pull Requests skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill pre-commitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 47 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Helps with git & pull requests tasks.
Files
PreCommit
A comprehensive skill for managing pre-commit hooks - the framework for multi-language pre-commit hook management that automates code quality, formatting, linting, and security scanning.
Quick Reference
| Command | Description |
|---|---|
pre-commit install | Install git hooks |
pre-commit run --all-files | Run all hooks on all files |
pre-commit autoupdate | Update hooks to latest versions |
pre-commit run <hook-id> | Run specific hook |
Workflow Routing
| Workflow | Trigger | File |
|---|---|---|
| Setup | "setup pre-commit", "initialize hooks", "create config" | Workflows/Setup.md |
| AddHooks | "add hook", "add linting", "add formatter", "add security" | Workflows/AddHooks.md |
| Troubleshoot | "fix pre-commit", "hook failing", "debug hooks" | Workflows/Troubleshoot.md |
| CIIntegration | "CI pipeline", "GitHub Actions", "GitLab CI" | Workflows/CIIntegration.md |
| CustomHook | "create custom hook", "local hook", "write hook" | Workflows/CustomHook.md |
Documentation
| Document | Purpose |
|---|---|
QuickStartGuide.md | Installation and first-time setup |
HooksReference.md | Comprehensive hook catalog by language/purpose |
ConfigurationGuide.md | Advanced configuration options |
SecurityHooks.md | Secret detection and security scanning |
Tools
| Tool | Purpose |
|---|---|
Tools/PreCommitManager.ts | CLI for managing pre-commit configurations |
Tools/HookGenerator.ts | Generate .pre-commit-config.yaml templates |
Tools/HookValidator.ts | Validate hook configurations |
Examples
Example 1: Setup pre-commit for a new project
User: "Setup pre-commit for my Python project"
→ Invokes Setup workflow
→ Creates .pre-commit-config.yaml with Python hooks (black, isort, flake8)
→ Runs pre-commit installExample 2: Add Terraform hooks
User: "Add Terraform validation hooks"
→ Invokes AddHooks workflow
→ Adds terraform_fmt, terraform_validate, terraform_docs hooks
→ Configures tflint and checkov integrationExample 3: Add security scanning
User: "Add secret detection to pre-commit"
→ Invokes AddHooks workflow
→ Adds gitleaks, detect-secrets, trufflehog hooks
→ Configures appropriate exclusion patternsExample 4: Debug failing hook
User: "My eslint pre-commit hook is failing"
→ Invokes Troubleshoot workflow
→ Checks hook configuration and dependencies
→ Provides fix recommendationsSupported Hook Categories
- Python: black, isort, flake8, mypy, bandit, pyupgrade
- JavaScript/TypeScript: prettier, eslint, biome
- Infrastructure: terraform, terragrunt, helm, kustomize
- Kubernetes: kubeconform, kubeval, checkov
- Security: gitleaks, detect-secrets, trufflehog, trivy
- General: yamllint, jsonlint, shellcheck, markdownlint
---
Gotchas
- Hook output disappears when run via `git commit -m` in an editor terminal — stdout/stderr get swallowed by the editor wrapping. Run
pre-commit run --all-filesdirectly to see the actual failure message. - `pre-commit autoupdate` bumps to latest tag but doesn't update pinned config in hook repos — a hook that pins
flake8==6.0.0internally keeps that version. Look at each hook'sadditional_dependenciesafter autoupdate. - Hooks only run on staged files by default, so editing a file after
git addand then committing runs the hook against the OLD content. Use--all-filesin CI to catch this. - `exclude:` regex is anchored differently than `files:` —
exclude: tests/does NOT excludetests/foo.pyin some hook versions; useexclude: ^tests/to be safe. Anchor everything. - `language: system` hooks bypass the pre-commit venv and rely on the user's PATH — works on your machine, fails in CI with
command not found. Preferlanguage: pythonwithadditional_dependencies. - `pre-commit install` doesn't install for `commit-msg` or `pre-push` by default — separate
--hook-typecalls needed. Many teams add commit-msg hooks then wonder why they don't fire. - Skipping a hook via `SKIP=hookid git commit` is per-shell-invocation, not per-commit — CI runs with a fresh env where SKIP is unset and the hook fires anyway.
Pre-Commit Configuration Guide
Advanced configuration options for .pre-commit-config.yaml.
Configuration File Structure
# Top-level configuration
default_install_hook_types: [pre-commit, commit-msg]
default_language_version:
python: python3.11
node: "20.0.0"
default_stages: [pre-commit]
files: "" # Global file include pattern (regex)
exclude: "" # Global file exclude pattern (regex)
fail_fast: false # Stop after first failure
minimum_pre_commit_version: "3.0.0"
# Repository definitions
repos:
- repo: https://github.com/example/hooks
rev: v1.0.0
hooks:
- id: hook-name
# Hook-specific configurationRepository Configuration
Remote Repository
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 # Always use specific tag/SHA, never branch
hooks:
- id: trailing-whitespaceLocal Repository
- repo: local
hooks:
- id: custom-check
name: Custom Check
entry: ./scripts/check.sh
language: script
files: \.py$Meta Hooks
- repo: meta
hooks:
- id: check-hooks-apply
- id: check-useless-excludes
- id: identityHook Configuration Options
| Option | Type | Description |
|---|---|---|
id | string | Hook identifier (required) |
name | string | Display name override |
alias | string | Alternative hook reference |
entry | string | Command to run |
language | string | Runtime language |
language_version | string | Language version override |
files | regex | File pattern to include |
exclude | regex | File pattern to exclude |
types | list | File types (AND logic) |
types_or | list | File types (OR logic) |
exclude_types | list | File types to exclude |
args | list | Additional arguments |
stages | list | Git hook stages |
additional_dependencies | list | Extra packages |
always_run | bool | Run even without matches |
pass_filenames | bool | Pass filenames to hook |
require_serial | bool | Disable parallelization |
verbose | bool | Force output display |
log_file | string | Output log path |
File Filtering
Using Regex Patterns
hooks:
- id: check-yaml
files: ^config/.*\.ya?ml$
exclude: ^config/secrets/Using File Types
hooks:
- id: prettier
types_or:
- javascript
- jsx
- ts
- tsx
- json
- yaml
- markdownCommon File Types
text,binary,executable,directorypython,javascript,typescript,json,yaml,tomlmarkdown,html,css,scssshell,bash,zshgo,rust,java,c,cppdockerfile,terraform,hcl
Git Hook Stages
hooks:
- id: commitlint
stages: [commit-msg]
- id: gitleaks
stages: [pre-commit, pre-push]Available stages:
pre-commit(default)pre-merge-commitpre-pushcommit-msgpost-checkoutpost-commitpost-mergepost-rewriteprepare-commit-msgpre-rebase
Language Version Control
Global Default
default_language_version:
python: python3.11
node: "20.0.0"
ruby: 3.2.0
rust: 1.75.0Per-Hook Override
hooks:
- id: black
language_version: python3.11Passing Arguments
Static Arguments
hooks:
- id: flake8
args: [--max-line-length=88, --extend-ignore=E203]Environment Variables
hooks:
- id: terraform_validate
args:
- --env-vars=AWS_DEFAULT_REGION="us-west-2"Git Directory Placeholder
hooks:
- id: terraform_tflint
args:
- --args=--config=__GIT_WORKING_DIR__/.tflint.hclAdditional Dependencies
Python
hooks:
- id: mypy
additional_dependencies:
- types-requests
- types-PyYAML
- pydanticNode.js
hooks:
- id: eslint
additional_dependencies:
- eslint@9.14.0
- typescript
- "@typescript-eslint/parser"Conditional Execution
Always Run (Even Without Matches)
hooks:
- id: generate-docs
always_run: true
pass_filenames: falseExclude Patterns
hooks:
- id: trailing-whitespace
exclude: |
(?x)^(
.*\.snap$|
.*\.lock$|
vendor/.*
)$Branch Protection
hooks:
- id: no-commit-to-branch
args:
- --branch=main
- --branch=master
- --pattern=release/.*Performance Optimization
Parallelization Control
hooks:
- id: heavy-check
require_serial: true # Disable parallelizationFail Fast
fail_fast: true # Stop after first failureCache Management
# Clear cached environments
pre-commit clean
# Pre-download dependencies
pre-commit install --install-hooksCI/CD Configuration
Skip in CI
hooks:
- id: interactive-check
stages: [pre-commit] # Won't run in CI with --all-filesEnvironment Detection
- repo: local
hooks:
- id: ci-only-check
name: CI Only Check
entry: bash -c '[ -n "$CI" ] && ./check.sh || true'
language: systemDebugging
Verbose Output
hooks:
- id: complex-check
verbose: trueLog to File
hooks:
- id: long-running-check
log_file: /tmp/hook-output.logEnvironment Variables
# Debug mode
PRE_COMMIT_VERBOSE=1 pre-commit run
# Trace mode (for pre-commit-terraform)
PCT_LOG=trace pre-commit run terraform_validate
# Disable colors
PRE_COMMIT_COLOR=never pre-commit runMonorepo Support
Subdirectory Targeting
hooks:
- id: terraform_fmt
args:
- --hook-config=--tf-path=./infrastructureMultiple Configs
# Run with specific config
pre-commit run -c .pre-commit-config.python.yamlMigration from Other Tools
From husky (npm)
# Replace package.json husky config with:
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.14.0
hooks:
- id: eslintFrom git hooks directory
# Convert .git/hooks/pre-commit to:
- repo: local
hooks:
- id: legacy-hook
name: Legacy Pre-commit Hook
entry: ./scripts/legacy-hook.sh
language: scriptPre-Commit Hooks Reference
Comprehensive catalog of pre-commit hooks organized by language and purpose.
Official Pre-Commit Hooks
Repository: https://github.com/pre-commit/pre-commit-hooks
| Hook ID | Description |
|---|---|
trailing-whitespace | Trims trailing whitespace |
end-of-file-fixer | Ensures files end with newline |
check-yaml | Validates YAML syntax |
check-json | Validates JSON syntax |
check-toml | Validates TOML syntax |
check-xml | Validates XML syntax |
check-added-large-files | Prevents large files from being committed |
check-merge-conflict | Checks for merge conflict strings |
check-case-conflict | Checks for case conflicts in filenames |
check-symlinks | Checks for broken symlinks |
check-executables-have-shebangs | Ensures executables have shebangs |
check-shebang-scripts-are-executable | Ensures shebang scripts are executable |
detect-private-key | Detects private keys |
mixed-line-ending | Fixes mixed line endings |
no-commit-to-branch | Prevents commits to protected branches |
pretty-format-json | Formats JSON files |
requirements-txt-fixer | Sorts requirements.txt |
sort-simple-yaml | Sorts simple YAML files |
file-contents-sorter | Sorts file contents |
fix-byte-order-marker | Removes UTF-8 BOM |
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: [--maxkb=1000]
- id: check-merge-conflict
- id: detect-private-key
- id: no-commit-to-branch
args: [--branch, main, --branch, master]---
Python Hooks
Black (Formatter)
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
language_version: python3.11
args: [--line-length=88]isort (Import Sorter)
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: [--profile=black]Flake8 (Linter)
- repo: https://github.com/pycqa/flake8
rev: 7.1.1
hooks:
- id: flake8
args: [--max-line-length=88, --extend-ignore=E203]mypy (Type Checker)
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-PyYAML]Ruff (Fast Linter + Formatter)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.3
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatBandit (Security)
- repo: https://github.com/pycqa/bandit
rev: 1.7.10
hooks:
- id: bandit
args: [-c, pyproject.toml]
additional_dependencies: ["bandit[toml]"]pyupgrade (Modernizer)
- repo: https://github.com/asottile/pyupgrade
rev: v3.19.0
hooks:
- id: pyupgrade
args: [--py311-plus]autopep8 (Formatter)
- repo: https://github.com/hhatto/autopep8
rev: v2.3.1
hooks:
- id: autopep8---
JavaScript/TypeScript Hooks
Prettier (Formatter)
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, json, yaml, css, scss, markdown]ESLint (Linter)
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.14.0
hooks:
- id: eslint
files: \.[jt]sx?$
types: [file]
additional_dependencies:
- eslint@9.14.0
- typescript
- "@typescript-eslint/parser"
- "@typescript-eslint/eslint-plugin"Biome (Fast Linter + Formatter)
- repo: https://github.com/biomejs/pre-commit
rev: v0.5.0
hooks:
- id: biome-check
additional_dependencies: ["@biomejs/biome@1.9.4"]---
Terraform/Infrastructure Hooks
Repository: https://github.com/antonbabenko/pre-commit-terraform
Core Terraform Hooks
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.2
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docs
args:
- --hook-config=--path-to-file=README.md
- --hook-config=--create-file-if-not-exist=true
- id: terraform_tflint
args:
- --args=--config=__GIT_WORKING_DIR__/.tflint.hclSecurity Scanning
- id: terraform_trivy
args:
- --args=--severity=HIGH,CRITICAL
- id: terraform_checkov
args:
- --args=--quiet
- --args=--compactTerragrunt
- id: terragrunt_fmt
- id: terragrunt_validateCost Estimation
- id: infracost_breakdown
args:
- --args=--path=.
- --hook-config='.totalMonthlyCost|tonumber < 5000'---
Kubernetes/Helm Hooks
Helm Lint
- repo: https://github.com/gruntwork-io/pre-commit
rev: v0.1.23
hooks:
- id: helmlintKubeconform (Manifest Validation)
- repo: https://github.com/yannh/kubeconform
rev: v0.6.7
hooks:
- id: kubeconform
args: [-strict, -ignore-missing-schemas]Checkov (IaC Security)
- repo: https://github.com/bridgecrewio/checkov
rev: 3.2.277
hooks:
- id: checkov
args: [--framework, kubernetes]Kustomize
- repo: local
hooks:
- id: kustomize-build
name: kustomize build
entry: kustomize build
language: system
files: kustomization\.ya?ml$
pass_filenames: false---
YAML/JSON Hooks
yamllint
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
args: [-c, .yamllint.yaml]Sample `.yamllint.yaml`:
extends: default
rules:
line-length:
max: 120
truthy:
check-keys: false
document-start: disableyamlfmt (Formatter)
- repo: https://github.com/google/yamlfmt
rev: v0.14.0
hooks:
- id: yamlfmtcheck-jsonschema
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.29.4
hooks:
- id: check-github-workflows
- id: check-github-actions
- id: check-dependabot
- id: check-renovate---
Security Hooks
Gitleaks (Secret Detection)
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaksdetect-secrets
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: [--baseline, .secrets.baseline]TruffleHog
- repo: local
hooks:
- id: trufflehog
name: TruffleHog
entry: trufflehog git file://. --since-commit HEAD --results=verified,unknown --fail
language: system
stages: [pre-commit, pre-push]Trivy (Vulnerability Scanner)
- repo: https://github.com/aquasecurity/trivy
rev: v0.57.1
hooks:
- id: trivy-config
args: [--severity, HIGH,CRITICAL]---
Shell Script Hooks
shellcheck
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck
args: [-x]shfmt (Formatter)
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.10.0-1
hooks:
- id: shfmt
args: [-i, "2", -ci]bashate
- repo: https://github.com/openstack/bashate
rev: 2.1.1
hooks:
- id: bashate
args: [-i, E006]---
Go Hooks
golangci-lint
- repo: https://github.com/golangci/golangci-lint
rev: v1.62.0
hooks:
- id: golangci-lintgo-fmt
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: go-fmt
- id: go-vet
- id: go-imports---
Rust Hooks
rustfmt
- repo: https://github.com/doublify/pre-commit-rust
rev: v1.0
hooks:
- id: fmt
- id: cargo-check---
Docker Hooks
hadolint (Dockerfile Linter)
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolintdocker-compose-check
- repo: https://github.com/IamTheFij/docker-pre-commit
rev: v3.0.1
hooks:
- id: docker-compose-check---
Documentation Hooks
markdownlint
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.42.0
hooks:
- id: markdownlint
args: [--fix]codespell (Typos)
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
args: [-I, .codespell-ignore]typos (Fast Typo Checker)
- repo: https://github.com/crate-ci/typos
rev: v1.27.0
hooks:
- id: typos---
Git Commit Message Hooks
commitlint
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.18.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional"]conventional-pre-commit
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.6.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: [feat, fix, docs, style, refactor, perf, test, chore, ci, build]---
Complete Example Configuration
# .pre-commit-config.yaml
default_stages: [pre-commit]
fail_fast: false
repos:
# General hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args: [--allow-multiple-documents]
- id: check-json
- id: check-added-large-files
args: [--maxkb=1000]
- id: check-merge-conflict
- id: detect-private-key
- id: no-commit-to-branch
args: [--branch, main]
# YAML
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
args: [-c, .yamllint.yaml]
# Security
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
# Typos
- repo: https://github.com/crate-ci/typos
rev: v1.27.0
hooks:
- id: typos
# Commit messages
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.6.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]---
Version Update Schedule
Run pre-commit autoupdate monthly to keep hooks current:
# Update all hooks
pre-commit autoupdate
# Update specific repo
pre-commit autoupdate --repo https://github.com/psf/blackPre-Commit Quick Start Guide
Installation
Using pip (Recommended)
pip install pre-commitUsing Homebrew (macOS)
brew install pre-commitUsing pipx (Isolated)
pipx install pre-commitVerify Installation
pre-commit --version4-Step Setup
Step 1: Create Configuration
Create .pre-commit-config.yaml in your repository root:
# Minimal starter config
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflictOr generate a sample:
pre-commit sample-config > .pre-commit-config.yamlStep 2: Install Git Hooks
pre-commit installThis creates .git/hooks/pre-commit that runs automatically on git commit.
Step 3: Test on All Files (Optional but Recommended)
pre-commit run --all-filesStep 4: Commit Your Config
git add .pre-commit-config.yaml
git commit -m "chore: add pre-commit configuration"Essential Commands
| Command | Description |
|---|---|
pre-commit install | Install hooks to git |
pre-commit install --install-hooks | Install and download hook dependencies |
pre-commit run | Run on staged files |
pre-commit run --all-files | Run on all files |
pre-commit run <hook-id> | Run specific hook |
pre-commit autoupdate | Update hooks to latest versions |
pre-commit clean | Clear cached hook environments |
pre-commit uninstall | Remove git hooks |
Skipping Hooks
Skip all hooks (emergency only)
git commit --no-verify -m "message"Skip specific hooks
SKIP=flake8,black git commit -m "message"Common Starter Configurations
Python Project
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.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: 24.10.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/pycqa/flake8
rev: 7.1.1
hooks:
- id: flake8JavaScript/TypeScript Project
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-json
- id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, json, yaml, markdown]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.14.0
hooks:
- id: eslint
files: \.[jt]sx?$
additional_dependencies:
- eslint@9.14.0
- typescriptInfrastructure Project (Terraform/Kubernetes)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args: [--allow-multiple-documents]
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.2
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docs
- id: terraform_tflint
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
args: [-c, .yamllint.yaml]Best Practices
1. Pin versions: Always use specific rev tags, never branches 2. Run autoupdate regularly: Keep hooks current with pre-commit autoupdate 3. Test in CI: Run pre-commit run --all-files in your CI pipeline 4. Commit config first: Add .pre-commit-config.yaml before enabling hooks 5. Start minimal: Add hooks incrementally, fix issues as you go
Troubleshooting
Hooks not running
pre-commit install # Reinstall hooksClear cache if hooks are stale
pre-commit clean
pre-commit install --install-hooksCheck hook environments
ls ~/.cache/pre-commit/Debug mode
PRE_COMMIT_VERBOSE=1 pre-commit run --all-filesSecurity Hooks for Pre-Commit
Comprehensive guide to secret detection and security scanning hooks.
Secret Detection
Gitleaks
Industry-standard secret detection tool.
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaksCustom Configuration (`.gitleaks.toml`):
[extend]
useDefault = true
[[rules]]
id = "custom-api-key"
description = "Custom API Key Pattern"
regex = '''CUSTOM_API_[A-Z0-9]{32}'''
tags = ["key", "custom"]
[allowlist]
paths = [
'''\.gitleaks\.toml$''',
'''tests/fixtures/.*''',
]With Custom Config:
- id: gitleaks
args: [--config, .gitleaks.toml]detect-secrets (Yelp)
Baseline-based secret detection.
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: [--baseline, .secrets.baseline]Initialize Baseline:
detect-secrets scan > .secrets.baselineAudit Baseline:
detect-secrets audit .secrets.baselineTruffleHog
Deep secret scanning with verification.
- repo: local
hooks:
- id: trufflehog
name: TruffleHog Secret Scan
entry: trufflehog git file://. --since-commit HEAD --results=verified,unknown --fail
language: system
stages: [pre-commit, pre-push]Docker Alternative:
- repo: local
hooks:
- id: trufflehog-docker
name: TruffleHog (Docker)
entry: docker run --rm -v "$(pwd):/repo" trufflesecurity/trufflehog:latest git file:///repo --since-commit HEAD --fail
language: system---
Infrastructure Security
Checkov (IaC Security)
Static analysis for Terraform, Kubernetes, Docker, CloudFormation.
- repo: https://github.com/bridgecrewio/checkov
rev: 3.2.277
hooks:
- id: checkov
args: [--quiet, --compact]Framework-Specific:
hooks:
- id: checkov
name: Checkov Terraform
args: [--framework, terraform]
files: \.tf$
- id: checkov
name: Checkov Kubernetes
args: [--framework, kubernetes]
files: \.(yaml|yml)$With Custom Config:
- id: checkov
args: [--config-file, .checkov.yaml]Sample `.checkov.yaml`:
skip-check:
- CKV_AWS_18 # Skip S3 logging check
- CKV_K8S_21 # Skip default namespace check
soft-fail: false
framework:
- terraform
- kubernetesTrivy (Vulnerability Scanner)
Comprehensive vulnerability scanner.
- repo: https://github.com/aquasecurity/trivy
rev: v0.57.1
hooks:
- id: trivy-config
args: [--severity, HIGH,CRITICAL]Terraform-Specific (via pre-commit-terraform):
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.2
hooks:
- id: terraform_trivy
args:
- --args=--severity=HIGH,CRITICAL
- --args=--skip-dirs=.terraformTerrascan
Policy-as-code for Terraform.
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.2
hooks:
- id: terrascan
args:
- --args=--policy-type=aws
- --args=--severity=hightfsec (Deprecated - Use Trivy)
# DEPRECATED: Use terraform_trivy instead
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.2
hooks:
- id: terraform_tfsec # Deprecated---
Dependency Security
Bandit (Python Security)
- repo: https://github.com/pycqa/bandit
rev: 1.7.10
hooks:
- id: bandit
args: [-c, pyproject.toml, -r, src/]
additional_dependencies: ["bandit[toml]"]Sample `pyproject.toml`:
[tool.bandit]
exclude_dirs = ["tests", "venv"]
skips = ["B101"] # Skip assert checkSafety (Python Dependencies)
- repo: https://github.com/Lucas-C/pre-commit-hooks-safety
rev: v1.3.3
hooks:
- id: python-safety-dependencies-check
files: requirements.*\.txt$npm-audit (Node.js)
- repo: local
hooks:
- id: npm-audit
name: npm audit
entry: npm audit --audit-level=high
language: system
files: package-lock\.json$
pass_filenames: false---
Code Quality Security
Semgrep
Pattern-based code analysis.
- repo: https://github.com/returntocorp/semgrep
rev: v1.95.0
hooks:
- id: semgrep
args: [--config, auto, --error]With Specific Rules:
- id: semgrep
args:
- --config=p/security-audit
- --config=p/owasp-top-ten
- --errorCodeQL (via local hook)
- repo: local
hooks:
- id: codeql
name: CodeQL Analysis
entry: codeql database analyze --format=sarif-latest
language: system
pass_filenames: false---
Complete Security Configuration
# .pre-commit-config.yaml - Security Focus
repos:
# Secret Detection
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: [--baseline, .secrets.baseline]
# IaC Security
- repo: https://github.com/bridgecrewio/checkov
rev: 3.2.277
hooks:
- id: checkov
args: [--quiet, --compact]
# Python Security
- repo: https://github.com/pycqa/bandit
rev: 1.7.10
hooks:
- id: bandit
args: [-c, pyproject.toml]
additional_dependencies: ["bandit[toml]"]
# Terraform Security
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.2
hooks:
- id: terraform_trivy
args:
- --args=--severity=HIGH,CRITICAL
# Pattern Analysis
- repo: https://github.com/returntocorp/semgrep
rev: v1.95.0
hooks:
- id: semgrep
args: [--config, auto, --error]---
Handling False Positives
Inline Ignores
Gitleaks:
API_KEY = "test_key_12345" # gitleaks:allowdetect-secrets:
API_KEY = "test_key_12345" # pragma: allowlist secretCheckov:
resource "aws_s3_bucket" "test" {
#checkov:skip=CKV_AWS_18:Test bucket doesn't need logging
bucket = "test-bucket"
}Trivy:
# trivy:ignore:AVD-AWS-0086
resource:
type: aws_s3_bucketBaseline Files
# detect-secrets: Update baseline
detect-secrets scan --baseline .secrets.baseline
# Audit and mark false positives
detect-secrets audit .secrets.baselineExclusion Patterns
- id: gitleaks
exclude: |
(?x)^(
tests/fixtures/.*|
.*\.example$
)$---
CI/CD Integration
GitHub Actions
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install pre-commit
- run: pre-commit run gitleaks --all-files
- run: pre-commit run checkov --all-filesGitLab CI
security-scan:
stage: test
image: python:3.11
script:
- pip install pre-commit
- pre-commit run --all-files
rules:
- if: $CI_MERGE_REQUEST_ID#!/usr/bin/env bun
/**
* HookGenerator - Generate .pre-commit-config.yaml templates
*
* Usage:
* bun run HookGenerator.ts <preset> [options]
*
* Presets:
* minimal - Basic hooks (whitespace, yaml, merge conflicts)
* python - Python project hooks (black, isort, flake8, mypy)
* javascript - JS/TS project hooks (prettier, eslint)
* terraform - Infrastructure hooks (fmt, validate, docs, tflint)
* kubernetes - K8s/Helm hooks (yamllint, kubeconform, checkov)
* security - Security-focused hooks (gitleaks, bandit, trivy)
* full - Comprehensive setup with all categories
*/
import { writeFileSync, existsSync } from "fs";
import { stringify as stringifyYaml } from "yaml";
interface HookConfig {
id: string;
name?: string;
args?: string[];
files?: string;
exclude?: string;
types?: string[];
types_or?: string[];
stages?: string[];
additional_dependencies?: string[];
always_run?: boolean;
pass_filenames?: boolean;
}
interface RepoConfig {
repo: string;
rev: string;
hooks: HookConfig[];
}
interface PreCommitConfig {
default_stages?: string[];
fail_fast?: boolean;
repos: RepoConfig[];
}
// Hook templates by category
const hookTemplates: Record<string, RepoConfig[]> = {
base: [
{
repo: "https://github.com/pre-commit/pre-commit-hooks",
rev: "v5.0.0",
hooks: [
{ id: "trailing-whitespace" },
{ id: "end-of-file-fixer" },
{ id: "check-yaml", args: ["--allow-multiple-documents"] },
{ id: "check-json" },
{ id: "check-added-large-files", args: ["--maxkb=1000"] },
{ id: "check-merge-conflict" },
{ id: "detect-private-key" },
{ id: "no-commit-to-branch", args: ["--branch", "main", "--branch", "master"] },
],
},
],
python: [
{
repo: "https://github.com/astral-sh/ruff-pre-commit",
rev: "v0.7.3",
hooks: [
{ id: "ruff", args: ["--fix"] },
{ id: "ruff-format" },
],
},
{
repo: "https://github.com/pre-commit/mirrors-mypy",
rev: "v1.13.0",
hooks: [
{
id: "mypy",
additional_dependencies: ["types-requests", "types-PyYAML"],
},
],
},
{
repo: "https://github.com/pycqa/bandit",
rev: "1.7.10",
hooks: [
{
id: "bandit",
args: ["-c", "pyproject.toml"],
additional_dependencies: ["bandit[toml]"],
},
],
},
],
python_classic: [
{
repo: "https://github.com/psf/black",
rev: "24.10.0",
hooks: [{ id: "black" }],
},
{
repo: "https://github.com/pycqa/isort",
rev: "5.13.2",
hooks: [{ id: "isort", args: ["--profile=black"] }],
},
{
repo: "https://github.com/pycqa/flake8",
rev: "7.1.1",
hooks: [
{ id: "flake8", args: ["--max-line-length=88", "--extend-ignore=E203"] },
],
},
],
javascript: [
{
repo: "https://github.com/biomejs/pre-commit",
rev: "v0.5.0",
hooks: [
{
id: "biome-check",
additional_dependencies: ["@biomejs/biome@1.9.4"],
},
],
},
],
javascript_classic: [
{
repo: "https://github.com/pre-commit/mirrors-prettier",
rev: "v4.0.0-alpha.8",
hooks: [
{
id: "prettier",
types_or: ["javascript", "jsx", "ts", "tsx", "json", "yaml", "markdown"],
},
],
},
{
repo: "https://github.com/pre-commit/mirrors-eslint",
rev: "v9.14.0",
hooks: [
{
id: "eslint",
files: "\\.[jt]sx?$",
additional_dependencies: [
"eslint@9.14.0",
"typescript",
"@typescript-eslint/parser",
"@typescript-eslint/eslint-plugin",
],
},
],
},
],
terraform: [
{
repo: "https://github.com/antonbabenko/pre-commit-terraform",
rev: "v1.96.2",
hooks: [
{ id: "terraform_fmt" },
{ id: "terraform_validate" },
{
id: "terraform_docs",
args: [
"--hook-config=--path-to-file=README.md",
"--hook-config=--create-file-if-not-exist=true",
],
},
{
id: "terraform_tflint",
args: ["--args=--config=__GIT_WORKING_DIR__/.tflint.hcl"],
},
{
id: "terraform_trivy",
args: ["--args=--severity=HIGH,CRITICAL"],
},
],
},
],
kubernetes: [
{
repo: "https://github.com/adrienverge/yamllint",
rev: "v1.35.1",
hooks: [{ id: "yamllint", args: ["-c", ".yamllint.yaml"] }],
},
{
repo: "https://github.com/gruntwork-io/pre-commit",
rev: "v0.1.23",
hooks: [{ id: "helmlint" }],
},
{
repo: "https://github.com/bridgecrewio/checkov",
rev: "3.2.277",
hooks: [
{
id: "checkov",
args: ["--framework", "kubernetes", "--quiet", "--compact"],
},
],
},
],
security: [
{
repo: "https://github.com/gitleaks/gitleaks",
rev: "v8.21.2",
hooks: [{ id: "gitleaks" }],
},
{
repo: "https://github.com/Yelp/detect-secrets",
rev: "v1.5.0",
hooks: [
{ id: "detect-secrets", args: ["--baseline", ".secrets.baseline"] },
],
},
],
yaml: [
{
repo: "https://github.com/adrienverge/yamllint",
rev: "v1.35.1",
hooks: [{ id: "yamllint", args: ["-c", ".yamllint.yaml"] }],
},
],
shell: [
{
repo: "https://github.com/shellcheck-py/shellcheck-py",
rev: "v0.10.0.1",
hooks: [{ id: "shellcheck", args: ["-x"] }],
},
{
repo: "https://github.com/scop/pre-commit-shfmt",
rev: "v3.10.0-1",
hooks: [{ id: "shfmt", args: ["-i", "2", "-ci"] }],
},
],
go: [
{
repo: "https://github.com/golangci/golangci-lint",
rev: "v1.62.0",
hooks: [{ id: "golangci-lint" }],
},
{
repo: "https://github.com/dnephin/pre-commit-golang",
rev: "v0.5.1",
hooks: [{ id: "go-fmt" }, { id: "go-vet" }, { id: "go-imports" }],
},
],
docker: [
{
repo: "https://github.com/hadolint/hadolint",
rev: "v2.12.0",
hooks: [{ id: "hadolint" }],
},
],
docs: [
{
repo: "https://github.com/igorshubovych/markdownlint-cli",
rev: "v0.42.0",
hooks: [{ id: "markdownlint", args: ["--fix"] }],
},
{
repo: "https://github.com/crate-ci/typos",
rev: "v1.27.0",
hooks: [{ id: "typos" }],
},
],
commits: [
{
repo: "https://github.com/compilerla/conventional-pre-commit",
rev: "v3.6.0",
hooks: [
{
id: "conventional-pre-commit",
stages: ["commit-msg"],
args: ["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "ci", "build"],
},
],
},
],
};
// Preset definitions
const presets: Record<string, string[]> = {
minimal: ["base"],
python: ["base", "python", "security"],
python_classic: ["base", "python_classic", "security"],
javascript: ["base", "javascript", "security"],
javascript_classic: ["base", "javascript_classic", "security"],
terraform: ["base", "terraform"],
kubernetes: ["base", "kubernetes", "security"],
infrastructure: ["base", "terraform", "kubernetes", "security"],
security: ["base", "security"],
go: ["base", "go", "security"],
full: ["base", "yaml", "shell", "docker", "docs", "security", "commits"],
};
function generateConfig(categories: string[]): PreCommitConfig {
const repos: RepoConfig[] = [];
const seenRepos = new Set<string>();
for (const category of categories) {
const templates = hookTemplates[category];
if (!templates) {
console.warn(`⚠️ Unknown category: ${category}`);
continue;
}
for (const template of templates) {
// Avoid duplicate repos
if (seenRepos.has(template.repo)) {
// Merge hooks into existing repo
const existingRepo = repos.find((r) => r.repo === template.repo);
if (existingRepo) {
for (const hook of template.hooks) {
if (!existingRepo.hooks.some((h) => h.id === hook.id)) {
existingRepo.hooks.push(hook);
}
}
}
} else {
repos.push({ ...template, hooks: [...template.hooks] });
seenRepos.add(template.repo);
}
}
}
return {
default_stages: ["pre-commit"],
fail_fast: false,
repos,
};
}
function generateYaml(config: PreCommitConfig): string {
// Custom YAML generation for cleaner output
let yaml = "# .pre-commit-config.yaml\n";
yaml += "# Generated by HookGenerator\n\n";
if (config.default_stages) {
yaml += `default_stages: [${config.default_stages.join(", ")}]\n`;
}
if (config.fail_fast !== undefined) {
yaml += `fail_fast: ${config.fail_fast}\n`;
}
yaml += "\nrepos:\n";
for (const repo of config.repos) {
yaml += ` - repo: ${repo.repo}\n`;
yaml += ` rev: ${repo.rev}\n`;
yaml += ` hooks:\n`;
for (const hook of repo.hooks) {
yaml += ` - id: ${hook.id}\n`;
if (hook.name) {
yaml += ` name: ${hook.name}\n`;
}
if (hook.args && hook.args.length > 0) {
if (hook.args.length === 1) {
yaml += ` args: [${hook.args[0]}]\n`;
} else {
yaml += ` args:\n`;
for (const arg of hook.args) {
yaml += ` - ${arg}\n`;
}
}
}
if (hook.files) {
yaml += ` files: ${hook.files}\n`;
}
if (hook.exclude) {
yaml += ` exclude: ${hook.exclude}\n`;
}
if (hook.types && hook.types.length > 0) {
yaml += ` types: [${hook.types.join(", ")}]\n`;
}
if (hook.types_or && hook.types_or.length > 0) {
yaml += ` types_or: [${hook.types_or.join(", ")}]\n`;
}
if (hook.stages && hook.stages.length > 0) {
yaml += ` stages: [${hook.stages.join(", ")}]\n`;
}
if (hook.additional_dependencies && hook.additional_dependencies.length > 0) {
yaml += ` additional_dependencies:\n`;
for (const dep of hook.additional_dependencies) {
yaml += ` - ${dep}\n`;
}
}
if (hook.always_run) {
yaml += ` always_run: true\n`;
}
if (hook.pass_filenames === false) {
yaml += ` pass_filenames: false\n`;
}
}
yaml += "\n";
}
return yaml.trim() + "\n";
}
function showHelp(): void {
console.log(`
HookGenerator - Generate .pre-commit-config.yaml templates
Usage:
bun run HookGenerator.ts <preset> [options]
Presets:
minimal Basic hooks (whitespace, yaml, merge conflicts)
python Python project (ruff, mypy, bandit)
python_classic Python project (black, isort, flake8)
javascript JS/TS project (biome)
javascript_classic JS/TS project (prettier, eslint)
terraform Terraform/IaC hooks
kubernetes K8s/Helm hooks
infrastructure Terraform + Kubernetes
security Security-focused hooks
go Go project hooks
full Comprehensive setup
Options:
--output, -o <file> Output file (default: .pre-commit-config.yaml)
--force, -f Overwrite existing file
--dry-run Print config without writing
--add <categories> Add specific categories (comma-separated)
--list List available categories
Categories:
base, python, python_classic, javascript, javascript_classic,
terraform, kubernetes, security, yaml, shell, go, docker, docs, commits
Examples:
bun run HookGenerator.ts python
bun run HookGenerator.ts minimal --add security,commits
bun run HookGenerator.ts terraform --dry-run
bun run HookGenerator.ts --list
`);
}
function listCategories(): void {
console.log("Available Categories:\n");
for (const [category, repos] of Object.entries(hookTemplates)) {
const hookCount = repos.reduce((acc, r) => acc + r.hooks.length, 0);
console.log(` ${category.padEnd(20)} ${hookCount} hooks`);
for (const repo of repos) {
const repoName = repo.repo.split("/").slice(-1)[0];
console.log(` └─ ${repoName}: ${repo.hooks.map((h) => h.id).join(", ")}`);
}
}
console.log("\nPresets:\n");
for (const [preset, categories] of Object.entries(presets)) {
console.log(` ${preset.padEnd(20)} ${categories.join(", ")}`);
}
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
showHelp();
process.exit(0);
}
if (args.includes("--list")) {
listCategories();
process.exit(0);
}
// Parse options
const preset = args[0];
const outputIndex = args.findIndex((a) => a === "--output" || a === "-o");
const outputFile =
outputIndex > -1 ? args[outputIndex + 1] : ".pre-commit-config.yaml";
const force = args.includes("--force") || args.includes("-f");
const dryRun = args.includes("--dry-run");
const addIndex = args.findIndex((a) => a === "--add");
const additionalCategories = addIndex > -1 ? args[addIndex + 1].split(",") : [];
// Determine categories
let categories: string[];
if (presets[preset]) {
categories = [...presets[preset], ...additionalCategories];
} else if (hookTemplates[preset]) {
categories = ["base", preset, ...additionalCategories];
} else {
console.error(`❌ Unknown preset or category: ${preset}`);
console.log("💡 Use --list to see available options");
process.exit(1);
}
// Generate config
console.log(`🔧 Generating config with: ${categories.join(", ")}\n`);
const config = generateConfig(categories);
const yaml = generateYaml(config);
if (dryRun) {
console.log(yaml);
return;
}
// Check existing file
if (existsSync(outputFile) && !force) {
console.error(`❌ ${outputFile} already exists`);
console.log("💡 Use --force to overwrite");
process.exit(1);
}
// Write file
writeFileSync(outputFile, yaml);
console.log(`✅ Generated ${outputFile}`);
console.log(` Repositories: ${config.repos.length}`);
const hookCount = config.repos.reduce((acc, r) => acc + r.hooks.length, 0);
console.log(` Total Hooks: ${hookCount}`);
console.log("\n📋 Next steps:");
console.log(" 1. Review the generated configuration");
console.log(" 2. Run: pre-commit install");
console.log(" 3. Test: pre-commit run --all-files");
}
main().catch(console.error);
#!/usr/bin/env bun
/**
* HookValidator - Validate pre-commit hook configurations
*
* Usage:
* bun run HookValidator.ts [options]
*
* Validates:
* - YAML syntax
* - Required fields
* - Version formats
* - Hook availability
* - Configuration consistency
*/
import { existsSync, readFileSync } from "fs";
import { parse as parseYaml } from "yaml";
import { $ } from "bun";
interface Hook {
id: string;
name?: string;
alias?: string;
entry?: string;
language?: string;
language_version?: string;
files?: string;
exclude?: string;
types?: string[];
types_or?: string[];
exclude_types?: string[];
args?: string[];
stages?: string[];
additional_dependencies?: string[];
always_run?: boolean;
pass_filenames?: boolean;
require_serial?: boolean;
verbose?: boolean;
log_file?: string;
}
interface Repo {
repo: string;
rev: string;
hooks: Hook[];
}
interface PreCommitConfig {
repos: Repo[];
default_install_hook_types?: string[];
default_language_version?: Record<string, string>;
default_stages?: string[];
files?: string;
exclude?: string;
fail_fast?: boolean;
minimum_pre_commit_version?: string;
}
interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
info: string[];
}
const CONFIG_FILE = ".pre-commit-config.yaml";
const VALID_STAGES = [
"pre-commit",
"pre-merge-commit",
"pre-push",
"commit-msg",
"post-checkout",
"post-commit",
"post-merge",
"post-rewrite",
"prepare-commit-msg",
"pre-rebase",
];
const VALID_LANGUAGES = [
"python",
"node",
"ruby",
"rust",
"golang",
"swift",
"docker",
"docker_image",
"dotnet",
"lua",
"perl",
"r",
"script",
"system",
"fail",
"pygrep",
];
const COMMON_FILE_TYPES = [
"text",
"binary",
"executable",
"python",
"javascript",
"jsx",
"ts",
"tsx",
"json",
"yaml",
"toml",
"xml",
"html",
"css",
"scss",
"markdown",
"shell",
"bash",
"zsh",
"go",
"rust",
"java",
"c",
"cpp",
"dockerfile",
"terraform",
"hcl",
];
function loadConfig(filePath: string): PreCommitConfig | null {
if (!existsSync(filePath)) {
console.error(`❌ ${filePath} not found`);
return null;
}
try {
const content = readFileSync(filePath, "utf-8");
return parseYaml(content) as PreCommitConfig;
} catch (error) {
console.error(`❌ Failed to parse ${filePath}:`, error);
return null;
}
}
function validateRegex(pattern: string, fieldName: string): string | null {
try {
new RegExp(pattern);
return null;
} catch {
return `Invalid regex in ${fieldName}: ${pattern}`;
}
}
function validateVersion(rev: string, repoUrl: string): string | null {
if (repoUrl === "local" || repoUrl === "meta") {
return null;
}
// Valid formats: v1.0.0, v1.0, 1.0.0, commit SHA (7-40 hex chars)
const versionPattern = /^v?\d+\.\d+(\.\d+)?(-[\w.]+)?$/;
const shaPattern = /^[a-f0-9]{7,40}$/;
if (versionPattern.test(rev) || shaPattern.test(rev)) {
return null;
}
return `Suspicious 'rev' format '${rev}' - use version tags (v1.0.0) or commit SHAs`;
}
function validateHook(hook: Hook, repoUrl: string, repoIndex: number, hookIndex: number): ValidationResult {
const result: ValidationResult = {
valid: true,
errors: [],
warnings: [],
info: [],
};
const prefix = `Repo ${repoIndex + 1}, Hook ${hookIndex + 1}`;
// Required: id
if (!hook.id) {
result.errors.push(`${prefix}: Missing required 'id' field`);
result.valid = false;
return result;
}
const hookPrefix = `${prefix} (${hook.id})`;
// Local hooks require additional fields
if (repoUrl === "local") {
if (!hook.name) {
result.warnings.push(`${hookPrefix}: Local hooks should have a 'name'`);
}
if (!hook.entry) {
result.errors.push(`${hookPrefix}: Local hooks require 'entry'`);
result.valid = false;
}
if (!hook.language) {
result.errors.push(`${hookPrefix}: Local hooks require 'language'`);
result.valid = false;
}
}
// Validate language
if (hook.language && !VALID_LANGUAGES.includes(hook.language.toLowerCase())) {
result.warnings.push(
`${hookPrefix}: Unknown language '${hook.language}' - valid: ${VALID_LANGUAGES.join(", ")}`
);
}
// Validate stages
if (hook.stages) {
for (const stage of hook.stages) {
if (!VALID_STAGES.includes(stage)) {
result.errors.push(
`${hookPrefix}: Invalid stage '${stage}' - valid: ${VALID_STAGES.join(", ")}`
);
result.valid = false;
}
}
}
// Validate file types
const allTypes = [...(hook.types || []), ...(hook.types_or || []), ...(hook.exclude_types || [])];
for (const type of allTypes) {
if (!COMMON_FILE_TYPES.includes(type)) {
result.info.push(`${hookPrefix}: Uncommon file type '${type}'`);
}
}
// Validate regex patterns
if (hook.files) {
const regexError = validateRegex(hook.files, "files");
if (regexError) {
result.errors.push(`${hookPrefix}: ${regexError}`);
result.valid = false;
}
}
if (hook.exclude) {
const regexError = validateRegex(hook.exclude, "exclude");
if (regexError) {
result.errors.push(`${hookPrefix}: ${regexError}`);
result.valid = false;
}
}
// Warn about conflicting options
if (hook.types && hook.types_or) {
result.warnings.push(
`${hookPrefix}: Using both 'types' (AND) and 'types_or' (OR) - this may cause confusion`
);
}
if (hook.always_run && hook.files) {
result.warnings.push(
`${hookPrefix}: 'always_run: true' with 'files' pattern may be redundant`
);
}
return result;
}
function validateRepo(repo: Repo, index: number): ValidationResult {
const result: ValidationResult = {
valid: true,
errors: [],
warnings: [],
info: [],
};
const prefix = `Repo ${index + 1}`;
// Validate repo URL
if (!repo.repo) {
result.errors.push(`${prefix}: Missing 'repo' field`);
result.valid = false;
return result;
}
const repoPrefix = `${prefix} (${repo.repo})`;
// Validate rev
if (repo.repo !== "local" && repo.repo !== "meta") {
if (!repo.rev) {
result.errors.push(`${repoPrefix}: Missing 'rev' field`);
result.valid = false;
} else {
const versionWarning = validateVersion(repo.rev, repo.repo);
if (versionWarning) {
result.warnings.push(`${repoPrefix}: ${versionWarning}`);
}
}
}
// Validate hooks
if (!repo.hooks || repo.hooks.length === 0) {
result.errors.push(`${repoPrefix}: No hooks defined`);
result.valid = false;
} else {
for (let i = 0; i < repo.hooks.length; i++) {
const hookResult = validateHook(repo.hooks[i], repo.repo, index, i);
result.errors.push(...hookResult.errors);
result.warnings.push(...hookResult.warnings);
result.info.push(...hookResult.info);
if (!hookResult.valid) {
result.valid = false;
}
}
// Check for duplicate hook IDs
const hookIds = repo.hooks.map((h) => h.id);
const duplicates = hookIds.filter((id, idx) => hookIds.indexOf(id) !== idx);
if (duplicates.length > 0) {
result.warnings.push(
`${repoPrefix}: Duplicate hook IDs: ${[...new Set(duplicates)].join(", ")}`
);
}
}
return result;
}
function validateConfig(config: PreCommitConfig): ValidationResult {
const result: ValidationResult = {
valid: true,
errors: [],
warnings: [],
info: [],
};
// Validate top-level structure
if (!config.repos || !Array.isArray(config.repos)) {
result.errors.push("Missing or invalid 'repos' field");
result.valid = false;
return result;
}
if (config.repos.length === 0) {
result.errors.push("No repositories defined");
result.valid = false;
return result;
}
// Validate default_stages
if (config.default_stages) {
for (const stage of config.default_stages) {
if (!VALID_STAGES.includes(stage)) {
result.errors.push(`Invalid default_stage '${stage}'`);
result.valid = false;
}
}
}
// Validate global patterns
if (config.files) {
const regexError = validateRegex(config.files, "files");
if (regexError) {
result.errors.push(regexError);
result.valid = false;
}
}
if (config.exclude) {
const regexError = validateRegex(config.exclude, "exclude");
if (regexError) {
result.errors.push(regexError);
result.valid = false;
}
}
// Validate each repository
for (let i = 0; i < config.repos.length; i++) {
const repoResult = validateRepo(config.repos[i], i);
result.errors.push(...repoResult.errors);
result.warnings.push(...repoResult.warnings);
result.info.push(...repoResult.info);
if (!repoResult.valid) {
result.valid = false;
}
}
// Check for duplicate repositories
const repoUrls = config.repos.map((r) => r.repo);
const duplicateRepos = repoUrls.filter((url, idx) => repoUrls.indexOf(url) !== idx);
if (duplicateRepos.length > 0) {
result.warnings.push(
`Duplicate repositories: ${[...new Set(duplicateRepos)].join(", ")}`
);
}
return result;
}
async function checkHookAvailability(config: PreCommitConfig): Promise<string[]> {
const warnings: string[] = [];
// Try to run pre-commit to check hook availability
try {
await $`pre-commit --version`.quiet();
} catch {
return ["pre-commit not installed - skipping hook availability check"];
}
for (const repo of config.repos) {
if (repo.repo === "local" || repo.repo === "meta") {
continue;
}
for (const hook of repo.hooks) {
try {
// Use try-repo to check if hook exists (dry-run)
await $`pre-commit try-repo ${repo.repo} --rev ${repo.rev} ${hook.id} --verbose 2>&1`.quiet();
} catch {
// Hook might not exist or repo might not be accessible
warnings.push(`Hook '${hook.id}' from '${repo.repo}' may not be available`);
}
}
}
return warnings;
}
function printResult(result: ValidationResult, verbose: boolean): void {
const totalIssues = result.errors.length + result.warnings.length;
if (result.errors.length > 0) {
console.log("\n❌ Errors:");
result.errors.forEach((e) => console.log(` ${e}`));
}
if (result.warnings.length > 0) {
console.log("\n⚠️ Warnings:");
result.warnings.forEach((w) => console.log(` ${w}`));
}
if (verbose && result.info.length > 0) {
console.log("\nℹ️ Info:");
result.info.forEach((i) => console.log(` ${i}`));
}
console.log("\n" + "─".repeat(50));
if (result.valid && result.warnings.length === 0) {
console.log("✅ Configuration is valid!");
} else if (result.valid) {
console.log(`✅ Configuration is valid with ${result.warnings.length} warning(s)`);
} else {
console.log(`❌ Configuration has ${result.errors.length} error(s)`);
}
}
function showHelp(): void {
console.log(`
HookValidator - Validate pre-commit hook configurations
Usage:
bun run HookValidator.ts [options]
Options:
--file, -f <path> Config file path (default: .pre-commit-config.yaml)
--verbose, -v Show info messages
--check-availability Check if hooks are available (slow)
--json Output as JSON
--help, -h Show this help
Validates:
- YAML syntax
- Required fields (repo, rev, id)
- Version/rev format
- Stage names
- Language types
- Regex patterns
- Duplicate detection
Examples:
bun run HookValidator.ts
bun run HookValidator.ts --verbose
bun run HookValidator.ts --file custom-config.yaml
bun run HookValidator.ts --check-availability
`);
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h")) {
showHelp();
process.exit(0);
}
// Parse options
const fileIndex = args.findIndex((a) => a === "--file" || a === "-f");
const configFile = fileIndex > -1 ? args[fileIndex + 1] : CONFIG_FILE;
const verbose = args.includes("--verbose") || args.includes("-v");
const checkAvailability = args.includes("--check-availability");
const jsonOutput = args.includes("--json");
console.log(`🔍 Validating ${configFile}...\n`);
// Load configuration
const config = loadConfig(configFile);
if (!config) {
process.exit(1);
}
// Validate configuration
const result = validateConfig(config);
// Check hook availability if requested
if (checkAvailability) {
console.log("Checking hook availability (this may take a while)...");
const availabilityWarnings = await checkHookAvailability(config);
result.warnings.push(...availabilityWarnings);
}
// Output results
if (jsonOutput) {
console.log(JSON.stringify(result, null, 2));
} else {
// Summary
console.log("📊 Summary:");
console.log(` Repositories: ${config.repos.length}`);
const hookCount = config.repos.reduce((acc, r) => acc + r.hooks.length, 0);
console.log(` Total Hooks: ${hookCount}`);
printResult(result, verbose);
}
process.exit(result.valid ? 0 : 1);
}
main().catch(console.error);
{
"name": "precommit-tools",
"version": "1.0.0",
"description": "TypeScript tools for managing pre-commit configurations",
"type": "module",
"scripts": {
"manager": "bun run PreCommitManager.ts",
"generator": "bun run HookGenerator.ts",
"validator": "bun run HookValidator.ts",
"status": "bun run PreCommitManager.ts status",
"install": "bun run PreCommitManager.ts install",
"run": "bun run PreCommitManager.ts run --all-files",
"update": "bun run PreCommitManager.ts update",
"validate": "bun run HookValidator.ts",
"generate:python": "bun run HookGenerator.ts python",
"generate:javascript": "bun run HookGenerator.ts javascript",
"generate:terraform": "bun run HookGenerator.ts terraform",
"generate:minimal": "bun run HookGenerator.ts minimal"
},
"dependencies": {
"yaml": "^2.6.0"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.6.0"
},
"engines": {
"bun": ">=1.0.0"
}
}
#!/usr/bin/env bun
/**
* PreCommitManager - CLI tool for managing pre-commit configurations
*
* Usage:
* bun run PreCommitManager.ts <command> [options]
*
* Commands:
* status - Show pre-commit installation status
* install - Install pre-commit hooks
* run - Run hooks on files
* update - Update hooks to latest versions
* clean - Clean cached environments
* validate - Validate configuration file
*/
import { $ } from "bun";
import { existsSync, readFileSync } from "fs";
import { parse as parseYaml } from "yaml";
interface Hook {
id: string;
name?: string;
args?: string[];
files?: string;
exclude?: string;
stages?: string[];
additional_dependencies?: string[];
}
interface Repo {
repo: string;
rev: string;
hooks: Hook[];
}
interface PreCommitConfig {
repos: Repo[];
default_stages?: string[];
fail_fast?: boolean;
files?: string;
exclude?: string;
}
const CONFIG_FILE = ".pre-commit-config.yaml";
async function checkPreCommitInstalled(): Promise<boolean> {
try {
await $`pre-commit --version`.quiet();
return true;
} catch {
return false;
}
}
async function getPreCommitVersion(): Promise<string> {
try {
const result = await $`pre-commit --version`.text();
return result.trim();
} catch {
return "Not installed";
}
}
function loadConfig(): PreCommitConfig | null {
if (!existsSync(CONFIG_FILE)) {
console.error(`❌ ${CONFIG_FILE} not found`);
return null;
}
try {
const content = readFileSync(CONFIG_FILE, "utf-8");
return parseYaml(content) as PreCommitConfig;
} catch (error) {
console.error(`❌ Failed to parse ${CONFIG_FILE}:`, error);
return null;
}
}
async function status(): Promise<void> {
console.log("🔍 Pre-commit Status\n");
// Check installation
const installed = await checkPreCommitInstalled();
const version = await getPreCommitVersion();
console.log(`📦 Pre-commit: ${installed ? "✅ Installed" : "❌ Not installed"}`);
console.log(` Version: ${version}`);
// Check config file
const configExists = existsSync(CONFIG_FILE);
console.log(`\n📄 Config: ${configExists ? "✅ Found" : "❌ Not found"}`);
if (configExists) {
const config = loadConfig();
if (config) {
console.log(` Repositories: ${config.repos?.length || 0}`);
const hookCount = config.repos?.reduce(
(acc, repo) => acc + (repo.hooks?.length || 0),
0
) || 0;
console.log(` Total Hooks: ${hookCount}`);
if (config.default_stages) {
console.log(` Default Stages: ${config.default_stages.join(", ")}`);
}
if (config.fail_fast) {
console.log(` Fail Fast: enabled`);
}
}
}
// Check git hooks installation
const gitHooksPath = ".git/hooks/pre-commit";
const hooksInstalled = existsSync(gitHooksPath);
console.log(`\n🪝 Git Hooks: ${hooksInstalled ? "✅ Installed" : "❌ Not installed"}`);
if (!hooksInstalled && configExists) {
console.log("\n💡 Run 'pre-commit install' to set up git hooks");
}
}
async function install(options: { installHooks?: boolean }): Promise<void> {
console.log("📦 Installing pre-commit hooks...\n");
const installed = await checkPreCommitInstalled();
if (!installed) {
console.log("❌ pre-commit is not installed");
console.log("💡 Install with: pip install pre-commit");
process.exit(1);
}
if (!existsSync(CONFIG_FILE)) {
console.log(`❌ ${CONFIG_FILE} not found`);
console.log("💡 Create one with: pre-commit sample-config > .pre-commit-config.yaml");
process.exit(1);
}
try {
if (options.installHooks) {
console.log("Installing hooks with dependencies...");
await $`pre-commit install --install-hooks`;
} else {
await $`pre-commit install`;
}
console.log("\n✅ Pre-commit hooks installed successfully!");
} catch (error) {
console.error("❌ Failed to install hooks:", error);
process.exit(1);
}
}
async function run(options: {
allFiles?: boolean;
hookId?: string;
files?: string[];
}): Promise<void> {
const installed = await checkPreCommitInstalled();
if (!installed) {
console.error("❌ pre-commit is not installed");
process.exit(1);
}
const args: string[] = ["run"];
if (options.hookId) {
args.push(options.hookId);
}
if (options.allFiles) {
args.push("--all-files");
}
if (options.files && options.files.length > 0) {
args.push("--files", ...options.files);
}
console.log(`🚀 Running: pre-commit ${args.join(" ")}\n`);
try {
const proc = Bun.spawn(["pre-commit", ...args], {
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
process.exit(exitCode);
} catch (error) {
console.error("❌ Hook execution failed");
process.exit(1);
}
}
async function update(options: { bleedingEdge?: boolean }): Promise<void> {
console.log("🔄 Updating pre-commit hooks...\n");
const installed = await checkPreCommitInstalled();
if (!installed) {
console.error("❌ pre-commit is not installed");
process.exit(1);
}
try {
if (options.bleedingEdge) {
await $`pre-commit autoupdate --bleeding-edge`;
} else {
await $`pre-commit autoupdate`;
}
console.log("\n✅ Hooks updated successfully!");
} catch (error) {
console.error("❌ Update failed:", error);
process.exit(1);
}
}
async function clean(): Promise<void> {
console.log("🧹 Cleaning pre-commit cache...\n");
const installed = await checkPreCommitInstalled();
if (!installed) {
console.error("❌ pre-commit is not installed");
process.exit(1);
}
try {
await $`pre-commit clean`;
console.log("✅ Cache cleaned successfully!");
} catch (error) {
console.error("❌ Clean failed:", error);
process.exit(1);
}
}
function validate(): void {
console.log("🔍 Validating pre-commit configuration...\n");
const config = loadConfig();
if (!config) {
process.exit(1);
}
const issues: string[] = [];
const warnings: string[] = [];
// Validate repos
if (!config.repos || config.repos.length === 0) {
issues.push("No repositories defined");
} else {
config.repos.forEach((repo, index) => {
// Check repo URL
if (!repo.repo) {
issues.push(`Repo ${index + 1}: Missing 'repo' field`);
}
// Check rev (warn if using branch-like names)
if (!repo.rev) {
issues.push(`Repo ${index + 1}: Missing 'rev' field`);
} else if (!repo.rev.startsWith("v") && !repo.rev.match(/^[a-f0-9]{7,40}$/)) {
if (repo.repo !== "local" && repo.repo !== "meta") {
warnings.push(
`Repo ${index + 1} (${repo.repo}): 'rev' should be a version tag or commit SHA, not '${repo.rev}'`
);
}
}
// Check hooks
if (!repo.hooks || repo.hooks.length === 0) {
issues.push(`Repo ${index + 1}: No hooks defined`);
} else {
repo.hooks.forEach((hook, hookIndex) => {
if (!hook.id) {
issues.push(`Repo ${index + 1}, Hook ${hookIndex + 1}: Missing 'id' field`);
}
});
}
});
}
// Report results
if (issues.length > 0) {
console.log("❌ Errors:");
issues.forEach((issue) => console.log(` - ${issue}`));
}
if (warnings.length > 0) {
console.log("\n⚠️ Warnings:");
warnings.forEach((warning) => console.log(` - ${warning}`));
}
if (issues.length === 0 && warnings.length === 0) {
console.log("✅ Configuration is valid!");
}
// Summary
console.log("\n📊 Summary:");
console.log(` Repositories: ${config.repos?.length || 0}`);
const hookCount =
config.repos?.reduce((acc, repo) => acc + (repo.hooks?.length || 0), 0) || 0;
console.log(` Total Hooks: ${hookCount}`);
if (issues.length > 0) {
process.exit(1);
}
}
function listHooks(): void {
console.log("📋 Configured Hooks\n");
const config = loadConfig();
if (!config) {
process.exit(1);
}
config.repos?.forEach((repo) => {
const repoName =
repo.repo === "local"
? "local"
: repo.repo === "meta"
? "meta"
: repo.repo.split("/").slice(-2).join("/");
console.log(`\n📦 ${repoName} (${repo.rev})`);
repo.hooks?.forEach((hook) => {
const name = hook.name || hook.id;
const stages = hook.stages?.join(", ") || "pre-commit";
console.log(` └─ ${hook.id}`);
if (hook.name && hook.name !== hook.id) {
console.log(` Name: ${hook.name}`);
}
if (hook.files) {
console.log(` Files: ${hook.files}`);
}
if (hook.stages) {
console.log(` Stages: ${stages}`);
}
if (hook.args && hook.args.length > 0) {
console.log(` Args: ${hook.args.join(" ")}`);
}
});
});
}
function showHelp(): void {
console.log(`
Pre-commit Manager - CLI for managing pre-commit configurations
Usage:
bun run PreCommitManager.ts <command> [options]
Commands:
status Show pre-commit installation status
install Install pre-commit hooks
--install-hooks Also download hook dependencies
run [hook-id] Run hooks
--all-files Run on all files
--files <files> Run on specific files
update Update hooks to latest versions
--bleeding-edge Use latest commits instead of tags
clean Clean cached environments
validate Validate configuration file
list List configured hooks
help Show this help message
Examples:
bun run PreCommitManager.ts status
bun run PreCommitManager.ts install --install-hooks
bun run PreCommitManager.ts run --all-files
bun run PreCommitManager.ts run black
bun run PreCommitManager.ts update
bun run PreCommitManager.ts validate
`);
}
// Main CLI
async function main(): Promise<void> {
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case "status":
await status();
break;
case "install":
await install({
installHooks: args.includes("--install-hooks"),
});
break;
case "run":
const hookId = args[1] && !args[1].startsWith("--") ? args[1] : undefined;
const allFiles = args.includes("--all-files");
const filesIndex = args.indexOf("--files");
const files =
filesIndex > -1 ? args.slice(filesIndex + 1).filter((a) => !a.startsWith("--")) : [];
await run({ allFiles, hookId, files });
break;
case "update":
await update({
bleedingEdge: args.includes("--bleeding-edge"),
});
break;
case "clean":
await clean();
break;
case "validate":
validate();
break;
case "list":
listHooks();
break;
case "help":
case "--help":
case "-h":
showHelp();
break;
default:
if (command) {
console.error(`Unknown command: ${command}\n`);
}
showHelp();
process.exit(command ? 1 : 0);
}
}
main().catch(console.error);
AddHooks Workflow
Add new hooks to an existing pre-commit configuration.
Trigger
- "add hook"
- "add linting"
- "add formatter"
- "add security scanning"
- "add terraform hooks"
- "add python linting"
Workflow
Step 1: Analyze Current Configuration
# Check existing hooks
bun run Tools/PreCommitManager.ts list
# Or manually review
cat .pre-commit-config.yamlStep 2: Identify Hook to Add
Common hook additions by category:
| Need | Hook |
|---|---|
| Python formatting | black, ruff-format |
| Python linting | flake8, ruff, pylint |
| Python type checking | mypy, pyright |
| JS/TS formatting | prettier, biome |
| JS/TS linting | eslint, biome |
| Terraform validation | terraform_fmt, terraform_validate |
| Terraform security | terraform_trivy, checkov |
| Secret detection | gitleaks, detect-secrets |
| YAML validation | yamllint |
| Shell linting | shellcheck |
| Commit messages | conventional-pre-commit |
Step 3: Add Hook Configuration
Adding to Existing Repository
If the hook is from a repository already in your config, just add to hooks::
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
# Add new hook here
- id: check-astAdding New Repository
Add new repo block:
repos:
# Existing repos...
# New repo
- repo: https://github.com/new-repo/hooks
rev: v1.0.0
hooks:
- id: new-hookStep 4: Common Hook Additions
Add Secret Detection
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaksAdd Python Type Checking
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies:
- types-requests
- types-PyYAMLAdd Terraform Docs
# Add to existing pre-commit-terraform repo
- id: terraform_docs
args:
- --hook-config=--path-to-file=README.md
- --hook-config=--create-file-if-not-exist=trueAdd Conventional Commits
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.6.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: [feat, fix, docs, style, refactor, perf, test, chore, ci, build]Then install commit-msg hook:
pre-commit install --hook-type commit-msgAdd Security Scanning (Checkov)
- repo: https://github.com/bridgecrewio/checkov
rev: 3.2.277
hooks:
- id: checkov
args: [--quiet, --compact]Add Markdown Linting
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.42.0
hooks:
- id: markdownlint
args: [--fix]Add Shell Script Linting
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck
args: [-x]Step 5: Test New Hook
# Update hook cache
pre-commit clean
pre-commit install --install-hooks
# Run specific hook
pre-commit run <hook-id> --all-files
# Run all hooks
pre-commit run --all-filesStep 6: Commit Changes
git add .pre-commit-config.yaml
git commit -m "chore: add <hook-name> pre-commit hook"Hook Configuration Tips
Filtering Files
- id: eslint
files: \.[jt]sx?$ # Only JS/TS files
exclude: ^(vendor|node_modules)/Custom Arguments
- id: flake8
args: [--max-line-length=88, --extend-ignore=E203]Adding Dependencies
- id: mypy
additional_dependencies:
- types-requests>=2.31
- pydantic>=2.0Running at Specific Stages
- id: gitleaks
stages: [pre-commit, pre-push]Always Run (Even Without Matches)
- id: generate-api-docs
always_run: true
pass_filenames: falseFinding Hook IDs
1. Check repository's .pre-commit-hooks.yaml:
curl -s https://raw.githubusercontent.com/pre-commit/pre-commit-hooks/main/.pre-commit-hooks.yaml2. Check pre-commit hooks index: https://pre-commit.com/hooks.html
3. Check repository README for hook documentation
CI Integration Workflow
Integrate pre-commit hooks into CI/CD pipelines.
Trigger
- "CI pipeline"
- "GitHub Actions pre-commit"
- "GitLab CI pre-commit"
- "Azure Pipelines pre-commit"
- "automate pre-commit"
GitHub Actions
Recommended: Official Action
# .github/workflows/pre-commit.yml
name: Pre-commit
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: pre-commit/action@v3.0.1With Caching
name: Pre-commit
on: [push, pull_request]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache pre-commit
uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- uses: pre-commit/action@v3.0.1With Additional Tools (Terraform, Node)
name: Pre-commit
on: [push, pull_request]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.9.0'
- name: Install tflint
run: |
curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
- name: Cache pre-commit
uses: actions/cache@v4
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- uses: pre-commit/action@v3.0.1Manual Installation (Alternative)
name: Pre-commit
on: [push, pull_request]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install pre-commit
run: pip install pre-commit
- name: Run pre-commit
run: pre-commit run --all-files---
GitLab CI
Basic Configuration
# .gitlab-ci.yml
stages:
- lint
pre-commit:
stage: lint
image: python:3.11
variables:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
cache:
key: pre-commit
paths:
- .cache/pre-commit
before_script:
- pip install pre-commit
script:
- pre-commit run --all-files
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHWith Terraform Tools
pre-commit:
stage: lint
image: python:3.11
variables:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
TERRAFORM_VERSION: "1.9.0"
TFLINT_VERSION: "0.53.0"
cache:
key: pre-commit-${CI_COMMIT_REF_SLUG}
paths:
- .cache/pre-commit
before_script:
- pip install pre-commit
# Install Terraform
- apt-get update && apt-get install -y unzip
- curl -o terraform.zip https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip
- unzip terraform.zip && mv terraform /usr/local/bin/
# Install TFLint
- curl -L -o tflint.zip https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip
- unzip tflint.zip && mv tflint /usr/local/bin/
script:
- pre-commit run --all-files---
Azure Pipelines
Basic Configuration
# azure-pipelines.yml
trigger:
- main
- develop
pr:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.11'
- script: pip install pre-commit
displayName: 'Install pre-commit'
- script: pre-commit run --all-files
displayName: 'Run pre-commit'With Caching
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
PRE_COMMIT_HOME: $(Pipeline.Workspace)/.cache/pre-commit
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.11'
- task: Cache@2
inputs:
key: 'pre-commit | "$(Agent.OS)" | .pre-commit-config.yaml'
path: $(PRE_COMMIT_HOME)
displayName: 'Cache pre-commit'
- script: pip install pre-commit
displayName: 'Install pre-commit'
- script: pre-commit run --all-files
displayName: 'Run pre-commit'---
CircleCI
# .circleci/config.yml
version: 2.1
jobs:
pre-commit:
docker:
- image: cimg/python:3.11
steps:
- checkout
- restore_cache:
keys:
- pre-commit-{{ checksum ".pre-commit-config.yaml" }}
- run:
name: Install pre-commit
command: pip install pre-commit
- run:
name: Run pre-commit
command: pre-commit run --all-files
- save_cache:
key: pre-commit-{{ checksum ".pre-commit-config.yaml" }}
paths:
- ~/.cache/pre-commit
workflows:
version: 2
lint:
jobs:
- pre-commit---
Bitbucket Pipelines
# bitbucket-pipelines.yml
image: python:3.11
pipelines:
default:
- step:
name: Pre-commit
caches:
- pre-commit
script:
- pip install pre-commit
- pre-commit run --all-files
definitions:
caches:
pre-commit: ~/.cache/pre-commit---
Jenkins
Jenkinsfile (Declarative)
// Jenkinsfile
pipeline {
agent {
docker {
image 'python:3.11'
}
}
stages {
stage('Pre-commit') {
steps {
sh 'pip install pre-commit'
sh 'pre-commit run --all-files'
}
}
}
post {
always {
cleanWs()
}
}
}---
pre-commit.ci (Dedicated Service)
Auto-fix PRs
Add .pre-commit-ci.yaml to repository root:
# .pre-commit-ci.yaml
ci:
autofix_prs: true
autofix_commit_msg: 'style: auto-fix from pre-commit.ci'
autoupdate_schedule: monthly
autoupdate_commit_msg: 'chore: pre-commit autoupdate'
skip: [terraform_validate] # Skip hooks that need external toolsBadge
Add to README.md:
[](https://results.pre-commit.ci/latest/github/OWNER/REPO/main)---
Best Practices
1. Run on Changed Files Only (for PRs)
# GitHub Actions
- uses: pre-commit/action@v3.0.1
with:
extra_args: --from-ref origin/${{ github.base_ref }} --to-ref HEAD2. Fail Fast in CI
# .pre-commit-config.yaml
fail_fast: true # Stop after first failure3. Skip CI-Incompatible Hooks
Some hooks require local tools or user interaction:
# .pre-commit-config.yaml
- id: terraform_validate
stages: [pre-commit] # Won't run in CI with --all-filesOr skip in CI:
SKIP=interactive-hook pre-commit run --all-files4. Separate Security Scanning
Run security hooks separately for better visibility:
# GitHub Actions
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pre-commit/action@v3.0.1
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install pre-commit
- run: pre-commit run gitleaks --all-files
- run: pre-commit run checkov --all-files5. Cache Effectively
Always cache ~/.cache/pre-commit with config hash as key:
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}CustomHook Workflow
Create custom pre-commit hooks for project-specific needs.
Trigger
- "create custom hook"
- "write local hook"
- "project-specific hook"
- "custom pre-commit"
Local Hook Types
1. Script Hook
Simple shell/python script executed as-is.
- repo: local
hooks:
- id: my-script-hook
name: My Script Hook
entry: ./scripts/check.sh
language: script
files: \.py$2. System Hook
Uses system-installed executable.
- repo: local
hooks:
- id: my-system-hook
name: My System Hook
entry: /usr/local/bin/my-tool
language: system
types: [python]3. Python Hook
Python script with optional dependencies.
- repo: local
hooks:
- id: my-python-hook
name: My Python Hook
entry: python scripts/check.py
language: python
additional_dependencies: [requests, pyyaml]4. Node Hook
Node.js script with npm dependencies.
- repo: local
hooks:
- id: my-node-hook
name: My Node Hook
entry: node scripts/check.js
language: node
additional_dependencies: [lodash, chalk]Common Custom Hook Examples
API Key Check
- repo: local
hooks:
- id: check-api-keys
name: Check for API Keys
entry: bash -c 'if grep -rE "(api_key|API_KEY|apikey)\\s*[:=]\\s*['\''\""][^'\''\"\n]+['\''\""]" "$@"; then echo "API key found!"; exit 1; fi'
language: system
types: [text]Kubernetes Manifest Validation
- repo: local
hooks:
- id: validate-k8s
name: Validate Kubernetes Manifests
entry: kubectl apply --dry-run=client -f
language: system
files: \.(yaml|yml)$
types: [yaml]Helm Template Validation
- repo: local
hooks:
- id: helm-template
name: Helm Template Check
entry: bash -c 'for chart in $(find . -name Chart.yaml -exec dirname {} \;); do helm template "$chart" > /dev/null || exit 1; done'
language: system
pass_filenames: false
always_run: trueDocker Build Check
- repo: local
hooks:
- id: docker-build
name: Docker Build Check
entry: docker build --no-cache -f
language: system
files: DockerfilePython Import Sort Check
- repo: local
hooks:
- id: check-imports
name: Check Import Order
entry: python -c "
import sys
for f in sys.argv[1:]:
with open(f) as file:
lines = file.readlines()
imports = [l for l in lines if l.startswith('import') or l.startswith('from')]
if imports != sorted(imports):
print(f'{f}: imports not sorted')
sys.exit(1)
"
language: system
types: [python]TypeScript Type Check
- repo: local
hooks:
- id: tsc-check
name: TypeScript Type Check
entry: npx tsc --noEmit
language: system
files: \.[jt]sx?$
pass_filenames: falsePrettier with Specific Config
- repo: local
hooks:
- id: prettier-custom
name: Prettier (Custom Config)
entry: npx prettier --config .prettierrc.custom.json --write
language: system
types_or: [javascript, jsx, ts, tsx, json, yaml]Database Migration Check
- repo: local
hooks:
- id: check-migrations
name: Check Database Migrations
entry: python manage.py makemigrations --check --dry-run
language: system
pass_filenames: false
always_run: trueLicense Header Check
- repo: local
hooks:
- id: license-header
name: Check License Header
entry: bash -c '
header="# Copyright (c) 2024 Company Name"
for file in "$@"; do
if ! head -1 "$file" | grep -q "^$header"; then
echo "$file: missing license header"
exit 1
fi
done
'
language: system
types: [python]File Size Check
- repo: local
hooks:
- id: check-file-size
name: Check File Size
entry: bash -c '
max_size=1000000 # 1MB
for file in "$@"; do
size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file")
if [ "$size" -gt "$max_size" ]; then
echo "$file: exceeds max size ($size > $max_size bytes)"
exit 1
fi
done
'
language: system
types: [file]Creating a Hook Repository
For reusable hooks, create a dedicated repository.
Structure
my-hooks/
├── .pre-commit-hooks.yaml
├── hooks/
│ ├── check-api-keys.sh
│ ├── validate-k8s.py
│ └── lint-terraform.sh
└── README.md.pre-commit-hooks.yaml
- id: check-api-keys
name: Check for API Keys
entry: hooks/check-api-keys.sh
language: script
types: [text]
- id: validate-k8s
name: Validate Kubernetes Manifests
entry: hooks/validate-k8s.py
language: python
types: [yaml]
additional_dependencies: [kubernetes>=25.0.0, pyyaml>=6.0]
- id: lint-terraform
name: Lint Terraform Files
entry: hooks/lint-terraform.sh
language: script
files: \.tf$Sample Hook Script (hooks/check-api-keys.sh)
#!/bin/bash
set -euo pipefail
# Patterns to search for
patterns=(
"api_key\s*[:=]\s*['\"][^'\"]+['\"]"
"API_KEY\s*[:=]\s*['\"][^'\"]+['\"]"
"secret\s*[:=]\s*['\"][^'\"]+['\"]"
)
found=0
for file in "$@"; do
for pattern in "${patterns[@]}"; do
if grep -qiE "$pattern" "$file"; then
echo "Potential secret found in $file"
grep -niE "$pattern" "$file"
found=1
fi
done
done
exit $foundSample Python Hook (hooks/validate-k8s.py)
#!/usr/bin/env python3
import sys
import yaml
def validate_manifest(filepath):
with open(filepath) as f:
docs = list(yaml.safe_load_all(f))
for doc in docs:
if doc is None:
continue
if 'apiVersion' not in doc:
print(f"{filepath}: missing apiVersion")
return False
if 'kind' not in doc:
print(f"{filepath}: missing kind")
return False
return True
if __name__ == '__main__':
success = True
for filepath in sys.argv[1:]:
if not validate_manifest(filepath):
success = False
sys.exit(0 if success else 1)Using Your Hook Repository
# In .pre-commit-config.yaml
- repo: https://github.com/yourorg/my-hooks
rev: v1.0.0
hooks:
- id: check-api-keys
- id: validate-k8sBest Practices
1. Handle No Files Gracefully
#!/bin/bash
if [ $# -eq 0 ]; then
exit 0 # No files to check
fi2. Exit Codes
0: Success (hook passed)1: Failure (issues found)- Other non-zero: Error
3. Provide Helpful Output
echo "Checking $file..."
if [ $found -eq 1 ]; then
echo "❌ Issues found in $file"
echo " Line 42: potential API key"
else
echo "✅ $file passed"
fi4. Make Scripts Executable
chmod +x hooks/*.sh
chmod +x hooks/*.py5. Test Locally
# Test with specific files
pre-commit run my-hook --files path/to/file.py
# Test with all files
pre-commit run my-hook --all-files
# Try repo directly
pre-commit try-repo . my-hook --all-filesSetup Workflow
Initialize pre-commit for a new or existing project.
Trigger
- "setup pre-commit"
- "initialize pre-commit"
- "add pre-commit to project"
- "create pre-commit config"
Workflow
Step 1: Check Prerequisites
# Check if pre-commit is installed
pre-commit --version
# If not installed, recommend installation
pip install pre-commit
# or
brew install pre-commitStep 2: Detect Project Type
Examine the project to determine appropriate hooks:
| Indicator | Project Type |
|---|---|
pyproject.toml, setup.py, requirements.txt | Python |
package.json | JavaScript/TypeScript |
*.tf files | Terraform |
Chart.yaml | Helm |
kustomization.yaml | Kustomize |
Dockerfile | Docker |
go.mod | Go |
Cargo.toml | Rust |
Step 3: Generate Configuration
Use HookGenerator to create appropriate config:
# For Python project
bun run Tools/HookGenerator.ts python
# For JavaScript project
bun run Tools/HookGenerator.ts javascript
# For Infrastructure project
bun run Tools/HookGenerator.ts infrastructure
# For minimal setup
bun run Tools/HookGenerator.ts minimalStep 4: Install Git Hooks
# Install hooks
pre-commit install
# Also install commit-msg hooks if using conventional commits
pre-commit install --hook-type commit-msg
# Pre-download dependencies (optional but recommended)
pre-commit install --install-hooksStep 5: Initial Run
# Test on all files
pre-commit run --all-files
# If issues found, fix them
git add -A
pre-commit run --all-filesStep 6: Commit Configuration
git add .pre-commit-config.yaml
git commit -m "chore: add pre-commit configuration"Sample Configurations by Project Type
Python Project
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.3
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaksJavaScript/TypeScript Project
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-json
- id: check-added-large-files
- repo: https://github.com/biomejs/pre-commit
rev: v0.5.0
hooks:
- id: biome-check
additional_dependencies: ["@biomejs/biome@1.9.4"]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaksTerraform Project
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.2
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docs
- id: terraform_tflint
- id: terraform_trivy
args:
- --args=--severity=HIGH,CRITICAL
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaksSupporting Files
.yamllint.yaml
extends: default
rules:
line-length:
max: 120
truthy:
check-keys: false
document-start: disablepyproject.toml (for Python)
[tool.ruff]
line-length = 88
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
[tool.mypy]
python_version = "3.11"
strict = truebiome.json (for JavaScript)
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"formatter": {
"enabled": true,
"indentStyle": "space"
},
"linter": {
"enabled": true
}
}Common Issues
Permission Denied
chmod +x .git/hooks/pre-commitHooks Not Running
pre-commit install # ReinstallCache Issues
pre-commit clean
pre-commit install --install-hooksTroubleshoot Workflow
Debug and fix pre-commit hook issues.
Trigger
- "fix pre-commit"
- "pre-commit failing"
- "hook not working"
- "debug pre-commit"
- "pre-commit error"
Common Issues and Solutions
1. Hooks Not Running
Symptoms:
- Git commits without running hooks
- No output when committing
Solutions:
# Reinstall hooks
pre-commit install
# Also install commit-msg hooks if needed
pre-commit install --hook-type commit-msg
# Verify hook is installed
ls -la .git/hooks/pre-commit2. Hook Execution Errors
Symptoms:
- Hook fails with error messages
- Specific hook returns non-zero exit code
Debug Steps:
# Run with verbose output
PRE_COMMIT_VERBOSE=1 pre-commit run <hook-id> --all-files
# Run specific hook on specific file
pre-commit run <hook-id> --files path/to/file.py
# Check hook environment
pre-commit run <hook-id> --verbose3. Cache/Environment Issues
Symptoms:
- "Failed to install..." errors
- Inconsistent behavior
- Wrong versions running
Solutions:
# Clear all cached environments
pre-commit clean
# Reinstall with fresh dependencies
pre-commit install --install-hooks
# Check cache location
ls ~/.cache/pre-commit/4. Version Mismatch
Symptoms:
- "Hook not found" errors
- Repository version doesn't have expected hook
Solutions:
# Update hooks to latest versions
pre-commit autoupdate
# Check available hooks in repo
pre-commit try-repo <repo-url> --all-files
# Validate configuration
bun run Tools/HookValidator.ts5. Configuration Errors
Symptoms:
- YAML parsing errors
- "Invalid config" messages
Solutions:
# Validate YAML syntax
python -c "import yaml; yaml.safe_load(open('.pre-commit-config.yaml'))"
# Validate configuration
bun run Tools/HookValidator.ts --verbose
# Check for common issues
pre-commit validate-config .pre-commit-config.yaml6. Slow Hook Execution
Symptoms:
- Hooks take too long
- Timeout errors
Solutions:
# Limit files processed
- id: slow-hook
files: ^src/ # Only check src/ directory
# Run hooks in parallel (default)
# Or force serial for problematic hooks
- id: problematic-hook
require_serial: true
# Skip slow hooks temporarily
SKIP=slow-hook git commit -m "message"7. Dependency Issues
Symptoms:
- "ModuleNotFoundError"
- "Package not found"
- Missing dependencies
Solutions:
# Add required dependencies
- id: mypy
additional_dependencies:
- types-requests
- pydantic>=2.0
# For node hooks
- id: eslint
additional_dependencies:
- eslint@9.14.0
- typescript
- "@typescript-eslint/parser"8. Git Hook Conflicts
Symptoms:
- Other tools overwriting hooks
- Husky/lefthook conflicts
Solutions:
# Check what's in hooks directory
cat .git/hooks/pre-commit
# Reinstall pre-commit hooks
pre-commit install --allow-missing-config
# If using husky, disable it
rm -rf .husky9. Terraform Hook Issues
Symptoms:
- terraform_validate fails
- "Provider not found"
Solutions:
# Initialize terraform first
terraform init
# Clear terraform cache
rm -rf .terraform
terraform init
# Use retry option
- id: terraform_validate
args:
- --hook-config=--retry-once-with-cleanup=true10. File Pattern Issues
Symptoms:
- Hook not running on expected files
- Hook running on wrong files
Debug:
# Test file pattern matching
pre-commit run <hook-id> --files specific/file.py
# Check what files would be matched
pre-commit run <hook-id> --all-files --verboseFix:
# Use correct regex patterns
- id: eslint
files: \.[jt]sx?$ # Matches .js, .jsx, .ts, .tsx
exclude: ^(dist|node_modules)/Debug Techniques
Enable Verbose Mode
PRE_COMMIT_VERBOSE=1 pre-commit run --all-filesTrace Mode (for pre-commit-terraform)
PCT_LOG=trace pre-commit run terraform_validateCheck Hook Exit Code
pre-commit run <hook-id> --all-files; echo "Exit code: $?"Inspect Hook Environment
# Find hook's virtual environment
ls ~/.cache/pre-commit/
# Activate and inspect
source ~/.cache/pre-commit/<hash>/py_env-python3.11/bin/activate
pip listTest Hook in Isolation
# Try running hook directly from repo
pre-commit try-repo https://github.com/psf/black --all-filesRecovery Steps
Complete Reset
# 1. Remove all hooks
pre-commit uninstall
# 2. Clear cache
pre-commit clean
# 3. Remove git hooks
rm .git/hooks/pre-commit
rm .git/hooks/commit-msg
# 4. Reinstall
pre-commit install --install-hooks
pre-commit install --hook-type commit-msg
# 5. Test
pre-commit run --all-filesSkip Hooks Temporarily
# Skip all hooks (use sparingly)
git commit --no-verify -m "emergency commit"
# Skip specific hooks
SKIP=flake8,mypy git commit -m "skip linting"Error Messages Reference
| Error | Cause | Solution |
|---|---|---|
Hook '<id>' not found | Wrong hook ID or version | Check repo's .pre-commit-hooks.yaml |
Failed to install... | Network/dependency issue | pre-commit clean && pre-commit install --install-hooks |
Executable not found | Missing system dependency | Install required tool (terraform, helm, etc.) |
Check failed | Hook found issues | Fix the issues or add exceptions |
Timeout | Hook too slow | Add timeout or file filters |