Git Auditor: hooks nativos con shell para auditoría de equipo

Cuando un equipo crece, los estilos de commit se dispersan y el lint llega tarde. Git Auditor usa hooks shell versionados en .githooks/ para reforzar un estándar mínimo viable — en local, antes de la CI.

EP

Ender Puentes

Contexto: un estándar mínimo viable para el equipo

Cuando un equipo crece — o cuando vuelve a un repo después de un tiempo — suele aparecer la misma fricción: cada uno commitea con un estilo distinto, las ramas no se entienden a simple vista y los problemas de lint o tipos llegan tarde, en el push o en CI. No es mala intención; es falta de un piso común acordado.

Este documento describe cómo abordamos eso con Git Auditor: hooks nativos en .githooks/ que refuerzan un estándar mínimo viable — mensajes de commit legibles, ramas trazables, lint y tipos antes de subir — sin complicar el día a día.

La idea es simple: las reglas viven en el repo, se activan solas al instalar dependencias y ayudan a que todo el mundo escriba código de forma parecida antes de que el remoto o la CI tengan que corregir.

Cómo está armado

El patrón separa tres capas que conviene no mezclar:

  • Init (prepare) · Activa los hooks en la máquina del desarrollador — Node (solo para el bootstrap)
  • Hooks (.githooks/*) · Aplica las reglas acordadas en cada commit/push — Shell + Git (universal)
  • Comandos invocados · Ejecutan lint, types, dead-code checks, etc. — El runtime del proyecto (pnpm, go test, ruff, …)

Los hooks son scripts #!/bin/sh. Ahí va la política ligera — formato de commits, nombres de rama, qué corre en pre-commit vs pre-push — con herramientas que Git ya sabe ejecutar. Los chequeos más pesados (linter, TypeScript, etc.) se delegan al toolchain del proyecto.

Elegimos hooks nativos con core.hooksPath porque Git ya trae el gancho; versionamos los scripts en el repo y evitamos depender de otra capa solo para orquestar lo mismo. Husky, Commitlint y herramientas similares pueden cumplir un rol parecido; aquí priorizamos scripts legibles, pocas dependencias en la validación de mensajes y un patrón que se puede replicar en otros stacks cambiando solo los comandos que invoca cada hook.

Por qué .git/ no versiona hooks (y hace falta un script)

Git guarda metadatos locales del repositorio dentro de .git/. Ese directorio no se sube al remoto: cada clon es independiente. Los hooks que Git ejecuta por defecto viven en .git/hooks/ — también locales, también fuera del control de versiones.

En la práctica, eso significa que un pre-commit copiado a mano en .git/hooks/ no llega al resto del equipo con un git pull. Quien clona el repo de cero no hereda esos scripts. Para compartir reglas hace falta otro enfoque.

La salida es un patrón en dos piezas:

  • Scripts de auditoría · .githooks/ en la raíz del repo — Sí — viajan con el código
  • Enlace “usa estos scripts como hooks” · core.hooksPath en .git/config — No — es config local de cada clon

core.hooksPath le dice a Git: “cuando vaya a ejecutar pre-commit, commit-msg, etc., búscalos en .githooks/ del working tree, no en .git/hooks/”. Los archivos sí están en el repo; el enlace no. Por eso hace falta correr un script de init (setup:githooks:init) que ejecute, como mínimo:

bash
git config core.hooksPath .githooks

Sin ese paso, el equipo comparte las reglas en .githooks/ pero Git nunca las invoca. El script cierra esa brecha en cada máquina. Variantes del init pueden delegar a init.sh / init.bat en Windows o concentrar la lógica en un solo init.js; el problema y la solución son los mismos.

Cómo se activa: el script prepare

En package.json:

json
"setup:githooks:init": "node .githooks/scripts/init.js",
"prepare": "pnpm run setup:githooks:init"

prepare es un lifecycle script de npm/pnpm que corre automáticamente después de `pnpm install`. Así, clonar el repo e instalar dependencias ya dispara el init — no dependemos de que alguien recuerde un paso extra en el README.

Lo que hace el init en cada ejecución:

1. Lee core.hooksPath local; si no apunta a .githooks, lo configura.

2. Verifica que exista el directorio .githooks/.

3. En Unix, marca los hooks como ejecutables (chmod +x).

Si alguien clonó antes de que existiera prepare, o borró su .git/config, puede reactivar manualmente:

bash
pnpm run setup:githooks:init
# equivalente:
git config core.hooksPath .githooks

Cómo se aplican las reglas en cada máquina

Conviene distinguir dos cosas: las reglas que el repo define y cómo se activan en cada computadora.

  • Versionadas: el contenido de .githooks/* — qué validar, qué mensajes de error mostrar, qué comandos correr.
  • Locales: la activación (core.hooksPath) y la ejecución en la máquina de cada desarrollador al hacer git commit o git push.

Cada miembro del equipo tiene su propia copia de .git/config. Dos personas con el mismo repo pueden estar en estados distintos: una con hooks activos, otra con core.hooksPath vacío si nunca corrió install/init. La auditoría no es centralizada como un pre-receive hook en el remoto; es un acuerdo de proceso reforzado en local.

Eso implica límites explícitos:

  • Un desarrollador puede ejecutar git commit --no-verify o git push --no-verify y saltarse los hooks.
  • Puede editar o borrar core.hooksPath en su config local.
  • Actualizar .githooks/pre-push en el repo no retroactivamente “instala” nada en máquinas que no vuelvan a correr init o pnpm install (donde prepare revalida el enlace).

Por eso los hooks son un acompañamiento del estándar de equipo — mejor experiencia de desarrollo, no un muro de seguridad. Ayudan temprano, en el flujo habitual, a quien tenga el init hecho. Para lo que quede fuera de ese alcance, CI y branch protection en el remoto siguen siendo la red de contención.

Por qué usamos shell en los hooks

Git ejecuta el hook como un proceso hijo del binario git. El contrato es simple: un ejecutable que termina con código 0 (ok) o distinto de 0 (bloquea la operación).

Shell cumple ese contrato en Linux, macOS y la mayoría de entornos de desarrollo sin compilar nada:

  • Portabilidad del patrón: el mismo esquema .githooks/ + prepare funciona en cualquier stack. Solo cambian los comandos que el hook invoca (pnpm lint, make test, cargo clippy, etc.).
  • Validaciones ligeras sin runtime: reglas de Conventional Commits, longitud de header, regex de nombre de rama o detección de merge commits no necesitan Node. Un grep -qE en commit-msg es suficiente y arranca en milisegundos.
  • Un solo lugar de verdad: .githooks/commit-msg es la fuente de verdad del formato de commits; la documentación del repo apunta ahí.
  • Menos puntos de fallo en el hook mismo: si el hook dependiera de node node_modules/.bin/commitlint, un install incompleto o un PATH roto impediría commitear antes de que el mensaje llegue a validarse.

Los hooks sí delegan en el stack del proyecto para la parte pesada del estándar: linter, comprobación de tipos, detección de código muerto. Shell orquesta; el lenguaje del repo ejecuta.

Qué puede incluir el estándar mínimo viable

Las reglas concretas se definen por equipo y madurez del repo. El patrón admite combinar capas sin cambiar de mecanismo:

  • pre-commit · Antes de crear el commit — Lint en archivos staged; formato; tests rápidos. Omite merges.
  • commit-msg · Al escribir el mensaje — Conventional Commits; longitud máxima; política por rama (main solo merge/hotfix).
  • pre-push · Antes de subir — Typecheck; lint completo; dead-code analysis; validación de nombre de rama.

Bypass explícito (solo cuando hace falta): git commit --no-verify / git push --no-verify.

El init lo llama Git Auditor en consola: resume la intención de canalizar el estándar antes de que el código llegue al remoto o a CI.

El estándar mínimo viable crece con el equipo — más reglas en pre-push cuando el repo madura, ventanas de release, protección de ramas — pero siempre con el mismo contrato: política versionada en `.githooks/`, activación local por script, ejecución en shell, checks pesados delegados al toolchain del proyecto.

Cuándo tiene sentido y cuándo no

Tiene sentido cuando:

  • Quieres que todo el equipo escriba código y commits bajo las mismas reglas base, sin depender de memoria o revisión manual constante.
  • El equipo usa Git de forma homogénea y puedes enganchar el init a pnpm install (o equivalente con prepare en npm/yarn).
  • Prefieres hooks legibles y ajustables sin un framework extra para validar mensajes.

Limitaciones honestas (complemento al apartado de restricciones locales):

  • Windows requiere atención (sh, Git Bash, o scripts .bat en el init).
  • Si las reglas crecen mucho, conviene extraer funciones compartidas en .githooks/lib/ (sigue siendo shell) antes de migrar a otra herramienta.

Referencia rápida

text
Objetivo:       estándar mínimo viable de código y commits, reforzado en local
Activación:     pnpm install  →  prepare  →  setup:githooks:init
Config Git:     git config core.hooksPath .githooks   (local, en .git/config)
Hooks:          .githooks/pre-commit | commit-msg | pre-push   (versionados)
Init:           .githooks/scripts/init.js
Documentación:  .githooks/README.md

Verificación manual:

bash
git config --get core.hooksPath   # debe imprimir: .githooks
pnpm run setup:githooks:init      # re-ejecutar init si hace falta