Automatiza tu flujo de trabajo: Una guía práctica para los hooks del lado del cliente en Git

Usa los hooks del lado del cliente de Git para verificaciones locales rápidas, configuración compartida, reglas de mensajes de commit y automatización post-merge más segura.

Automatiza tu flujo de trabajo: Una guía práctica para los hooks del lado del cliente en Git

Los hooks del lado del cliente de Git son pequeños scripts que se ejecutan en tu máquina cuando Git alcanza ciertos puntos en un flujo de trabajo. Un hook pre-commit se ejecuta antes de que se cree un commit. Un hook commit-msg se ejecuta después de que escribes el mensaje pero antes de que Git lo acepte. Un hook post-merge se ejecuta después de que finaliza una fusión. Usados correctamente, los hooks detectan errores aburridos temprano: formato olvidado, archivos generados rotos, instalaciones de dependencias faltantes o mensajes de commit que no coinciden con la convención de tu equipo.

La limitación importante es que los hooks del lado del cliente son locales. No viajan automáticamente con el repositorio cuando alguien lo clona. Esto los hace excelentes para retroalimentación rápida y conveniencia local, pero débiles como la única capa de aplicación para una regla de equipo. Si una verificación realmente protege la rama principal, ponla también en CI o en una regla del lado del servidor.

Cada repositorio tiene un directorio de hooks bajo .git/hooks:

ls .git/hooks

Un repositorio nuevo generalmente contiene archivos de muestra como pre-commit.sample. Un hook de muestra no hace nada hasta que creas un archivo ejecutable sin el sufijo .sample:

cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Los hooks pueden ser scripts de shell, scripts de Python, scripts de Ruby, scripts de Node o cualquier otra cosa que tu máquina pueda ejecutar. La primera línea debe apuntar al intérprete:

#!/usr/bin/env bash

Para la mayoría de los equipos, el mejor patrón a largo plazo no es editar manualmente .git/hooks en cada portátil. Almacena los scripts de hook en el repositorio, luego configura Git para usar ese directorio:

git config core.hooksPath .githooks
mkdir -p .githooks

Ahora un hook en .githooks/pre-commit puede ser confirmado y revisado como código normal del proyecto. Cada desarrollador todavía necesita la configuración core.hooksPath, pero la configuración se puede agregar a un script de arranque o documentarse en la incorporación.

Un Hook Pre-Commit Útil

Un buen hook pre-commit debe ser rápido y enfocado. Si toma dos minutos en cada commit, la gente lo evitará con git commit --no-verify, y el hook se convertirá en ruido. Guarda las suites de prueba completas para CI a menos que el proyecto sea lo suficientemente pequeño como para que realmente sean rápidas.

Aquí hay un hook de shell práctico que verifica solo los archivos preparados. Esa distinción importa. Puede que tengas trabajo sin terminar en tu árbol de trabajo que no quieras probar todavía. El commit debe ser juzgado por lo que está preparado.

Crea .githooks/pre-commit:

#!/usr/bin/env bash
set -u

changed_files=$(git diff --cached --name-only --diff-filter=ACMR)

if [ -z "$changed_files" ]; then
  exit 0
fi

if git diff --cached --check; then
  :
else
  echo "Corrige los errores de espacios en blanco antes de confirmar."
  exit 1
fi

secret_matches=$(git diff --cached --name-only --diff-filter=ACMR | xargs grep -nE 'AKIA[0-9A-Z]{16}|BEGIN RSA PRIVATE KEY' 2>/dev/null || true)
if [ -n "$secret_matches" ]; then
  echo "Posible secreto encontrado en archivos preparados:"
  echo "$secret_matches"
  exit 1
fi

python_files=$(printf '%s\n' "$changed_files" | grep '\.py$' || true)
if [ -n "$python_files" ]; then
  printf '%s\n' "$python_files" | while IFS= read -r file; do
    [ -f "$file" ] || continue
    python3 -m py_compile "$file" || exit 1
  done
fi

exit 0

Este hook hace tres cosas modestas: permite que Git detecte errores de espacios en blanco, verifica los archivos preparados para un par de patrones de secretos obvios y compila los archivos Python modificados. No es un reemplazo para un escáner de secretos real o una suite de pruebas. Es un cable de aviso rápido.

Un error común es usar grep contra nombres de archivo en lugar de contenidos de archivo. Este patrón roto solo verifica si la ruta contiene TODO, no si el archivo lo contiene:

git diff --cached --name-only | grep TODO

Si quieres bloquear comentarios TODO, inspecciona el diff preparado en su lugar:

if git diff --cached -U0 | grep -E '^\+.*TODO:'; then
  echo "Comentarios TODO preparados encontrados."
  exit 1
fi

Incluso entonces, ten cuidado. Algunos equipos usan comentarios TODO de manera responsable. Bloquear cada TODO puede ser más molesto que útil.

