Skip to main content
AllDevToolsHub
2026-07-29
Last reviewed: Aug 2026
DX
Est Read: 09_MIN

Git Hooks Deep Dive: Automate Code Quality Before Every Commit (2026)

Git Hooks Deep Dive: Automate Code Quality Before Every Commit (2026)
Processing_Node: 01

#1Git hooks: lightweight automation before every commit

What we tested: We evaluated the tools and techniques described in this article using real developer workflows. All browser-based tools on this site run locally, your data never leaves your machine.

Git hooks are the quickest way to run small checks before a commit lands.

Hooks are useful for fast feedback, but they are not a replacement for CI. The real value is knowing which hooks are worth using and which ones usually get in the way.


#2What Git Hooks Are (and Are Not)

Git hooks are executable scripts stored in .git/hooks/. When Git performs certain operations, creating a commit, pushing, merging, it runs the corresponding hook script before or after. If the script exits with a non-zero code, Git aborts the operation.

What hooks ARE good for:

  • Linting code before it is committed (catch problems in under a second)
  • Enforcing commit message format (conventional commits, ticket references)
  • Running fast unit tests before pushing
  • Preventing secrets from being committed

What hooks ARE NOT good for:

  • Full test suites (too slow, they block developers)
  • Anything that requires network access (can time out in poor connectivity)
  • Enforcing team-wide policies (hooks live in .git/hooks/, which is not committed, every developer can bypass or delete them)

For hard enforcement, use CI. Hooks are a developer experience tool, fast feedback, not a security boundary.


#2All the Hook Types

HookWhen it runsExits non-zero =
pre-commitBefore commit message is enteredCommit aborted
prepare-commit-msgAfter default message created, before editor opensCommit aborted
commit-msgAfter commit message enteredCommit aborted
post-commitAfter commit createdNo effect on commit
pre-pushBefore git push startsPush aborted
post-pushAfter push completesNo effect
pre-rebaseBefore rebase startsRebase aborted
post-mergeAfter git merge completesNo effect
post-checkoutAfter git checkoutNo effect

The three you will actually use: pre-commit, commit-msg, pre-push.


#2The Problem with Plain .git/hooks/

Git does not commit the .git/ directory. If you write your hook scripts directly into .git/hooks/, they:

  • Do not exist on other developers' machines when they clone the repo
  • Are lost if someone deletes and re-clones the repo
  • Cannot be versioned, reviewed in PRs, or updated systematically

The solution: use Husky to store hook scripts in a committed directory (.husky/) and tell Git to look there instead of .git/hooks/.


#2Husky v9 Setup (The New Way)

Husky v9, released in early 2024, completely reworked its configuration. Most tutorials and Stack Overflow answers still show the v8 API. Here is the correct v9 setup.

#3Install

bash
npm install --save-dev husky

#3Initialize

bash
npx husky init

This does three things:

  1. Creates a .husky/ directory with a sample pre-commit file
  2. Adds "prepare": "husky" to your package.json scripts
  3. Configures Git to use .husky/ as the hooks directory

The prepare script runs automatically after npm install, so every developer who clones your repo and runs npm install gets the hooks set up automatically, no manual step required.

#3Verify

bash
# Check that Git knows about .husky/
git config core.hooksPath
# Should output: .husky

#2lint-staged, Lint Only Changed Files

Running your linter on the entire codebase on every commit is too slow. lint-staged runs linters only on the files staged for commit (the files in the "green" state in git status).

#3Install

bash
npm install --save-dev lint-staged

#3Configure in package.json

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

Or use a standalone lint-staged.config.js:

javascript
// lint-staged.config.js
export default {
  '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
  '*.{css,scss}': ['stylelint --fix', 'prettier --write'],
  '*.{json,md}': ['prettier --write'],
};

#3Wire it to the pre-commit hook

bash
# .husky/pre-commit
npx lint-staged

That's the entire pre-commit file. When you run git commit, Husky runs this script, which runs lint-staged, which lints only the staged files. If any linter exits non-zero, the commit is aborted.


#2commitlint, Enforce Conventional Commits

Conventional Commits is a specification for writing structured commit messages:

protocol
type(scope): description

feat(auth): add Google OAuth login
fix(api): handle null response from payment gateway
docs(readme): update installation instructions
chore(deps): update eslint to v9

The types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.

#3Install

