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

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:

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

References