Git

Git Hooks Explained: Automate Your Workflow

How Git hooks work, the most useful ones to set up, and why teams reach for Husky instead of raw hooks for anything shared across a repo.

Marcus LeeJune 12, 20263 min read
Share:

Git hooks are scripts that run automatically at specific points in Git's workflow — commit, push, merge — and they're one of the most underused tools for catching mistakes before they leave your machine.

Where hooks live

bash
ls .git/hooks/

Every Git repo has a .git/hooks/ directory with sample hooks (.sample files) that aren't active by default. Removing the .sample extension and making the file executable activates it.

bash
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

The most useful hook: pre-commit

Runs before a commit is finalized — the natural place to catch problems before they enter history.

bash
#!/bin/sh
# .git/hooks/pre-commit
npm run lint
if [ $? -ne 0 ]; then
  echo "Lint failed — commit aborted."
  exit 1
fi

Exiting with a non-zero status blocks the commit. This is the mechanism behind "can't commit code that fails lint" workflows.

pre-push — a second checkpoint

bash
#!/bin/sh
# .git/hooks/pre-push
npm test

Runs before git push sends commits to a remote — a reasonable place for a slower check (like a full test suite) that would be annoying to run on every single commit but is worth enforcing before code reaches a shared branch.

commit-msg — enforcing message format

bash
#!/bin/sh
# .git/hooks/commit-msg
if ! grep -qE "^(feat|fix|docs|chore|refactor)(\(.+\))?: .+" "$1"; then
  echo "Commit message must follow Conventional Commits format."
  exit 1
fi

Useful for teams that rely on commit message conventions for automated changelog generation or semantic versioning.

The problem with raw hooks: they're not committed to the repo

.git/hooks/ lives inside the .git directory, which isn't tracked by Git itself — so a hook you set up locally doesn't automatically apply to anyone else who clones the repo. This is the main reason raw hooks don't scale to a team by themselves.

Husky — the standard fix

bash
npm install -D husky
npx husky init

Husky stores hook scripts inside the repo (typically in a .husky/ directory) and configures Git to use that directory instead of .git/hooks/ — so hooks get versioned, reviewed, and applied automatically for anyone who runs npm install after cloning.

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

Pairing Husky with lint-staged is the common combination — it runs lint/format only on the files actually staged for commit, rather than the entire codebase, keeping the pre-commit check fast even in large repos.

Where hooks fit vs. CI

Hooks catch problems early, on the developer's own machine, before code is even pushed — faster feedback than waiting for CI. But hooks can be bypassed (git commit --no-verify) and only run on whoever has them installed. CI is the backstop that can't be skipped. The two aren't redundant — hooks are for fast, optional-but-encouraged local feedback; CI is the actual enforcement.

Advertisement
Marcus Lee
Marcus Lee

Cloud & DevOps Engineer

Marcus covers Kubernetes, cloud infrastructure, and CI/CD. He's spent a decade running production systems at scale.

Related Articles