By admin , 29 April 2026

Why pre-commit hooks

The cheapest place to catch a lint error is on the developer's machine, before the commit even forms. Pre-commit hooks run lint, format, and quick checks; if they fail, the commit is rejected. CI catches what slips through, but pre-commit catches the 95% of issues that should never reach CI.

The simplest hook

# .git/hooks/pre-commit
#!/usr/bin/env bash
set -e
npm run lint --silent
npm run format:check --silent
chmod +x .git/hooks/pre-commit

Now git commit runs both checks; failures abort.

Linting only changed files

#!/usr/bin/env bash
set -e
files=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\\.(js|ts|tsx)$' || true)
if [ -z "$files" ]; then
  exit 0
fi
echo "$files" | xargs npx eslint --max-warnings=0
echo "$files" | xargs npx prettier --check

Linting only the staged files keeps hooks fast - critical, because slow hooks get bypassed.

Husky for Node projects

npm install --save-dev husky lint-staged
npx husky init

# .husky/pre-commit
npx lint-staged

# package.json
"lint-staged": {
  "*.{js,ts,tsx}": ["eslint --fix", "prettier --write"]
}

Husky checks itself in via .husky/ so the whole team gets the hooks on npm install. lint-staged runs commands only on staged files.

The pre-commit framework (Python ecosystem)

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-merge-conflict
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.5.0
    hooks:
      - id: ruff
      - id: ruff-format
pip install pre-commit
pre-commit install

This framework supports any language and is the de facto choice for Python codebases.

Sharing hooks via core.hooksPath

Without an installer, point Git at a tracked directory:

git config core.hooksPath .githooks

Now hooks live in .githooks/ and are part of the repo. Each developer runs the config command once.

Bypassing when necessary

git commit --no-verify -m "WIP"

Sometimes you need to bypass - emergency hotfix, work-in-progress checkpoint. Document the policy and rely on CI as the authoritative gate.

Common pre-commit checks

  • Lint and format.
  • Type checks (TypeScript, mypy).
  • Trailing whitespace, end-of-file newlines.
  • Merge conflict markers.
  • Secret detection (gitleaks, trufflehog).
  • File size limits.

Keeping hooks fast

A hook that takes 30 seconds will be bypassed within a week. Aim for under 5 seconds. Run only on changed files. Cache aggressively. If something is slow, move it to CI and accept the trade-off.

Pre-commit hooks turn the local git commit into a quality gate. Set them up once per repo; reap the benefits forever.