Automate Code Quality Checks with Git Hooks
Run linters, tests, and formatters automatically before every commit or push — without relying on anyone remembering to do it manually.
Git hooks are scripts that run automatically at specific points in the Git workflow. A pre-commit hook that runs your linter means a commit with failing checks is physically impossible, not just discouraged. No CI pipeline failure, no "oh I forgot to lint" — the problem never leaves the developer's machine.
Where hooks live
Every Git repository has a .git/hooks/ directory with sample scripts:
ls .git/hooks/
applypatch-msg.sample commit-msg.sample pre-commit.sample ...
To activate a hook, create a file with the exact hook name (no .sample extension) and make it executable. The script can be any executable — bash, Python, Node.
A pre-commit hook
The most useful hook. Runs before the commit is created. A non-zero exit code aborts the commit.
# .git/hooks/pre-commit
#!/bin/sh
set -e
echo "Running linter..."
npm run lint
echo "Running type check..."
npm run typecheck
Make it executable:
chmod +x .git/hooks/pre-commit
Now every git commit runs the linter and type checker first. If either fails, the commit is blocked and the output tells you exactly what is wrong.
A commit-msg hook
Validates the commit message format. Useful for enforcing Conventional Commits or JIRA ticket references:
# .git/hooks/commit-msg
#!/bin/sh
commit_msg=$(cat "$1")
pattern="^(feat|fix|docs|chore|refactor|test|style)(\(.+\))?: .{1,72}"
if ! echo "$commit_msg" | grep -qE "$pattern"; then
echo "Error: commit message does not follow Conventional Commits format."
echo "Expected: type(scope): description"
echo "Example: feat(auth): add OAuth2 login support"
exit 1
fi
$1 is the path to the file containing the commit message. The script reads it and validates the format. A rejected commit returns the developer to their editor to fix the message.
A pre-push hook
Runs before git push. Useful for running the full test suite, which may be too slow to run on every commit:
# .git/hooks/pre-push
#!/bin/sh
set -e
echo "Running tests before push..."
npm test
The developer can still commit freely, but cannot push broken code.
The problem: hooks do not sync with the repo
.git/hooks/ is not tracked by version control. If you add a hook, a teammate cloning the repo does not get it. Two solutions:
Option 1: Store hooks in the repo and configure Git to use them
Create a hooks/ directory in your project root (tracked by Git) and configure Git to look there:
mkdir hooks
git config core.hooksPath hooks
Or set it globally for all repos:
git config --global core.hooksPath ~/.git-hooks
Add your hooks to hooks/pre-commit, commit the directory, and everyone who runs git config core.hooksPath hooks gets them. Document this step in your README.
Option 2: Use a hook manager
Tools like husky (Node.js ecosystem) or pre-commit (Python, works with any language) manage hooks as part of the project's dependency setup:
npm install --save-dev husky
npx husky init
Hooks become part of package.json and run automatically when a developer installs dependencies. The trade-off: one more tool in the project.
Bypass a hook when necessary
Hooks can be bypassed with --no-verify:
git commit --no-verify -m "WIP: temporary hack"
git push --no-verify
This is an escape hatch for legitimate situations (committing broken work to a personal branch, committing generated files that fail the linter). It should be used deliberately, not as a habit. If teammates are using --no-verify regularly, the hooks are probably too strict or too slow.
Useful patterns
Run only on staged files (much faster than linting everything):
# .git/hooks/pre-commit
#!/bin/sh
staged=$(git diff --cached --name-only --diff-filter=ACM | grep '\.js$')
if [ -n "$staged" ]; then
echo "$staged" | xargs npx eslint
fi
Format staged files automatically and re-add them:
# .git/hooks/pre-commit
#!/bin/sh
staged=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
if [ -n "$staged" ]; then
echo "$staged" | xargs black
echo "$staged" | xargs git add
fi
Auto-formatting and re-adding means the developer never sees a "formatted files need to be staged" failure — the hook handles it silently.
SysEmperor