bash
npm install --save-dev @commitlint/cli @commitlint/config-conventional

#3Configure

javascript
// commitlint.config.js
export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    // Customize rules here
    'scope-enum': [2, 'always', ['auth', 'api', 'ui', 'deps', 'config']],
    'header-max-length': [2, 'always', 100],
  },
};

#3Wire it to the commit-msg hook

bash
# .husky/commit-msg
npx --no -- commitlint --edit "$1"

$1 is the path to the file containing the commit message. commitlint reads this file and validates it against your rules.

Now if you try to commit with git commit -m "fixed stuff", Git aborts with:

protocol
⧗   input: fixed stuff
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings

#2Pre-push Hook, Run Tests Before Pushing

A pre-push hook runs before git push sends commits to the remote. This is the right place for tests, it runs once per push, not once per commit.

bash
# .husky/pre-push
npm test -- --passWithNoTests --watchAll=false

For TypeScript projects, add a type-check:

bash
# .husky/pre-push
npx tsc --noEmit
npm test -- --passWithNoTests --watchAll=false

Tip: Keep pre-push hooks fast. If your full test suite takes 10+ minutes, run only unit tests here and leave integration tests to CI.


#2Skipping Hooks

Sometimes you need to bypass hooks, for a WIP commit, a documentation-only change, or an emergency fix. Use --no-verify:

bash
git commit --no-verify -m "wip: work in progress"
git push --no-verify

This is intentional. Hooks are developer experience tools, not security gates. Document in your CONTRIBUTING.md when it is acceptable to skip them (and when it is not).


#2Complete Setup in a Fresh Project

Here is the entire setup from scratch:

bash
# 1. Install all the tools
npm install --save-dev husky lint-staged @commitlint/cli @commitlint/config-conventional

# 2. Initialize Husky (creates .husky/ and updates package.json)
npx husky init
json
// package.json additions
{
  "scripts": {
    "prepare": "husky"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yaml}": ["prettier --write"]
  }
}
javascript
// commitlint.config.js
export default { extends: ['@commitlint/config-conventional'] };
bash
# .husky/pre-commit
npx lint-staged
bash
# .husky/commit-msg
npx --no -- commitlint --edit "$1"
bash
# .husky/pre-push
npx tsc --noEmit

#2Monorepo Considerations

In a monorepo (Turborepo, Nx, pnpm workspaces), hooks typically live in the root package.

lint-staged with scoped linting:

json
{
  "lint-staged": {
    "apps/web/**/*.{ts,tsx}": ["eslint --fix"],
    "apps/api/**/*.{ts}": ["eslint --fix"],
    "packages/**/*.{ts,tsx}": ["eslint --fix", "prettier --write"]
  }
}

Running only the affected tests on pre-push (Turborepo):

bash
# .husky/pre-push
npx turbo run type-check test --affected

--affected (Turborepo) or --affected (Nx) runs only the tasks for packages that changed since the last commit, keeping pre-push times manageable even in large monorepos.


#2Common Pitfalls

#3Husky v8 vs v9 config

If you copied setup from a tutorial before 2024, you probably have the v8 approach: "husky": { "hooks": { "pre-commit": "..." } } in package.json. This does not work in v9. The v9 way is individual scripts in .husky/ with no package.json husky config key.

#3The prepare script does not run in CI

prepare runs after npm install but is skipped in CI environments when NODE_ENV=production. This is actually correct behaviour, CI should not install dev dependencies or set up Git hooks. If your CI does a plain npm install --production, hooks will not be set up, which is what you want.

#3lint-staged re-adds files when linters modify them

If ESLint/Prettier auto-fix a file, lint-staged automatically re-adds the fixed version to the commit. This is intentional and correct, you get a clean commit with both your changes and the auto-fix applied.

#3Windows line ending issues

On Windows, Husky hook scripts may fail if saved with CRLF line endings. Ensure .husky/* files have LF endings by adding this to .gitattributes:

protocol
.husky/* text eol=lf

#3Pre-commit hook is too slow

If your hook takes more than 3–5 seconds, developers will start using --no-verify habitually. Profile what is running: time npx lint-staged. If it is slow, ensure you are running in a tight scope (*.{ts,tsx} not **/*) and using eslint --fix (not eslint + a separate prettier run where prettier runs twice).


#2Try It In Your Browser