Hooks de Mensaje de Commit

Un hook commit-msg recibe la ruta al archivo de mensaje de commit temporal como su primer argumento. Esto lo hace útil para reglas como "cada commit debe comenzar con un ID de ticket" o "usa Commits Convencionales".

Un pequeño ejemplo:

#!/usr/bin/env bash
set -u

message_file="$1"
first_line=$(head -n 1 "$message_file")

if printf '%s' "$first_line" | grep -Eq '^(feat|fix|docs|test|refactor|chore)(\(.+\))?: .+'; then
  exit 0
fi

echo "El mensaje de commit debería verse así: fix(api): handle empty token"
exit 1

Esto es útil cuando las notas de lanzamiento o los registros de cambios se generan a partir de los commits. Es menos útil cuando tu equipo hace fusiones squash y reescribe los títulos de PR de todos modos. Adapta el hook al flujo de trabajo que realmente usas.

Hooks Post-Merge

Un hook post-merge es mejor para la limpieza local después de que tu árbol de trabajo cambia. El ejemplo clásico es actualizar las dependencias después de que un archivo de bloqueo cambia.

#!/usr/bin/env bash
set -u

previous_head="HEAD@{1}"

if git diff --name-only "$previous_head" HEAD | grep -Eq '(^package-lock\.json$|^pnpm-lock\.yaml$|^yarn\.lock$)'; then
  if command -v npm >/dev/null 2>&1 && [ -f package-lock.json ]; then
    echo "Lockfile cambiado; ejecutando npm install."
    npm install
  fi
fi

if git diff --name-only "$previous_head" HEAD | grep -q '^\.gitmodules$'; then
  echo "Configuración de submódulo cambiada; sincronizando submódulos."
  git submodule sync --recursive
  git submodule update --init --recursive
fi

Este hook no debe hacer cambios sorprendentes. Si instala dependencias, imprime lo que está haciendo. Si la instalación falla, dile al desarrollador cómo recuperarse. Un hook que cambia silenciosamente el árbol de trabajo es difícil de confiar.

Compartir Hooks Sin Hacer un Desastre

Hay tres formas comunes de compartir hooks.

La más simple es core.hooksPath, donde el repositorio contiene .githooks/ y la configuración establece que Git lo use. Esto es transparente y no requiere otro gestor de paquetes.

Los proyectos de JavaScript a menudo usan Husky porque se integra con los flujos de instalación de npm, pnpm o yarn. Puede ser una buena opción cuando cada colaborador ya usa la cadena de herramientas de Node.

Muchos equipos de lenguaje mixto usan el marco pre-commit. Instala y ejecuta hooks definidos en .pre-commit-config.yaml, con versiones fijas para herramientas como formateadores, linters y verificaciones de archivos. Agrega otra herramienta, pero resuelve el problema de "¿cómo instalamos los mismos hooks en todas partes?" mejor que una página wiki.

Lo que evito es copiar scripts grandes en .git/hooks manualmente. Nadie los revisa, nadie sabe qué versión está instalada y la depuración se convierte en arqueología personal.

Depuración de Hooks

Cuando un hook no se ejecuta, verifica estos en orden:

git config --get core.hooksPath
ls -l .git/hooks .githooks 2>/dev/null

Si core.hooksPath está configurado, Git ignora .git/hooks y usa el directorio configurado. Si el archivo de hook no es ejecutable en macOS o Linux, Git no lo ejecutará:

chmod +x .githooks/pre-commit

Cuando un hook se ejecuta pero falla misteriosamente, agrega trazado temporal:

set -x
pwd
env | sort

Los hooks se ejecutan desde la raíz del repositorio en el uso normal de Git, pero los clientes GUI y los IDE pueden exponer diferencias de ruta o entorno. Usa command -v toolname dentro del hook antes de asumir que un linter o gestor de paquetes está disponible.

También recuerda el interruptor de omisión:

git commit --no-verify

Esto no es un agujero de seguridad por sí mismo; es cómo funciona Git. Es otra razón por la que la aplicación seria pertenece a CI o reglas de rama protegida.

Una Política de Hook Sensata

Usa hooks para verificaciones que sean rápidas, deterministas y fáciles de explicar. Formatear archivos preparados, detectar errores de espacios en blanco, validar mensajes de commit y recordar a los desarrolladores instalar dependencias son buenos candidatos. Evita hooks que requieran acceso a la red, tomen mucho tiempo o dependan de un estado local frágil.

Si un hook bloquea un commit, su mensaje debe decir exactamente qué falló y cómo solucionarlo. "Hook falló" no es suficiente. Un desarrollador en medio de una fusión o un hotfix de producción necesita un comando claro a continuación.

