Skip to content

Git Hooks & Automation 🪝

Git hooks are scripts that run automatically at specific points in your Git workflow — before a commit, before a push, or after a merge. By combining Git hooks with tools like Husky, lint-staged, and commitlint, you can automate code quality enforcement so that every commit is linted, formatted, and follows your team’s conventions.

Git hooks are scripts stored in the .git/hooks/ directory of a repository. They run automatically when triggered by Git operations:

HookTriggerCommon Use
pre-commitBefore a commit is createdLint and format staged files
commit-msgAfter commit message is enteredValidate commit message format
pre-pushBefore pushing to remoteRun tests
post-mergeAfter a merge completesRun npm install
post-checkoutAfter switching branchesRun npm install

The challenge is that .git/hooks/ is not tracked by Git, so sharing hooks across a team requires a tool like Husky.

Husky makes Git hooks easy to manage and share with your team.

Terminal window
npx husky init

This command:

  1. Installs husky as a dev dependency
  2. Creates a .husky/ directory
  3. Adds a prepare script to package.json that runs husky on npm install
  4. Creates a sample pre-commit hook

Your package.json now includes:

{
"scripts": {
"prepare": "husky"
}
}

Running linters on your entire codebase before every commit is slow. lint-staged solves this by running tools only on files that are staged for commit.

Terminal window
npm install --save-dev lint-staged

Add the configuration to package.json:

{
"lint-staged": {
"*.{ts,html}": [
"eslint --fix"
],
"*.{ts,html,scss,json,md}": [
"prettier --write"
]
}
}

Edit .husky/pre-commit:

Terminal window
npx lint-staged

Now when you run git commit:

  1. lint-staged identifies files staged for commit
  2. ESLint runs on .ts and .html files with auto-fix
  3. Prettier formats all supported file types
  4. If any tool reports an unfixable error, the commit is aborted
  5. Fixed files are automatically re-staged
Terminal window
# Stage your changes
git add src/app/user/user.component.ts
# Commit — lint-staged runs automatically
git commit -m "feat: add user component"
# Output:
# ✔ Preparing lint-staged...
# ✔ Running tasks for staged files...
# ✔ *.{ts,html} — eslint --fix
# ✔ *.{ts,html,scss,json,md} — prettier --write
# ✔ Applying modifications from tasks...
# ✔ Cleaning up...
# [main abc1234] feat: add user component

Consistent commit messages make your Git history readable and enable automated changelogs.

The Conventional Commits specification defines a standard format:

<type>(<scope>): <subject>
<body>
<footer>

Types:

TypeDescription
featNew feature
fixBug fix
docsDocumentation changes
styleFormatting (no code change)
refactorCode change that neither fixes nor adds
perfPerformance improvement
testAdding or fixing tests
buildBuild system or dependency changes
ciCI/CD configuration changes
choreOther changes (tooling, etc.)

Examples:

Terminal window
feat(auth): add login with Google OAuth
fix(user-profile): prevent crash when avatar URL is null
docs(readme): update installation instructions
refactor(api): replace callbacks with async/await
perf(dashboard): lazy load chart library

commitlint validates commit messages against the Conventional Commits format.

Terminal window
npm install --save-dev @commitlint/cli @commitlint/config-conventional

Create commitlint.config.js in your project root:

module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'build', 'ci', 'chore', 'revert',
],
],
'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']],
'subject-max-length': [2, 'always', 72],
'body-max-line-length': [1, 'always', 100],
},
};

Create the hook file .husky/commit-msg:

Terminal window
npx --no -- commitlint --edit $1
Terminal window
# ❌ Rejected — no type prefix
git commit -m "add login feature"
# ⧗ input: add login feature
# ✖ subject may not be empty [subject-empty]
# ✖ type may not be empty [type-empty]
# ❌ Rejected — wrong type
git commit -m "feature: add login"
# ✖ type must be one of [feat, fix, docs, ...] [type-enum]
# ✅ Accepted
git commit -m "feat(auth): add login feature"

Run tests before pushing to catch issues early:

Create .husky/pre-push:

Terminal window
npx ng test --no-watch --code-coverage

Here’s the full setup for a new Angular project:

Terminal window
npm install --save-dev husky lint-staged @commitlint/cli @commitlint/config-conventional prettier
Terminal window
npx husky init

Add to package.json:

{
"lint-staged": {
"*.{ts,html}": [
"eslint --fix"
],
"*.{ts,html,scss,json,md}": [
"prettier --write"
]
}
}

Update .husky/pre-commit:

Terminal window
npx lint-staged

Create .husky/commit-msg:

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

Create .husky/pre-push:

Terminal window
npx ng test --no-watch

Create commitlint.config.js:

module.exports = {
extends: ['@commitlint/config-conventional'],
};
Terminal window
# Test pre-commit hook
echo "test" >> README.md
git add README.md
git commit -m "test: verify git hooks"
# Should pass lint-staged and commitlint
# Test commit message validation
git commit --allow-empty -m "bad message"
# Should fail commitlint

Git hooks enforce quality locally, but CI/CD is the final safety net. Your CI should run the same checks:

name: Quality Checks
on: [pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Needed for commitlint
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- run: npm ci
# Validate commit messages
- name: Validate commits
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to HEAD
# Lint
- name: Lint
run: npx ng lint
# Format check
- name: Check formatting
run: npx prettier --check .
# Test
- name: Test
run: npx ng test --no-watch --code-coverage