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

#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
| Hook | When it runs | Exits non-zero = |
|---|---|---|
pre-commit | Before commit message is entered | Commit aborted |
prepare-commit-msg | After default message created, before editor opens | Commit aborted |
commit-msg | After commit message entered | Commit aborted |
post-commit | After commit created | No effect on commit |
pre-push | Before git push starts | Push aborted |
post-push | After push completes | No effect |
pre-rebase | Before rebase starts | Rebase aborted |
post-merge | After git merge completes | No effect |
post-checkout | After git checkout | No 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
npm install --save-dev husky#3Initialize
npx husky initThis does three things:
- Creates a
.husky/directory with a samplepre-commitfile - Adds
"prepare": "husky"to yourpackage.jsonscripts - 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
# 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
npm install --save-dev lint-staged#3Configure in package.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:
// 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
# .husky/pre-commit
npx lint-stagedThat'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:
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 v9The types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.
#3Install
npm install --save-dev @commitlint/cli @commitlint/config-conventional#3Configure
// 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
# .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:
⧗ 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.
# .husky/pre-push
npm test -- --passWithNoTests --watchAll=falseFor TypeScript projects, add a type-check:
# .husky/pre-push
npx tsc --noEmit
npm test -- --passWithNoTests --watchAll=falseTip: 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:
git commit --no-verify -m "wip: work in progress"
git push --no-verifyThis 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:
# 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// package.json additions
{
"scripts": {
"prepare": "husky"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yaml}": ["prettier --write"]
}
}// commitlint.config.js
export default { extends: ['@commitlint/config-conventional'] };# .husky/pre-commit
npx lint-staged# .husky/commit-msg
npx --no -- commitlint --edit "$1"# .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:
{
"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):
# .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:
.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
- Git SCM - git hooks documentation
- pre-commit - Framework documentation
- Husky - Git hooks for Node.js
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
- 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.
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.
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.
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).
Tools Mentioned in This Article
.gitignore Generator
Generate .gitignore files for any language, framework, OS, or editor.
Git Commit Message Generator
Generate structured conventional commit messages from git diffs or change descriptions with AI or instant rule-based parsing.
.env File Parser
Parse, edit, and export .env files as JSON, Docker flags, or shell exports.
Crontab Expression Generator
Build and validate cron expressions with a visual editor.
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.