Skip to content

Git Hooks

Git hooks are scripts that run automatically at specific points in the Git workflow — before a commit, after a merge, before a push, and more. They’re stored in .git/hooks/.

HookTriggerCommon Uses
pre-commitBefore committingRun linter, format code, check for secrets
commit-msgAfter commit message is writtenValidate message format
prepare-commit-msgBefore message editor opensPrepend issue number
post-commitAfter commit is createdNotify, trigger CI locally
pre-pushBefore pushRun tests
pre-rebaseBefore rebaseSafety checks

Hooks are executable scripts in .git/hooks/. Create pre-commit:

.git/hooks/pre-commit
#!/bin/sh
# Run ESLint on staged TypeScript files
STAGED=$(git diff --cached --name-only --diff-filter=ACM | grep '\.ts$')
if [ -n "$STAGED" ]; then
npx eslint $STAGED
if [ $? -ne 0 ]; then
echo "ESLint failed — fix errors before committing"
exit 1
fi
fi
Terminal window
# Make it executable
chmod +x .git/hooks/pre-commit

The problem with raw Git hooks: .git/hooks/ isn’t committed. Husky stores hooks in the repo and registers them via npm prepare.

Terminal window
npm install -D husky
npx husky init

This creates a .husky/ directory and a prepare script in package.json.

Only lint the files that are staged (not the whole project):

Terminal window
npm install -D lint-staged

.husky/pre-commit:

Terminal window
npx lint-staged

package.json:

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

commit-msg — Enforce Conventional Commits

Section titled “commit-msg — Enforce Conventional Commits”
Terminal window
npm install -D @commitlint/cli @commitlint/config-conventional

commitlint.config.js:

export default { extends: ['@commitlint/config-conventional'] };

.husky/commit-msg:

Terminal window
npx --no -- commitlint --edit $1

Now commit messages must follow the format:

feat: add user authentication
fix: resolve null pointer in payment
chore: upgrade dependencies
docs: update API reference

.husky/pre-push:

#!/bin/sh
npm test
Terminal window
# Skip hooks for a commit (use sparingly — breaks CI guarantees)
git commit --no-verify -m "Emergency fix"
git push --no-verify
#!/bin/sh
# .husky/pre-commit — find leftover console.log / debugger
if git diff --cached | grep -E '^\+.*(console\.log|debugger)' > /dev/null; then
echo "Found console.log or debugger in staged changes."
exit 1
fi
.husky/prepare-commit-msg
#!/bin/sh
BRANCH=$(git symbolic-ref --short HEAD)
JIRA=$(echo $BRANCH | grep -oE 'PROJ-[0-9]+')
if [ -n "$JIRA" ]; then
sed -i "1s/^/$JIRA: /" "$1"
fi

A commit message specification:

<type>(<scope>): <description>
[optional body]
[optional footer]

Types: feat, fix, docs, style, refactor, test, chore, perf

Benefits:

  • Automated changelog generation
  • Semantic version bumping
  • Clear commit history
  • Better PR reviews