Git Hooks
Git hooks are scripts that Git runs automatically at points in its workflow: before a commit is created, after a commit message is written, before a push, after a checkout. They let you catch problems at the earliest, cheapest moment. Unformatted code, a leaked API key, a malformed commit message, or a failing test gets stopped before it ever leaves your machine.
Hooks are also easy to overdo. A pre-commit hook that runs the whole test suite for three minutes trains everyone to use --no-verify. The best setups keep local hooks fast and focused and leave comprehensive checks to CI.
TL;DR
- Hooks are executable files in
.git/hooks/named after the event (pre-commit,commit-msg,pre-push…). A non-zero exit aborts the action. .git/hooksisn't versioned. Share hooks withcore.hooksPathor a manager: Husky or lint-staged (JS), lefthook (any language), or the pre-commit framework (Python-based, language-agnostic).- Good pre-commit checks: format and lint staged files, secret scanning, and quick sanity checks.
- commit-msg enforces message conventions such as Conventional Commits.
- pre-push can run faster test subsets; keep the full suite in CI.
- Hooks can be bypassed (
--no-verify), so CI must re-check anything that matters.
Quick Example
A lefthook configuration shared through the repo:
Every developer who runs npm install (with a prepare script calling lefthook install) gets the same hooks.
Core Concepts
Client-Side Hooks
Hooks receive arguments. For example, commit-msg gets the path to the message file, and pre-push gets the remote name and URL on the command line plus refs on stdin. Their exit code decides whether Git proceeds.
Server-Side Hooks
On a self-hosted Git server, pre-receive, update, and post-receive run when pushes arrive and can reject them, enforcing rules developers can't bypass. Hosted platforms (GitHub, GitLab, Bitbucket) offer the same outcomes through branch protection, rulesets, push rules, and required status checks rather than custom scripts.
Sharing Hooks
Because .git/ isn't committed, hooks must be installed per clone:
git config core.hooksPath .githookspoints Git at a committed directory. It's simple and dependency-free.- Husky (JavaScript projects) installs hooks via
npm install; pair it with lint-staged to run tools only on staged files. - lefthook is a fast single binary for any language, with parallel commands, glob filters, and staged-file templating.
- pre-commit (the framework) keeps a registry of reusable hooks with pinned versions in
.pre-commit-config.yaml, is popular in Python and polyglot repos, and runs in CI withpre-commit run --all-files.
What to Run Where
Rule of thumb: pre-commit under ~5 seconds, pre-push under ~30 seconds. Anything slower belongs in CI.
Best Practices
Only Check Staged Files
Linting the entire repository on every commit is slow and flags problems in files the developer didn't touch. lint-staged, lefthook's {staged_files}, and the pre-commit framework all scope checks to what's being committed.
Auto-Fix Where Possible
Formatters should fix and re-stage files rather than fail and make the developer rerun them. Reserve hard failures for problems that need human judgment.
Scan for Secrets Before They Leave
A committed secret is compromised the moment it's pushed, and it lives on in history even after a follow-up commit removes it. Running gitleaks or trufflehog in pre-commit is the cheapest protection there is. See secrets management.
Mirror Every Hook in CI
Hooks are a convenience, not a control: they can be skipped with --no-verify, disabled, or never installed. Run the same checks in CI and make them required for merging. See linting and formatting.
Common Mistakes
Slow Hooks That Everyone Skips
Move heavy checks to pre-push or CI, and keep pre-commit fast enough that nobody wants to bypass it.
Hooks That Aren't Executable or Portable
A hook file without the executable bit silently doesn't run. Bash-specific scripts break on Windows machines without Git Bash. Use a hook manager or a portable runtime (Node, Python) and test on all platforms your team uses.
Checking the Working Tree Instead of the Index
A naive pre-commit hook linting files on disk may pass on unstaged fixes while the staged version, the one actually being committed, still has errors. Use tools that operate on staged content.
FAQ
Are Git hooks committed to the repository?
Not by default: .git/hooks is local to each clone. Share them by committing a hooks directory and setting core.hooksPath, or by using a manager like Husky, lefthook, or the pre-commit framework that installs them on setup.
Can hooks be enforced?
Client-side hooks can always be bypassed with --no-verify or by not installing them. Enforce rules server-side, with branch protection, required status checks, rulesets, and push rules on your Git host, and use client hooks for fast feedback.
Husky, lefthook, or pre-commit?
Husky plus lint-staged is the conventional choice in JavaScript and TypeScript repos. lefthook is fast, language-agnostic, and handles parallelism and monorepos well. The pre-commit framework has a large catalog of reusable hooks and fits Python or polyglot projects. All three work; pick the one matching your ecosystem.
How do I run hooks on files already in the repo?
Most managers support it: pre-commit run --all-files, lefthook run pre-commit --all-files, or running the underlying tools directly. It's useful when adopting a new formatter, typically as one dedicated "format everything" commit.
Related Topics
- Git — The version control system overview
- Linting & Formatting — The tools hooks usually run
- Code Quality — Automated quality gates
- Secrets Management — Keeping credentials out of repositories
- CI/CD — The authoritative place for checks