Lefthook — Gestor de Git hooks
Qué es
Lefthook es un gestor de Git hooks escrito en Go. Permite definir en un archivo lefthook.yaml qué comandos se ejecutan antes de cada commit (pre-commit) o al validar el mensaje del commit (commit-msg), de forma paralela y con filtrado por archivos modificados.
Por qué lo usamos en Wiedii
| Alternativa | Problema | Lefthook |
|---|---|---|
husky | Requiere Node/npm activos para correr los hooks | Binario independiente — funciona sin entorno Node |
Scripts manuales en .git/hooks/ | No se versionan en el repo | lefthook.yaml se commitea y se comparte con todo el equipo |
| Sin hooks | Errores de lint/formato llegan al pipeline | Los problemas se detectan antes del push, en la máquina del dev |
Lefthook corre los comandos en paralelo por defecto, lo que lo hace más rápido que ejecutores secuenciales como husky.
Instalación
Lefthook se gestiona con mise en mise.toml, no como dependencia de npm. Así funciona correctamente aunque el proyecto no tenga node_modules instalado todavía.
# mise.toml
[tools]
"aqua:evilmartians/lefthook" = "2.1.3"
mise install --yes # instala lefthook junto al resto de herramientas
Los hooks se registran en .git/hooks/ ejecutando:
lefthook install
Esto ocurre automáticamente al correr mise install gracias al [hooks].postinstall en mise.toml:
[hooks]
postinstall = "[ -z \"$CI\" ] && lefthook install || true"
Es decir:
mise run setup→mise install --yes→[hooks].postinstall→lefthook install→ hooks registrados. Sin pasos manuales, sin dependencia de Node, sin ejecución en CI.
Configuración estándar en Wiedii
# lefthook.yaml
pre-commit:
parallel: true
jobs:
- run: mise trust
- run: mise install --yes
commands:
toml:
priority: 1
glob: '*.toml'
run: mise exec -- taplo fmt {staged_files}
stage_fixed: true
prettier:
priority: 2
glob: '*.{js,cjs,mjs,ts,json,md,yaml}'
run: mise exec -- bun prettier --write --cache {staged_files}
stage_fixed: true
lint:
priority: 3
glob: '*.{ts,tsx,js,jsx}'
run: mise exec -- bun eslint {staged_files} --max-warnings=0
shellcheck:
priority: 1
glob: '*.sh'
run: mise exec -- shellcheck {staged_files}
lefthook-validate:
priority: 1
glob: 'lefthook.yaml'
run: lefthook validate
commit-msg:
parallel: false
jobs:
- run: mise trust
- run: mise install --yes
commands:
commitlint:
priority: 1
run: mise exec -- bun commitlint --config commitlint.config.js --edit "$1"
post-merge:
parallel: false
jobs:
- run: mise trust
- run: mise install --yes
commands:
bun-install:
glob: '{package.json,bun.lock,bunfig.toml}'
run: |
if [ ! -d node_modules ] || git diff --name-only ORIG_HEAD HEAD -- package.json bun.lock bunfig.toml | grep -q .; then
echo "bun-install: running bun install --frozen-lockfile"
mise exec -- bun install --frozen-lockfile
else
echo "bun-install: no dependency file changes detected, skipping"
fi
post-checkout:
parallel: false
jobs:
- run: mise trust
- run: mise install --yes
commands:
bun-install:
glob: '{package.json,bun.lock,bunfig.toml}'
run: |
if [ ! -d node_modules ] || git diff --name-only ORIG_HEAD HEAD -- package.json bun.lock bunfig.toml | grep -q .; then
echo "bun-install: running bun install --frozen-lockfile"
mise exec -- bun install --frozen-lockfile
else
echo "bun-install: no dependency file changes detected, skipping"
fi
post-rewrite:
parallel: false
jobs:
- run: mise trust
- run: mise install --yes
commands:
bun-install:
glob: '{package.json,bun.lock,bunfig.toml}'
run: |
if [ ! -d node_modules ] || git diff --name-only ORIG_HEAD HEAD -- package.json bun.lock bunfig.toml | grep -q .; then
echo "bun-install: running bun install --frozen-lockfile"
mise exec -- bun install --frozen-lockfile
else
echo "bun-install: no dependency file changes detected, skipping"
fi
Puntos clave de esta configuración:
parallel: true— todos los comandos depre-commitcorren en paralelojobs—mise trust+mise install --yesgarantizan que las herramientas estén disponibles antes de correr los comandos; sineval "$(mise env)"para no contaminar el entornomise exec -- comando— cada comando corre con el entorno de mise aislado para ese comandopriority— dentro de un hook paralelo, ordena ejecución: 1 corre antes que 2, 2 antes que 3glob— cada comando solo se ejecuta si hay archivos en stage que coincidan{staged_files}— lefthook pasa solo los archivos en stage, no todo el proyectostage_fixed: true— si un formatter modifica archivos, lefthook los re-añade al stage automáticamente"$1"encommit-msg— path del archivo de mensaje de commit que Git pasa al hookpost-merge/post-checkout— instalan deps automáticamente si cambia el lockfile al cambiar de rama o hacer merge
Configuraciones compartidas — wiedii-configs
Los hooks commit-msg, post-merge, post-checkout y post-rewrite están centralizados en wildcat/wiedii-configs. En lugar de copiar el YAML completo en cada repo, se consume vía remotes de lefthook:
# lefthook.yaml — repos JS/TS
remotes:
- git_url: git@gitlab.wiedii.co:wildcat/wiedii-configs.git
configs:
- lefthook/base.yaml # commit-msg con commitlint
- lefthook/js.yaml # post-merge, post-checkout, post-rewrite con bun install
Los hooks compartidos disponibles:
| Archivo | Hooks | Stack |
|---|---|---|
lefthook/base.yaml | commit-msg (commitlint) | Todos los stacks |
lefthook/js.yaml | post-merge, post-checkout, post-rewrite (bun install) | JS / TS |
lefthook/python.yaml | (próximamente) | Python |
lefthook/go.yaml | (próximamente) | Go |
El lefthook.yaml local del repo solo necesita los hooks específicos del proyecto (pre-commit con linters) y el bloque remotes. Los hooks centralizados se descargan y cachean en .lefthook/ al correr lefthook install.
.lefthook/debe estar en.gitignore— es un artefacto local generado, no código fuente.
Comandos útiles
# Registrar los hooks en .git/hooks/ (se hace una vez por clon)
lefthook install
# Ejecutar manualmente los hooks pre-commit (útil para verificar que todo pasa)
lefthook run pre-commit
# Ejecutar un hook específico
lefthook run commit-msg
# Ver qué comandos están configurados
lefthook list
# Saltar hooks puntualmente (⚠️ solo en casos justificados)
git commit --no-verify -m "chore: fix typo"
LEFTHOOK=0 git commit -m "chore: fix typo"
--no-verifydebe usarse solo en situaciones excepcionales. Si los hooks fallan con frecuencia, el problema es la configuración del hook, no el hook en sí.
Integración con mise
Los hooks de Git se ejecutan en un contexto de shell aislado donde el entorno de mise puede no estar activado (commits desde IDE, GUIs de Git, SSH). El patrón en Wiedii combina dos mecanismos:
jobs — para preparar el entorno antes de ejecutar los comandos. Solo mise trust y mise install --yes: garantizan que las herramientas del mise.toml estén disponibles, sin modificar el entorno del hook.
mise exec -- comando — cada comando individual se ejecuta con los binarios de mise de forma aislada, sin contaminar el PATH del shell padre ni el contexto del hook.
commit-msg:
parallel: false
jobs:
- run: mise trust # acepta el mise.toml del repo sin prompts
- run: mise install --yes # instala herramientas si faltan
commands:
commitlint:
run: mise exec -- bun commitlint --config commitlint.config.js --edit "$1"
Por qué
mise exec --en lugar deeval "$(mise env)":eval "$(mise env)"exporta todas las variables de mise al shell actual, modificando el PATH de forma permanente dentro del hook.mise exec -- programacrea un entorno limpio solo para ese comando. Más preciso, sin efectos secundarios.
Troubleshooting rápido
Los hooks no se ejecutan al hacer commit:
lefthook install # verificar que los hooks están registrados
ls .git/hooks/ # debe existir pre-commit y commit-msg
mise exec: command not found en el hook:
# mise debe estar en el PATH del hook. Verificar en .zshrc o .zprofile:
which mise # debe retornar /opt/homebrew/bin/mise
Un comando falla solo en el IDE pero no en terminal:
# El IDE puede tener un PATH distinto. Usar siempre `mise exec --` en lefthook.yaml
# y verificar que mise está activado con shims:
mise activate zsh --shims # debe estar en .zprofile
Quiero omitir un hook puntualmente sin deshabilitar todos:
# lefthook.yaml — agregar `skip` a un comando concreto
pre-commit:
commands:
typecheck:
skip: true # deshabilitar solo este comando temporalmente
run: mise exec -- bun tsc --noEmit
Referencias
- Documentación oficial Lefthook
- Repositorio evilmartians/lefthook
- lefthook — política de hooks de Git en Wiedii
- mise — gestiona la versión de lefthook por proyecto
- bun — gestor de paquetes que ejecuta los hooks al instalar deps
- commits — política de mensajes de commit que
commit-msgvalida