Resourcesโ€บGit Tricksโ€บGit Hooks: Automate Code Quality Before Every Commit
๐Ÿ”€Git Tricksโ€” Git Hooks: Automate Code Quality Before Every Commitโฑ 6 min

Git Hooks: Automate Code Quality Before Every Commit

Git hooks run scripts at key points in the git workflow. Here's how to use pre-commit hooks to enforce quality automatically.

๐Ÿ“…February 4, 2026โœTechTwitter.iogithooksautomationcode-quality

What Are Git Hooks?

Git hooks are scripts that run automatically at specific points in the git workflow: before a commit, after a push, before a merge, etc. They live in .git/hooks/ and can be written in any language.

The most useful hook for daily development: pre-commit โ€” runs before every commit, can block the commit if it exits with a non-zero code.


The Quick Setup: Husky + lint-staged

Writing hooks manually is error-prone (they don't get committed with the repo). Husky makes hooks shareable:

npm install -D husky lint-staged

# Initialize husky
npx husky init

This creates a .husky/ directory and adds a prepare script to package.json.


Pre-commit Hook: Lint and Format Only Changed Files

Linting your entire codebase on every commit is slow. lint-staged runs linters only on staged files:

# .husky/pre-commit
npx lint-staged
// package.json
{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css}": [
      "prettier --write"
    ]
  }
}

Now every commit auto-fixes lint issues and formats code. If ESLint finds errors it can't auto-fix, the commit is blocked.


Pre-commit Hook: Run Type Check

TypeScript type errors shouldn't reach commits:

# .husky/pre-commit
npx lint-staged
npx tsc --noEmit  # type check without emitting files

This adds a few seconds per commit but catches type errors before they hit CI.


Commit Message Validation: commit-msg Hook

Enforce consistent commit message formats:

# .husky/commit-msg
npx --no -- commitlint --edit $1
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']],
    'subject-min-length': [2, 'always', 10],
  }
}
npm install -D @commitlint/cli @commitlint/config-conventional

Now this will be rejected:

fix stuff

And this will pass:

fix: resolve null pointer in user authentication middleware

Pre-push Hook: Run Tests Before Pushing

Block pushes if tests fail:

# .husky/pre-push
npm test -- --passWithNoTests

This is heavier than pre-commit (test suite runs), so pre-push is a good compromise: fast feedback locally, full verification before it hits CI.


Writing a Custom Hook

Plain shell scripts work fine. Here's one that prevents committing secrets:

#!/bin/bash
# .husky/pre-commit โ€” check for common secret patterns

SECRETS_PATTERN="(password|secret|api_key|access_token)\s*=\s*['\"][^'\"]{8,}"

if git diff --cached | grep -iE "$SECRETS_PATTERN"; then
  echo "โŒ Possible secrets detected in staged files. Commit blocked."
  echo "   Use environment variables or a secrets manager instead."
  exit 1
fi

Skip Hooks When You Need To

Sometimes you need to bypass hooks (fixing a CI emergency, WIP commit):

git commit --no-verify -m "WIP: temporary debugging commit"

Use sparingly. The --no-verify flag skips all hooks.


Sharing Hooks with Your Team

The .husky/ directory commits to git. Everyone on the team gets the hooks automatically when they run npm install (via the prepare script).

// package.json
{
  "scripts": {
    "prepare": "husky"
  }
}

Key Takeaways

  • Git hooks live in .git/hooks/ but use Husky to make them shareable via git
  • lint-staged runs linters only on staged files โ€” fast pre-commit checks
  • pre-commit: lint, format, type-check
  • commit-msg: enforce conventional commit format with commitlint
  • pre-push: run test suite before changes leave your machine
  • Skip with --no-verify in emergencies only