Setting Up Husky for Git Hooks in Web Projects
Husky allows you to run scripts on git events: before commit, before push, when writing commit messages. Practical application: automatic linter run before each commit prevents broken code from entering the history.
Installation
npm install --save-dev husky
npx husky init
After init, a .husky/ directory is created with a basic pre-commit hook and a "prepare": "husky" script is added to package.json. The prepare script runs automatically on npm install — new team members get hooks without additional steps.
pre-commit Hook
.husky/pre-commit:
npx lint-staged
Runs lint-staged — a tool that applies commands only to staged files, not the whole project. lint-staged configuration in a separate file.
commit-msg Hook
Validates commit message format. Convenient with Conventional Commits:
npm install --save-dev @commitlint/cli @commitlint/config-conventional
.husky/commit-msg:
npx --no -- commitlint --edit $1
commitlint.config.mjs:
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert', 'perf', 'ci'],
],
'subject-max-length': [2, 'always', 72],
'body-max-line-length': [2, 'always', 100],
},
};
Correct commit: feat: add user authentication. Incorrect: added stuff — hook will block.
pre-push Hook
To run tests before push:
.husky/pre-push:
npm run test:ci
npm run typecheck
Tests before push, not before every commit — a compromise between speed and reliability. If tests are slow, pre-commit with tests kills productivity.
prepare-commit-msg Hook
Automatically adds task number from branch name to commit message:
.husky/prepare-commit-msg:
#!/bin/sh
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
# Extract task number from branch name (e.g., feature/PROJ-123-description)
BRANCH_NAME=$(git symbolic-ref --short HEAD 2>/dev/null)
TICKET=$(echo "$BRANCH_NAME" | grep -oE '[A-Z]+-[0-9]+' | head -1)
# Add only if not already present and not merge/rebase
if [ -n "$TICKET" ] && [ "$COMMIT_SOURCE" != "merge" ] && [ "$COMMIT_SOURCE" != "squash" ]; then
CURRENT_MSG=$(cat "$COMMIT_MSG_FILE")
if ! echo "$CURRENT_MSG" | grep -q "$TICKET"; then
echo "[$TICKET] $CURRENT_MSG" > "$COMMIT_MSG_FILE"
fi
fi
Bypassing Hooks in Emergency Cases
git commit --no-verify -m "hotfix: emergency patch"
git push --no-verify
--no-verify skips hooks. This is an intentional feature for emergencies, not a system bypass — it's logged in git history.
CI and Hooks
In CI environments, hooks aren't needed — linting and tests are run explicitly there. Husky automatically skips hook installation if the CI=true variable is set:
# husky doesn't install hooks when CI=true
HUSKY=0 npm ci # or
CI=true npm install
Timeline
Installing Husky with pre-commit + lint-staged: 30–60 minutes. Adding commitlint and setting up Conventional Commits: another 30–60 minutes.







