Hooks de Git para automatizar el desarrollo
Cómo usar hooks de Git para automatizar calidad, validar mensajes de commit, ejecutar pruebas pre-push y nombrar ramas, con Husky y scripts propios.

Los hooks de Git son scripts que se ejecutan automáticamente en momentos específicos del flujo de trabajo de Git: antes de un commit, antes de un push, después de un merge. Son la capa de cumplimiento entre "quedó en que íbamos a lintear el código" y "el código realmente está linteado". Sin hooks, las revisiones de calidad dependen de la disciplina de cada desarrollador. Con hooks, son automáticas.
Los hooks más valiosos son pre-commit (valida el código antes de commitear), commit-msg (fuerza el formato del mensaje de commit) y pre-push (ejecuta pruebas antes de hacer push).
Configurar hooks con Husky
Los hooks de Git viven en .git/hooks/, que no se rastrea en el control de versiones. Esto significa que no puedes compartirlos a través del repositorio. Husky resuelve esto al colocar los hooks en un directorio .husky/ que sí se commitea.
# Install Husky
npm install --save-dev husky
# Initialize Husky (creates .husky/ directory)
npx husky init
# This creates .husky/pre-commit with a default script
# and adds "prepare": "husky" to package.json// package.json
{
"scripts": {
"prepare": "husky",
"lint": "eslint . --ext .ts,.tsx",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"devDependencies": {
"husky": "^9.0.0",
"lint-staged": "^15.0.0"
}
}Hook pre-commit: lintear solo los archivos en staging
El hook pre-commit se ejecuta antes de cada commit. La optimización clave es lint-staged: en lugar de lintear toda la base de código, solo se lintean los archivos que se van a commitear.
# .husky/pre-commit
npx lint-staged// lint-staged.config.ts
const config = {
// TypeScript/JavaScript files: lint and format
'*.{ts,tsx}': [
'eslint --fix --max-warnings 0',
'prettier --write',
],
// CSS/SCSS files: format only
'*.{css,scss}': [
'prettier --write',
],
// JSON/YAML files: format
'*.{json,yaml,yml}': [
'prettier --write',
],
// Markdown files: format
'*.md': [
'prettier --write',
],
};
export default config;# ❌ Linting the entire codebase on every commit
# .husky/pre-commit
npm run lint # Takes 30 seconds on a large codebase
npm run format:check # Another 15 seconds
npm run typecheck # Another 20 seconds
# Developers disable the hook because it's too slow
# ✅ Only check the files being committed
# .husky/pre-commit
npx lint-staged # Takes 2-3 seconds — only changed files
# Fast enough that developers never disable itHook commit-msg: forzar Conventional Commits
El hook commit-msg valida el formato del mensaje de commit. Los Conventional Commits (feat:, fix:, chore:, etc.) permiten generar changelogs automáticamente y hacer versionado semántico.
# .husky/commit-msg
npx --no -- commitlint --edit $1// commitlint.config.ts
const config = {
extends: ['@commitlint/config-conventional'],
rules: {
// Type must be one of these
'type-enum': [
2, // Error level
'always',
[
'feat', // New feature
'fix', // Bug fix
'docs', // Documentation only
'style', // Formatting (no code change)
'refactor', // Code change that neither fixes nor adds
'perf', // Performance improvement
'test', // Adding or updating tests
'chore', // Build process or auxiliary tools
'ci', // CI configuration
'revert', // Revert a previous commit
],
],
// Subject must be lowercase
'subject-case': [2, 'always', 'lower-case'],
// Subject must not be empty
'subject-empty': [2, 'never'],
// Subject must not end with period
'subject-full-stop': [2, 'never', '.'],
// Max line length for the header
'header-max-length': [2, 'always', 100],
},
};
export default config;# ❌ Bad commit messages that commit-msg hook rejects
git commit -m "fixed stuff" # No type prefix
git commit -m "feat: Updated the login." # Period at end, uppercase
git commit -m "wip" # No type, not descriptive
# ✅ Good commit messages that pass validation
git commit -m "fix: resolve race condition in payment webhook handler"
git commit -m "feat: add email notification preferences to user settings"
git commit -m "refactor: extract validation logic into shared utility"
git commit -m "docs: add API authentication examples to README"Hook pre-push: ejecutar pruebas antes de hacer push
El hook pre-push se ejecuta antes de que el código se envíe al remoto. Aquí van las comprobaciones más pesadas que serían demasiado lentas para pre-commit: verificación de tipos y suites de pruebas.
# .husky/pre-push
echo "Running type check..."
npx tsc --noEmit
echo "Running tests..."
npx vitest run --reporter=dot
echo "All checks passed. Pushing..."// For monorepos: only run tests for packages that changed
// .husky/pre-push (advanced)
import { execSync } from 'child_process';
function getChangedPackages(): string[] {
// Compare local HEAD with remote
const diff = execSync(
'git diff --name-only @{upstream}...HEAD',
{ encoding: 'utf-8' }
);
const changedFiles = diff.trim().split('\n');
const packages = new Set<string>();
for (const file of changedFiles) {
// Extract package name from path like "packages/auth/src/..."
const match = file.match(/^packages\/([^/]+)\//);
if (match) {
packages.add(match[1]);
}
}
return [...packages];
}
const changed = getChangedPackages();
if (changed.length > 0) {
console.log(`Testing changed packages: ${changed.join(', ')}`);
for (const pkg of changed) {
execSync(`npm run test --workspace=packages/${pkg}`, { stdio: 'inherit' });
}
}Hooks personalizados: nombres de ramas
Más allá de las comprobaciones estándar, puedes hacer cumplir convenciones del equipo como patrones de nomenclatura de ramas.
#!/bin/bash
# .husky/pre-push — branch naming validation
BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Allowed patterns: feat/*, fix/*, chore/*, hotfix/*, release/*
PATTERN="^(feat|fix|chore|hotfix|release|docs|test)\/[a-z0-9._-]+$"
if [[ "$BRANCH" == "main" || "$BRANCH" == "develop" ]]; then
exit 0 # Main branches are always allowed
fi
if [[ ! "$BRANCH" =~ $PATTERN ]]; then
echo "❌ Branch name '$BRANCH' does not match the required pattern."
echo " Use: feat/description, fix/description, chore/description"
echo " Example: feat/add-user-notifications"
exit 1
fi// ❌ Without branch naming enforcement
// Branches in the repo: wip, johns-branch, test123, fix, asdf
// Nobody knows what these branches are for
// ✅ With branch naming enforcement
// Branches: feat/user-notifications, fix/payment-race-condition, chore/update-deps
// Every branch name tells you what it contains and why it existsCómo omitir los hooks (cuando sea necesario)
A veces necesitas saltarte los hooks: durante un hotfix a las 2 AM, o al commitear archivos generados. Git ofrece salidas de emergencia, pero deberían ser raras.
# Skip pre-commit and commit-msg hooks
git commit --no-verify -m "hotfix: emergency patch for production outage"
# Skip pre-push hooks
git push --no-verify
# ⚠️ IMPORTANT: Hooks are a safety net, not a prison.
# If you're using --no-verify regularly, your hooks are too slow or too strict.
# Fix the hooks instead of bypassing them.Conclusiones clave
- Usa
lint-stagedenpre-commit— lintea solo los archivos cambiados para mantener el hook lo suficientemente rápido como para que nadie lo desactive - Fuerza Conventional Commits — los mensajes de commit estructurados permiten generar changelogs automáticos y hacer versionado semántico
- Ejecuta pruebas en
pre-push, no enpre-commit— las comprobaciones más pesadas van antes del push, no antes de cada commit - Husky hace que los hooks sean compartibles —
.husky/se commite en el repositorio, así que cada desarrollador obtiene los mismos hooks - Haz cumplir los nombres de ramas — los patrones consistentes hacen que el propósito de una rama sea inmediatamente claro
- Mantén los hooks rápidos — si un hook tarda más de 5 segundos, los desarrolladores lo omitirán; optímizalo o muévelo a CI


