Pre-Commit Hooks
Skill Purpose: Establish code quality enforcement patterns before commits
Core Skill Pattern
Objective: Set up automated quality checks that run before commits to ensure code standards.
Universal Pattern:
- •Install hook management tools
- •Configure staged file processing
- •Define quality check rules
- •Set up commit message standards
- •Test hook functionality
Key Decisions (Project-Specific):
- •Which tools to use (husky vs alternatives)
- •What file types to process
- •Which quality checks to run
- •Commit message format requirements
Project-Specific Implementation Notes
Customize per project:
- •Tool selection based on team preferences
- •File type rules based on project languages
- •Quality gate strictness based on project maturity
- •Integration with existing CI/CD pipeline
Example Implementation (Next.js/Husky Pattern)
Note: This is an example pattern. Adapt tools and configurations based on your specific project requirements.
Prerequisites (Example)
- •Git repository initialized
- •Package management available
- •Code quality tools configured
Example: Next.js/Husky Implementation Steps
Framework-Specific Example: This demonstrates the pattern using Next.js and husky. Adapt for your tech stack.
1. Install Husky and lint-staged
# Install husky and lint-staged npm install -D husky lint-staged # Install additional hook tools npm install -D @commitlint/cli @commitlint/config-conventional commitizen cz-conventional-changelog # Install file change detection npm install -D onchange
2. Initialize Husky
# Initialize husky npm pkg set scripts.prepare="husky install" npm run prepare # Create husky directory mkdir -p .husky
3. Configure lint-staged
Create .lintstagedrc.json:
{
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{json,md,yml,yaml}": [
"prettier --write",
"git add"
],
"*.{css,scss,less}": [
"prettier --write",
"git add"
],
"*.{html,xml}": [
"prettier --write",
"git add"
],
"package.json": [
"prettier --write",
"git add"
],
"*.md": [
"prettier --write",
"markdownlint --fix",
"git add"
]
}
Create .lintstagedrc.js (alternative JavaScript config):
module.exports = {
'*.{js,jsx,ts,tsx}': [
(filenames) => {
// Run ESLint only on changed files
return `eslint --fix ${filenames.map(f => `"${f}"`).join(' ')}`;
},
(filenames) => {
// Run Prettier only on changed files
return `prettier --write ${filenames.map(f => `"${f}"`).join(' ')}`;
},
],
'*.{json,md,yml,yaml}': [
'prettier --write',
],
'*.{css,scss,less}': [
'prettier --write',
],
'*.md': [
'prettier --write',
'markdownlint --fix',
],
// Custom patterns
'src/**/*.{ts,tsx}': [
'npm run type-check -- --noEmit',
],
// Ignore certain files
'!**/node_modules/**',
'!**/dist/**',
'!**/build/**',
'!**/.next/**',
'!**/coverage/**',
};
4. Create Pre-Commit Hook
Create .husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🔒 Running pre-commit hooks..."
# Run lint-staged
npx lint-staged
# Check for console.log statements
echo "🔍 Checking for console.log statements..."
if git diff --cached --name-only | xargs grep -l "console\.log\|console\.warn" 2>/dev/null; then
echo "❌ Found console.log or console.warn statements in staged files."
echo " Please remove them before committing."
echo ""
echo "Files with console statements:"
git diff --cached --name-only | xargs grep -l "console\.log\|console\.warn" 2>/dev/null
exit 1
fi
# Check for TODO/FIXME comments
echo "🔍 Checking for TODO/FIXME comments..."
if git diff --cached --name-only | xargs grep -l "TODO\|FIXME\|HACK\|XXX" 2>/dev/null; then
echo "⚠️ Found TODO/FIXME/HACK/XXX comments in staged files."
echo " Please review them before committing."
echo ""
echo "Files with TODO comments:"
git diff --cached --name-only | xargs grep -l "TODO\|FIXME\|HACK\|XXX" 2>/dev/null
echo ""
echo "Continue anyway? (y/n)"
read -r response
if [ "$response" != "y" ]; then
exit 1
fi
fi
# Check file sizes
echo "🔍 Checking file sizes..."
MAX_FILE_SIZE=1048576 # 1MB
large_files=$(git diff --cached --name-only | xargs du -b 2>/dev/null | awk -v max="$MAX_FILE_SIZE" '$1 > max { print $2 }')
if [ -n "$large_files" ]; then
echo "⚠️ Found large files in staged changes:"
echo "$large_files" | while read -r file; do
size=$(du -h "$file" | cut -f1)
echo " $file ($size)"
done
echo ""
echo "Consider removing large files from commits."
fi
# Run type checking on TypeScript files
echo "🔍 Running TypeScript type checking..."
ts_files=$(git diff --cached --name-only | grep -E '\.(ts|tsx)$')
if [ -n "$ts_files" ]; then
if ! npm run type-check -- --noEmit; then
echo "❌ TypeScript type checking failed."
exit 1
fi
fi
# Check for missing imports
echo "🔍 Checking for missing imports..."
if git diff --cached --name-only | grep -E '\.(js|jsx|ts|tsx)$' | xargs grep -l "import.*from.*['\"]\s*$" 2>/dev/null; then
echo "⚠️ Some files may have incomplete imports. Please review."
fi
# Validate package.json if changed
if git diff --cached --name-only | grep -q "package.json"; then
echo "🔍 Validating package.json..."
if ! npm run validate-package; then
echo "❌ package.json validation failed."
exit 1
fi
fi
echo "✅ Pre-commit hooks passed!"
5. Create Pre-Push Hook
Create .husky/pre-push:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🚀 Running pre-push hooks..."
# Run full test suite
echo "🧪 Running test suite..."
if ! npm run test:ci; then
echo "❌ Tests failed."
exit 1
fi
# Run build
echo "🏗️ Running build..."
if ! npm run build; then
echo "❌ Build failed."
exit 1
fi
# Run security audit
echo "🔒 Running security audit..."
if npm audit --audit-level moderate; then
echo "✅ Security audit passed."
else
echo "⚠️ Security audit found issues."
echo " Consider running 'npm audit fix'"
echo ""
echo "Continue anyway? (y/n)"
read -r response
if [ "$response" != "y" ]; then
exit 1
fi
fi
# Check for uncommitted changes
echo "🔍 Checking for uncommitted changes..."
if [ -n "$(git status --porcelain)" ]; then
echo "⚠️ You have uncommitted changes."
echo " Consider committing them before pushing."
echo ""
echo "Uncommitted files:"
git status --porcelain
echo ""
echo "Continue anyway? (y/n)"
read -r response
if [ "$response" != "y" ]; then
exit 1
fi
fi
# Check if current branch is up to date
echo "🔍 Checking if branch is up to date..."
current_branch=$(git branch --show-current)
if git fetch origin "$current_branch" 2>/dev/null; then
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/$current_branch)" ]; then
echo "⚠️ Your branch is not up to date with origin/$current_branch."
echo " Consider pulling latest changes before pushing."
echo ""
echo "Continue anyway? (y/n)"
read -r response
if [ "$response" != "y" ]; then
exit 1
fi
fi
fi
echo "✅ Pre-push hooks passed!"
6. Create Commit Message Hook
Create .husky/commit-msg:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "📝 Validating commit message..."
# Check commit message format
commit_regex='^(feat|fix|docs|style|refactor|test|chore|perf|breaking|ci|build|revert)(\(.+\))?: .{1,50}'
if ! grep -qE "$commit_regex" "$1"; then
echo "❌ Invalid commit message format!"
echo ""
echo "Commit message must follow the Conventional Commits format:"
echo " type(scope): description"
echo ""
echo "Types: feat, fix, docs, style, refactor, test, chore, perf, breaking, ci, build, revert"
echo "Scope: optional, in parentheses"
echo "Description: imperative mood, no period, max 50 characters"
echo ""
echo "Examples:"
echo " feat(auth): add user authentication"
echo " fix(api): resolve null reference error"
echo " docs: update API documentation"
echo " refactor(components): extract common button logic"
echo ""
echo "Please rewrite your commit message and try again."
exit 1
fi
# Check message length
message_length=$(wc -m < "$1")
if [ "$message_length" -gt 72 ]; then
echo "⚠️ Commit message is longer than 72 characters."
echo " Consider shortening it for better readability."
echo ""
echo "Continue anyway? (y/n)"
read -r response
if [ "$response" != "y" ]; then
exit 1
fi
fi
# Check for proper capitalization
if grep -qE "^[a-z]" "$1"; then
echo "✅ Commit message starts with lowercase (conventional)"
elif grep -qE "^[A-Z]" "$1"; then
echo "⚠️ Commit message starts with uppercase."
echo " Conventional commits typically start with lowercase."
echo ""
echo "Continue anyway? (y/n)"
read -r response
if [ "$response" != "y" ]; then
exit 1
fi
fi
echo "✅ Commit message validation passed!"
7. Configure Commitlint
Create commitlint.config.js:
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
// Type rules
'type-enum': [
2,
'always',
[
'feat', // New feature
'fix', // Bug fix
'docs', // Documentation
'style', // Code style (formatting, etc.)
'refactor', // Refactoring
'test', // Tests
'chore', // Maintenance tasks
'perf', // Performance improvements
'breaking', // Breaking changes
'ci', // CI/CD changes
'build', // Build system changes
'revert', // Revert previous commit
],
],
'type-empty': [2, 'never'],
'type-case': [2, 'always', 'lower-case'],
// Scope rules
'scope-empty': [0, 'never'], // Allow empty scope
'scope-case': [2, 'always', 'lower-case'],
'scope-enum': [
0,
'always',
[
'auth', // Authentication
'api', // API
'ui', // User interface
'db', // Database
'config', // Configuration
'deps', // Dependencies
'docs', // Documentation
'tests', // Tests
'utils', // Utilities
'types', // TypeScript types
'hooks', // React hooks
'components', // React components
'pages', // Next.js pages
'layouts', // Layouts
'styles', // Styles
'scripts', // Scripts
'build', // Build
'deploy', // Deployment
],
],
// Subject rules
'subject-empty': [2, 'never'],
'subject-case': [2, 'never', ['lower-case', 'sentence-case']],
'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [2, 'always', 50],
// Body rules
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 72],
// Footer rules
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 72],
// General rules
'header-max-length': [2, 'always', 72],
},
};
8. Configure Commitizen
Create .czrc:
{
"path": "cz-conventional-changelog",
"types": {
"feat": {
"description": "New feature",
"title": "Features"
},
"fix": {
"description": "Bug fix",
"title": "Bug Fixes"
},
"docs": {
"description": "Documentation",
"title": "Documentation"
},
"style": {
"description": "Code style (formatting, etc.)",
"title": "Styles"
},
"refactor": {
"description": "Refactoring",
"title": "Code Refactoring"
},
"test": {
"description": "Tests",
"title": "Tests"
},
"chore": {
"description": "Maintenance tasks",
"title": "Chores"
},
"perf": {
"description": "Performance improvements",
"title": "Performance Improvements"
},
"breaking": {
"description": "Breaking changes",
"title": "Breaking Changes"
},
"ci": {
"description": "CI/CD changes",
"title": "Continuous Integration"
},
"build": {
"description": "Build system changes",
"title": "Build System"
},
"revert": {
"description": "Revert previous commit",
"title": "Reverts"
}
},
"scopes": {
"auth": "Authentication",
"api": "API",
"ui": "User Interface",
"db": "Database",
"config": "Configuration",
"deps": "Dependencies",
"docs": "Documentation",
"tests": "Tests",
"utils": "Utilities",
"types": "TypeScript Types",
"hooks": "React Hooks",
"components": "React Components",
"pages": "Next.js Pages",
"layouts": "Layouts",
"styles": "Styles",
"scripts": "Scripts",
"build": "Build",
"deploy": "Deployment"
}
}
9. Create Custom Hook Scripts
Create scripts/setup-hooks.js:
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// ANSI color codes
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
magenta: '\x1b[35m',
};
function colorLog(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function createHookFile(hookName, content) {
const hookPath = path.join(process.cwd(), '.husky', hookName);
try {
fs.writeFileSync(hookPath, content, { mode: 0o755 });
colorLog(`✅ Created ${hookName}`, 'green');
return true;
} catch (error) {
colorLog(`❌ Failed to create ${hookName}`, 'red');
colorLog(error.message, 'red');
return false;
}
}
function setupHusky() {
colorLog('🔧 Setting up Husky hooks', 'magenta');
colorLog('=========================', 'magenta');
// Ensure .husky directory exists
const huskyDir = path.join(process.cwd(), '.husky');
if (!fs.existsSync(huskyDir)) {
fs.mkdirSync(huskyDir, { recursive: true });
colorLog('✅ Created .husky directory', 'green');
}
// Create hooks
const hooks = [
{
name: 'pre-commit',
content: `#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🔒 Running pre-commit hooks..."
# Run lint-staged
npx lint-staged
# Check for console.log statements
echo "🔍 Checking for console.log statements..."
if git diff --cached --name-only | xargs grep -l "console\\.log\\|console\\.warn" 2>/dev/null; then
echo "❌ Found console.log or console.warn statements in staged files."
echo " Please remove them before committing."
exit 1
fi
echo "✅ Pre-commit hooks passed!"
`,
},
{
name: 'pre-push',
content: `#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "🚀 Running pre-push hooks..."
# Run test suite
echo "🧪 Running test suite..."
if ! npm run test:ci; then
echo "❌ Tests failed."
exit 1
fi
# Run build
echo "🏗️ Running build..."
if ! npm run build; then
echo "❌ Build failed."
exit 1
fi
echo "✅ Pre-push hooks passed!"
`,
},
{
name: 'commit-msg',
content: `#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "📝 Validating commit message..."
commit_regex='^(feat|fix|docs|style|refactor|test|chore|perf|breaking|ci|build|revert)(\\(.+\\))?: .{1,50}'
if ! grep -qE "$commit_regex" "$1"; then
echo "❌ Invalid commit message format!"
echo "Commit message must follow: type(scope): description"
exit 1
fi
echo "✅ Commit message validation passed!"
`,
},
];
let successCount = 0;
for (const hook of hooks) {
if (createHookFile(hook.name, hook.content)) {
successCount++;
}
}
colorLog(`\n📊 Setup Summary: ${successCount}/${hooks.length} hooks created`,
successCount === hooks.length ? 'green' : 'yellow');
return successCount === hooks.length;
}
function installDependencies() {
colorLog('\n📦 Installing dependencies...', 'blue');
try {
execSync('npm install --save-dev husky lint-staged @commitlint/cli @commitlint/config-conventional', { stdio: 'inherit' });
colorLog('✅ Dependencies installed', 'green');
return true;
} catch (error) {
colorLog('❌ Failed to install dependencies', 'red');
return false;
}
}
function configurePackageJson() {
colorLog('\n📝 Configuring package.json...', 'blue');
try {
// Read package.json
const packageJsonPath = path.join(process.cwd(), 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Add scripts
packageJson.scripts = {
...packageJson.scripts,
'prepare': 'husky install',
'commit': 'git-cz',
};
// Add lint-staged config
packageJson['lint-staged'] = {
'*.{js,jsx,ts,tsx}': [
'eslint --fix',
'prettier --write',
],
'*.{json,md,yml,yaml}': [
'prettier --write',
],
};
// Write back package.json
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
colorLog('✅ package.json configured', 'green');
return true;
} catch (error) {
colorLog('❌ Failed to configure package.json', 'red');
return false;
}
}
function main() {
colorLog('🚀 Setting up Git hooks for Zeus framework', 'magenta');
colorLog('======================================', 'magenta');
let allSuccess = true;
// Install dependencies
if (!installDependencies()) {
allSuccess = false;
}
// Configure package.json
if (!configurePackageJson()) {
allSuccess = false;
}
// Setup Husky hooks
if (!setupHusky()) {
allSuccess = false;
}
// Initialize husky
try {
execSync('npm run prepare', { stdio: 'inherit' });
colorLog('✅ Husky initialized', 'green');
} catch (error) {
colorLog('❌ Failed to initialize Husky', 'red');
allSuccess = false;
}
colorLog('\n📊 Setup Summary', 'magenta');
colorLog('================', 'magenta');
if (allSuccess) {
colorLog('✅ Git hooks setup completed successfully!', 'green');
colorLog('\nNext steps:', 'cyan');
colorLog('1. Commit your changes to include the hooks', 'cyan');
colorLog('2. Try making a commit to test the hooks', 'cyan');
colorLog('3. Use "npm run commit" for guided commits', 'cyan');
} else {
colorLog('❌ Some setup steps failed. Please check the errors above.', 'red');
}
}
if (require.main === module) {
main();
}
module.exports = { setupHusky, installDependencies, configurePackageJson };
10. Update Package.json Scripts
Update package.json scripts:
{
"scripts": {
"prepare": "husky install",
"commit": "git-cz",
"hooks:setup": "node scripts/setup-hooks.js",
"hooks:test": "npx lint-staged --debug",
"hooks:uninstall": "npm uninstall husky"
}
}
Code Examples
Using Commitizen for Guided Commits
# Interactive commit creation npm run commit # Or directly git-cz
Manual Hook Testing
# Test pre-commit hooks git add . git commit -m "feat: test commit" # Test pre-push hooks git push origin main # Test commit message validation git commit -m "invalid commit message"
Custom Hook Configuration
// .lintstagedrc.js - Advanced configuration
module.exports = {
'*.{js,jsx,ts,tsx}': [
// Run ESLint with specific rules for staged files
(filenames) => `eslint --fix --max-warnings 0 ${filenames.join(' ')}`,
// Run Prettier
'prettier --write',
// Run TypeScript check only on TypeScript files
(filenames) => {
const tsFiles = filenames.filter(f => f.match(/\.(ts|tsx)$/));
if (tsFiles.length > 0) {
return `npx tsc --noEmit ${tsFiles.join(' ')}`;
}
},
],
// Parallel execution for different file types
'*.{json,md,yml,yaml}': ['prettier --write'],
'*.{css,scss,less}': ['prettier --write'],
// Custom commands
'package.json': [
'npm run validate-package',
'prettier --write',
],
// Ignore patterns
'!**/node_modules/**',
'!**/dist/**',
'!**/.next/**',
};
Hook Debugging
# Debug lint-staged npx lint-staged --debug # Test specific files npx lint-staged --verbose src/components/Button.tsx # Bypass hooks (not recommended) git commit --no-verify -m "feat: bypass hooks"
Configuration Templates
Complete .lintstagedrc.json
{
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
],
"*.{css,scss,less}": [
"prettier --write"
],
"*.md": [
"prettier --write",
"markdownlint --fix"
],
"package.json": [
"prettier --write",
"npm run validate-package"
],
"src/**/*.{ts,tsx}": [
"npm run type-check -- --noEmit"
]
}
Complete commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf', 'breaking', 'ci', 'build', 'revert'],
],
'type-empty': [2, 'never'],
'type-case': [2, 'always', 'lower-case'],
'scope-empty': [0, 'never'],
'scope-case': [2, 'always', 'lower-case'],
'subject-empty': [2, 'never'],
'subject-case': [2, 'never', ['lower-case', 'sentence-case']],
'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [2, 'always', 50],
'header-max-length': [2, 'always', 72],
},
};
Best Practices
- •Use conventional commits - Standardized message format
- •Configure proper file patterns - Efficient lint-staged execution
- •Use commitizen - Guided commit creation
- •Test hooks before deployment - Ensure they work correctly
- •Use appropriate validation levels - Balance speed and quality
- •Document hook behavior - Team understanding
- •Use bypass sparingly --no-verify only when necessary
- •Monitor hook performance - Don't slow down development
Stop Conditions
STOP and report if:
- •Husky installation fails
- •Hook scripts don't execute
- •lint-staged configuration errors
- •Commit message validation fails
- •Pre-push hooks break workflow
Expected Outcomes:
- •Husky installed and configured
- •All hooks functional
- •lint-staged working correctly
- •Commit messages validated
- •Code quality enforced
Verification Checklist
- • Husky installed successfully
- • All hook scripts created and executable
- • lint-staged configured correctly
- • Commitlint configuration working
- • Commitizen configured
- • Pre-commit hooks functional
- • Pre-push hooks working
- • Commit message validation active
- • Package.json scripts updated
- • Hook setup script functional
Version: 1.0.0 Last Updated: 2026-01-31 Skill Category: Architecture - CI