Los hooks del lado del cliente de Git funcionan mejor cuando se sienten como una barandilla útil en lugar de una burocracia local. Mantenlos pequeños, mantenlos versionados y mantén la autoridad final en CI.

Mantén los Hooks Amigables Durante Emergencias

Los hooks deben ayudar durante el trabajo normal sin atrapar a alguien durante una corrección urgente. Eso significa que cada hook de bloqueo necesita un mensaje de fallo claro y una salida de emergencia realista. Git ya proporciona --no-verify para hooks de commit y push, pero tu equipo aún debe decidir cuándo es aceptable omitir. Un hotfix de producción es diferente de saltarse el formato porque un desarrollador tiene prisa.

Un buen mensaje de hook dice qué falló, dónde falló y qué ejecutar a continuación:

echo "ESLint falló en archivos JavaScript preparados."
echo "Ejecuta: npm run lint -- --fix"
exit 1

Un mal mensaje dice solo falló o vuelca páginas de salida de herramientas sin contexto. La gente aprende a ignorar ese tipo de hook.

Si el hook modifica archivos, ten mucho cuidado. Los formateadores pueden ser útiles en pre-commit, pero también pueden crear confusión cuando cambian partes no preparadas de un archivo. Muchos equipos prefieren verificar el formato en el hook y dejar que el desarrollador ejecute el formateador manualmente. Otros usan herramientas que formatean solo los hunks preparados. Elige un comportamiento y documéntalo en el repositorio, no en un hilo de chat que desaparece.

Para los equipos, revisa los cambios de hook como código de aplicación. Un hook puede ralentizar cada commit, filtrar detalles del entorno en los registros o romper a los colaboradores en Windows si asume un comportamiento solo de Bash. Si tu proyecto tiene colaboradores de Windows, prueba los hooks en Git Bash o usa un ejecutor de hooks multiplataforma. Si tu proyecto tiene contenedores o shells de desarrollo, considera ejecutar hooks dentro del mismo entorno que la aplicación para que todos usen las mismas versiones de herramientas.

Los mejores hooks son casi invisibles cuando todo está bien y muy específicos cuando algo está mal. Ese es el estándar al que aspirar.

Versiona los Hooks Como Código de Producto

Un script de hook se convierte en parte de la experiencia del desarrollador. Si se rompe, cada colaborador lo siente. Mantén los scripts pequeños, nombra las funciones auxiliares claramente y evita trucos ingeniosos de shell cuando un comando directo funcionaría. Si un hook crece más de una o dos pantallas, mueve la lógica real a un script de proyecto probado y deja que el hook llame a ese script.

Por ejemplo, en lugar de incrustar una rutina larga de lint en .githooks/pre-commit, llama:

./scripts/check-staged-files.sh

Ese script puede ser ejecutado por desarrolladores, hooks y CI. También significa que un desarrollador puede reproducir el fallo sin pretender hacer un commit. La reproducibilidad es la diferencia entre un hook útil y un obstáculo local misterioso.

Fija las versiones de herramientas donde puedas. Un hook que llama a lo que sea black, eslint o prettier que esté primero en PATH puede comportarse de manera diferente en distintas máquinas. Las dependencias locales del proyecto, los lockfiles, los contenedores o los gestores de versiones hacen que la salida del hook sea más predecible.

Finalmente, mantén los hooks limitados al repositorio. Los hooks globales suenan convenientes, pero a menudo te sorprenden meses después cuando un repositorio no relacionado comienza a fallar debido a una regla personal antigua. Usa hooks globales solo para preferencias verdaderamente personales, no para políticas de equipo.

Una última regla práctica: nunca dejes que los hooks sean el único lugar donde existe un comando. Si el hook verifica archivos Python preparados, mantén ese comando también en un script o ejecutor de tareas. Los desarrolladores deberían poder ejecutar la misma verificación a propósito, antes de que Git los interrumpa.

Para proyectos de código abierto, asume que los colaboradores pueden no tener tu cadena de herramientas completa instalada todavía. Un hook que falla con un mensaje de configuración amigable está bien. Un hook que arroja un stack trace de un binario local faltante se siente roto. Verifica los requisitos previos antes de ejecutar comandos más pesados y señala a las personas el comando de configuración utilizado por el proyecto.

También piensa en los commits parciales. Muchos desarrolladores experimentados preparan solo parte de un archivo. Los hooks que formatean todo el archivo pueden accidentalmente arrastrar trabajo no preparado al commit. Si tu equipo usa commits parciales con frecuencia, prefiere verificaciones que lean el diff preparado o herramientas diseñadas para contenido preparado.

Si un hook sigue siendo omitido, trátalo como retroalimentación. O la verificación es demasiado lenta, el mensaje de fallo no es claro o la regla pertenece a CI en lugar de la ruta de commit local. Arregla la fricción en lugar de culpar a los desarrolladores por usar la omisión que Git proporciona.