ZyVOP Logo
Content That Connects
SeriesCategoriesTags
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Tags
  • Write Article

Company

  • About Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeAutomate Your Code Quality with Git Hooks (And Never Argue in Code Review Again)

Automate Your Code Quality with Git Hooks (And Never Argue in Code Review Again)

Stop Catching Style Issues in Code Review — Your Editor Should've Flagged Them Before You Even Committed

#git#git-hooks#automation#code-quality#husky#lint-staged#commitlint#developer-productivity#CI/CD#devtools
Z
ZyVOP

Senior Developer

May 28, 2026
8 min read
1 views
Automate Your Code Quality with Git Hooks (And Never Argue in Code Review Again)

The Problem Git Hooks Solve

Picture a typical pull request review. Someone opens a PR and a reviewer digs in — but before getting to the actual logic, they're leaving comments like "unused import on line 12," "forgot to run prettier," "this console.log shouldn't be here," and "can you write a more descriptive commit message than 'fix'?" The author fixes the comments. Another review pass happens. Eventually the real work gets looked at.

None of that first round should have involved a human. Those are deterministic rules — a computer can check them in milliseconds, consistently, for everyone on the team, before the code ever leaves the author's machine. Git hooks are how you do that.


How Git Hooks Actually Work

When you run git init, Git creates a .git/ folder. Inside it is a hooks/ directory filled with sample scripts — pre-commit.sample, commit-msg.sample, pre-push.sample, and others. Remove the .sample extension, make the file executable, and Git runs it automatically at that moment in the workflow.

The mechanism is simple but powerful: if a hook script exits with a non-zero status code, Git stops what it was doing. A failing pre-commit hook aborts the commit. A failing pre-push hook aborts the push. The developer sees the error output and knows exactly what to fix before proceeding.

Here are the hooks you'll actually use:

Hook

When It Fires

Best Used For

pre-commit

Before the commit object is created

Linting, formatting, fast checks

commit-msg

After the commit message is written

Enforcing message conventions

prepare-commit-msg

Before the commit message editor opens

Pre-filling message templates

pre-push

Before data is sent to the remote

Running the full test suite

post-merge

After a successful merge or pull

Auto-installing new dependencies


The Raw Hook Problem (And Why You Need Tooling)

Here's the catch with raw Git hooks: .git/hooks/ is not tracked by Git. That means every hook you write lives only on your machine. When a teammate clones the repository, they get nothing. You'd have to manually distribute scripts and have every developer install them — which in practice means it never happens.

There's a second problem too. Raw hooks run against your entire working tree. On a large project, linting all 4,000 files every time you commit takes 30–60 seconds. Developers get annoyed and start using git commit --no-verify to skip everything. Once that habit forms, your hooks are useless.

Both problems have clean solutions.


Husky: Share Hooks Through the Repository

Husky solves the sharing problem. Instead of putting hook scripts in .git/hooks/, it stores them in .husky/ — a regular directory that IS tracked by Git. It then tells Git to look for hooks there instead of the default location.

Install and initialize:

npm install --save-dev husky
npx husky init

husky init creates the .husky/ directory with a sample pre-commit hook and adds a "prepare": "husky" script to your package.json. That prepare script is the magic: npm runs it automatically after every npm install. So when a developer clones your repo and installs dependencies, Husky is set up without any extra step.

Your project structure now looks like this:

your-project/
├── .husky/
│   ├── pre-commit       ← tracked by Git, shared with everyone
│   └── commit-msg
├── src/
└── package.json

To add a hook, just create a file with the hook name inside .husky/:

# .husky/pre-commit
npx lint-staged

Every developer gets this hook the moment they run npm install. No manual setup. No documentation to follow.


lint-staged: Only Process What Changed

Running your full linter on every commit is what makes hooks feel slow and painful. lint-staged fixes this by running tools only on the files that are staged for this commit — nothing more.

If you changed two files, only those two files are linted. The other 3,998 files are untouched. Most commits go from 30 seconds to under a second.

Install it:

npm install --save-dev lint-staged

Configure in package.json:

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

Run ESLint before Prettier — ESLint may restructure code, and Prettier should have the last word on formatting. The --fix and --write flags auto-correct issues where possible, so the developer often doesn't need to do anything manually.

Point your pre-commit hook to it:

# .husky/pre-commit
npx lint-staged

Now every commit automatically lints and formats only the files being committed. Clean output, no waiting, no excuses.


commitlint: Make Every Commit Message Mean Something

A git log full of "fix," "update," "wip," and "asdfghjkl" is useless. When you need to understand why a change was made six months later, or when you want to auto-generate a changelog, or when you're trying to trace a regression — a well-structured commit history is invaluable.