For generating a clean .gitignore file before you set up your hooks (to make sure your hook scripts and their generated files are properly ignored), use the AllDevToolsHub .gitignore Generator. Select your stack (Node.js, TypeScript, macOS, Windows) and get a production-ready .gitignore in seconds.


#2Frequently Asked Questions

#3What is the difference between Husky and git config core.hooksPath?

They accomplish the same thing, pointing Git to a committed hooks directory. core.hooksPath is the raw Git config. Husky is a wrapper that sets core.hooksPath automatically via the prepare npm script, so every developer gets it when they run npm install. If you prefer zero dependencies, you can skip Husky and just run git config core.hooksPath .husky manually (or in a setup script).

#3Can I use Husky with Yarn or pnpm?

Yes. For Yarn v2+ (PnP): add "postinstall": "husky" instead of "prepare": "husky" (PnP does not run prepare scripts by default). For pnpm: "prepare": "husky" works as-is.

#3How do I share hooks across a team without npm?

If your project does not use npm (Go, Python, etc.), use the raw approach: create a .hooks/ directory, add your scripts, document that new team members should run git config core.hooksPath .hooks after cloning. Or use pre-commit (Python), which manages hooks across any language.

#3Should I run my full test suite in the pre-commit hook?

No. Pre-commit hooks run on every git commit. A full test suite that takes minutes will destroy developer velocity. Run lint and type checks pre-commit; run unit tests pre-push; leave integration tests to CI.

#3My pre-commit hook does not run when I commit from my GUI tool (VS Code, GitKraken, etc.)

Most GUI Git tools respect hooks if they call the git binary directly. If your GUI tool has its own Git implementation (rare), hooks may not run. Check your tool's documentation, VS Code's built-in Git panel honours hooks; most other major tools do too.


Git hooks are a force multiplier: you invest one hour of setup, and every future commit becomes faster and cleaner automatically. The Husky + lint-staged + commitlint stack covers 95% of what teams need. Start with just the pre-commit hook and lint-staged, you can add commitlint and pre-push type-checks incrementally once the basic setup is comfortable.

For checking your actual .gitignore patterns are working correctly before your first commit, use the AllDevToolsHub .gitignore Generator. For validating regex patterns used in your commitlint scope-enum rules, the Regex Tester is the fastest way to test without writing a test file.

Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.

#2Sources / Further reading

Quick Summary

>- A complete guide to git hooks in 2026. Set up Husky v9 (the new --init way), lint-staged for per-file linting, commitlint for conventional commits, and pre-push test runs — without slowing your dev loop. Covers monorepos, skip flags, and common pitfalls.

Key Takeaways

Key Takeaways

  • Git hooks are scripts that run automatically at specific points in the Git workflow — pre-commit, commit-msg, pre-push are the most useful.
  • pre-commit hooks catch issues before they enter the repository: linting errors, secrets in code, formatting violations.
  • Frameworks like Husky (Node.js) and pre-commit (Python) make hook management portable across teams.
Use Cases

When to use it

  • Running ESLint and Prettier on staged files before every commit.
  • Preventing commits that contain AWS keys, passwords, or private keys.
  • Validating commit message format (Conventional Commits) before accepting a commit.
Watch out

Common Mistakes

  • Not sharing hooks with the team — hooks are local to each clone. Use a framework that installs hooks automatically on npm install.
  • Running slow hooks on every commit — keep pre-commit hooks fast (<2s). Move slow checks (full test suite) to pre-push or CI.
  • Bypassing hooks with --no-verify — establish a team norm that hooks are never skipped.
FAQ

Git Hooks Deep Dive: Automate Code Quality Before Every Commit (2026), Frequently Asked

How do I share Git hooks across a team?

Use a framework like Husky (Node.js) or pre-commit (Python) that installs hooks automatically when developers run npm install or pip install. Alternatively, configure core.hooksPath to a shared directory in the repo.

What is the difference between pre-commit and pre-push hooks?

pre-commit runs before a commit is created — it checks staged changes. pre-push runs before git push — it can run the full test suite. Use pre-commit for fast checks (lint, format) and pre-push for slower checks (tests, build).

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-07-29Last reviewed 2026-08-23

Tools Mentioned in This Article

Tools, tactics, and toughened-up tips, once a week

New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.

Found an error or have feedback?

We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.

Last reviewed: 2026-08-23
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.