Skip to content

Pre-commit Hooks Setup

Overview

Pre-commit hooks automatically validate code quality, formatting, and commit messages before allowing commits. We use Husky + lint-staged for consistent code quality across all projects.

Quick Start

Installation

# Install Husky and lint-staged
npm install --save-dev husky lint-staged

# Initialize Husky
npx husky install
npm pkg set scripts.prepare="husky install"

# Create pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"
chmod +x .husky/pre-commit

Basic Configuration

Add to package.json:

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.py": [
      "black",
      "isort",
      "flake8"
    ],
    "*.{css,scss}": [
      "stylelint --fix",
      "prettier --write"
    ],
    "*.{json,yaml,yml,md}": [
      "prettier --write"
    ]
  }
}

Hook Execution Flow

graph TD
    A[git commit] --> B[Husky intercepts]
    B --> C[lint-staged runs on changed files]
    C --> D{Linting & formatting}
    D -->|Pass| E[commitlint validates message]
    D -->|Fail| F[Commit rejected]
    E -->|Pass| G[Commit succeeds]
    E -->|Fail| F
    F --> H[Fix issues and retry]
    style G fill:#00b894
    style F fill:#ff6b6b

Core Configurations

Pre-commit Hook

# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged
npm run type-check

Advanced lint-staged Config

For more control, create lint-staged.config.js:

module.exports = {
  '*.{js,jsx,ts,tsx}': [
    'eslint --fix',
    'prettier --write',
    () => 'tsc --noEmit',
  ],
  '*.py': [
    'black --line-length 88',
    'isort --profile black',
    'flake8 --max-line-length 88',
  ],
  '*.{css,scss}': ['stylelint --fix', 'prettier --write'],
  '*.{md,mdx}': ['markdownlint --fix', 'prettier --write'],
  '*.{json,yaml,yml}': ['prettier --write'],
  'package.json': ['prettier --write', 'npm audit --audit-level moderate'],
}

Commit Message Validation

Setup Commitlint

# Install
npm install --save-dev @commitlint/cli @commitlint/config-conventional

# Configure
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

# Create commit-msg hook
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
chmod +x .husky/commit-msg

Custom Commitlint Rules

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'ci', 'perf'],
    ],
    'scope-enum': [
      2,
      'always',
      ['auth', 'api', 'ui', 'database', 'config', 'deps', 'security'],
    ],
    'subject-max-length': [2, 'always', 72],
  },
}

Tool Configurations

ESLint

// .eslintrc.js - Basic setup
module.exports = {
  extends: [
    '@typescript-eslint/recommended',
    'prettier', // Must be last
  ],
  plugins: ['@typescript-eslint', 'import'],
  rules: {
    '@typescript-eslint/no-unused-vars': 'error',
    'import/order': ['error', { 'newlines-between': 'always' }],
  },
}

For detailed ESLint configuration, see ESLint documentation.

Prettier

{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 80
}

Python Tools

# pyproject.toml
[tool.black]
line-length = 88
target-version = ['py39', 'py310', 'py311']

[tool.isort]
profile = "black"
line_length = 88
# .flake8
[flake8]
max-line-length = 88
extend-ignore = E203, W503
max-complexity = 10

For detailed Python tool configurations, see Black, isort, and Flake8 documentation.

Pre-push Hook

# .husky/pre-push
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run test:ci
npm audit --audit-level moderate

Team-Specific Configurations

Frontend Projects

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write",
      "jest --bail --findRelatedTests --passWithNoTests"
    ],
    "*.{css,scss}": ["stylelint --fix", "prettier --write"]
  }
}

Backend Projects

{
  "lint-staged": {
    "*.py": [
      "black --check",
      "isort --check-only",
      "flake8",
      "pytest tests/ -x"
    ]
  }
}

Performance Optimization

Skip CI Environment

# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# Skip in CI
if [ "$CI" = "true" ]; then
  exit 0
fi

npx lint-staged

Profile Execution

# Debug slow hooks
DEBUG=lint-staged* npx lint-staged

Troubleshooting

Hooks Not Running

# Reinstall Husky
npx husky install

# Fix permissions
chmod +x .husky/pre-commit
chmod +x .husky/commit-msg

# Verify Git config
git config core.hooksPath

Tool Not Found Errors

# Ensure dependencies are installed
npm install

# For Python tools
pip install black isort flake8

ESLint/Prettier Conflicts

Ensure Prettier is last in ESLint extends:

module.exports = {
  extends: [
    'eslint:recommended',
    'prettier', // Must be last
  ],
}

Skip Hooks (Emergency Only)

# Skip pre-commit (use sparingly)
git commit --no-verify -m "hotfix: critical production fix"

# Skip pre-push
git push --no-verify

IDE Integration

VS Code

{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true,
    "source.organizeImports": true
  },
  "[python]": {
    "editor.defaultFormatter": "ms-python.black-formatter"
  }
}

Install recommended extensions: - ESLint - Prettier - Python (with Black, Flake8)

ClickUp Integration

Pre-commit hooks work seamlessly with our ClickUp workflow:

  1. Hooks validate code quality before commit
  2. Conventional commits enable automatic ClickUp status updates
  3. Commit messages link to ClickUp tasks (e.g., feat(CU-123): add feature)

See GitHub-ClickUp Integration for details.

Common Commands

# Run lint-staged manually
npx lint-staged

# Test commitlint locally
echo "feat: test message" | npx commitlint

# Check specific files
npx eslint src/**/*.ts --fix
npx prettier --write "src/**/*.{js,json}"

# Python checks
black --check .
isort --check-only .
flake8 .

External Resources


Last Updated: 2025-01-20 Tools Required: Husky, lint-staged, ESLint, Prettier, commitlint