Conventional Commits is the most widely adopted standard. The format is:

type(optional scope): short description

feat: add OAuth login with Google
fix(auth): handle expired JWT tokens gracefully
docs: update API reference for /users endpoint
chore: upgrade ESLint to v9

commitlint enforces this automatically via the commit-msg hook.

Install it:

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

Create the config:

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
};

Add the hook:

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

Git passes the path to the commit message file as $1. commitlint reads it, validates it, and either approves or rejects the commit with a clear explanation:

# ✖ Rejected
git commit -m "fixed the thing"
# ✖   subject may not be empty [subject-empty]
# ✖   type may not be empty [type-empty]

# ✔ Accepted
git commit -m "fix(auth): resolve token expiry not being handled"
# ✔ found 0 problems, 0 warnings

The common commit types are: feat (new feature), fix (bug fix), docs (documentation), refactor (code restructure without behavior change), test (adding or updating tests), chore (maintenance, build changes), and perf (performance improvement).

Once your history is consistently structured, tools like semantic-release can read it to automatically determine version bumps and generate release notes — something that previously required manual effort.


The pre-push Hook: The Last Safety Net

pre-commit is fast and runs on every commit. Your full test suite shouldn't run there — it would make every commit feel heavy. But tests absolutely need to run before code reaches the remote branch, where they become someone else's problem.

The pre-push hook is the right place for this:

# .husky/pre-push
npm test

If tests fail, the push is blocked. The remote branch stays clean. No broken builds in CI, no colleagues pulling a broken main branch.

For TypeScript projects, add a type check too:

# .husky/pre-push
npx tsc --noEmit && npm test

--noEmit runs the TypeScript compiler for type checking only without emitting any files. It catches type errors that might slip past your IDE, and it's much faster than a full build.


The post-merge Hook: Stop Hitting "Cannot Find Module"

Here's a frustration every developer knows: you pull from main, run your app, and immediately hit an error about a missing module. A teammate added a package and you forgot to run npm install after pulling.

The post-merge hook runs after every successful merge and pull. You can use it to check whether package.json changed, and auto-install if it did:

# .husky/post-merge
#!/bin/sh

changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"

if echo "$changed_files" | grep -q "package.json"; then
  echo "📦 package.json changed — running npm install..."
  npm install
fi

This small script has saved countless developers from wasted debugging time. It's the kind of automation that makes your repo feel professional.


When to Bypass Hooks

Sometimes you legitimately need to skip the hooks — saving a work-in-progress state, making an emergency fix, committing generated code. Git makes this easy:

git commit --no-verify -m "wip: checkpoint before major refactor"
git push --no-verify

--no-verify tells Git to skip all hooks for that operation. This is a valid escape hatch — just not a habit. If you notice teammates using it regularly, that's a signal that a hook is too slow or too noisy. Investigate and fix the hook, not the behavior.


Complete Setup Reference

Here's everything you need for a new Node.js project in one place:

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

# Initialize Husky
npx husky init

# Create hooks
echo "npx lint-staged" > .husky/pre-commit
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
echo "npm test" > .husky/pre-push
// package.json — add this block
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{css,scss,json,md}": ["prettier --write"]
  }
}
// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };

That's a complete, automatically shared code quality pipeline. Every developer on your team runs the same checks, every time, from the moment they clone the repo.


Summary

Git hooks shift quality enforcement from "someone reviews for it" to "the system enforces it automatically." Husky makes hooks part of the repository so the whole team uses them without thinking. lint-staged keeps things fast by only touching changed files. commitlint makes your git history readable and automation-friendly.

The best part is that once this is set up, it runs invisibly. Nobody has to remember to lint before committing, nobody argues about formatting in review, and nobody merges code that breaks tests. The developer experience gets better for everyone, and the codebase stays cleaner with no extra effort.

Z

ZyVOP

Passionate developer sharing knowledge about modern web technologies and best practices.

Comments (0)

Login to post a comment.

Stay Updated

Get the latest articles delivered to your inbox.

We respect your privacy. Unsubscribe anytime.

Related Posts

Stop Dropping Connections: The Engineer's Guide to Zero-Downtime Deployments with Docker Compose

Read article

Ditch Vercel: A Complete Guide to Auto-Deploying Next.js to a VPS via GitHub Actions

Read article

Popular Tags

#.env.example Node.js#0x profiling#12-factor#AI agents#AI code security#AI coding tools 2026#AI-assisted development#AI-generated vulnerabilities#ALTER TABLE no lock#API Design