Guía9 min

git hooks para coding agents: listar, no instalar ni --no-verify

Resumen

Los hooks de Git son programas en $GIT_DIR/hooks o en core.hooksPath. Sin bit ejecutable, Git los ignora. Un agente lista y respeta el exit code; no instala samples, no pone core.hooksPath=/dev/null y no usa --no-verify. git hook run es para comandos scripted, no para saltarse el hook real. Distinto de git commit y de git config. Git 2.50.1.

GitHub
Directorio de hooks Git: programas con bit ejecutable disparan el comando; sin +x Git los ignora

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

Los hooks de Git son programas que Git invoca en puntos fijos de su ejecución. El man (githooks(5); git-scm.com/docs/githooks HTTP 200, last-modified 2026-08-31; man local Git 2.50.1 / Apple Git-155, 2025-07-22): Hooks are programs you can place in a hooks directory to trigger actions at certain points in git’s execution. Hooks that don’t have the executable bit set are ignored. SYNOPSIS: $GIT_DIR/hooks/* o git config core.hooksPath/*.

No es commit. commit crea el objeto; el hook puede abortar ese create. Tampoco es config: core.hooksPath redirige el directorio, no “apaga Git”. Ponerlo en /dev/null (el man de config lo documenta) deshabilita todos los hooks. Un agente no lo toca.

Contrato: listar. Respetar el exit code. Cero instalar samples. Cero chmod +x de *.sample. Cero core.hooksPath=/dev/null. Cero --no-verify / -n. Cero git hook run como sustituto del comando real.

Dónde viven y cuándo Git los ignora

Por default el directorio es $GIT_DIR/hooks (.git/hooks en un clone no-bare). core.hooksPath lo cambia: absoluto o relativo al directorio donde corre el hook (man de git-config(1), sección core.hooksPath, HTTP 200). Relativo no es “relativo al repo root” a ciegas.

Antes de invocar, Git cambia el cwd: en un repo no-bare, a la raíz del working tree; en bare, a $GIT_DIR. Excepción: hooks de push (pre-receive, update, post-receive, post-update, push-to-checkout) siempre corren en $GIT_DIR.

GIT_DIR y GIT_WORK_TREE se exportan para que el hook hable con este repo. Si necesita un repo extranjero, el man exige unset $(git rev-parse --local-env-vars) antes del git -C. Un agente no reescribe hooks ajenos para “arreglar” el cwd.

git init puede copiar samples del template (git-init(1), TEMPLATE DIRECTORY). Un *.sample no es un hook activo: le falta el bit ejecutable y, a menudo, el nombre sin sufijo. Activarlo es del humano, no del agente.

Cliente vs servidor: dos contratos

Los hooks de cliente viven en el clone del humano y disparan commit, merge, rebase, push local. Los de servidor viven en el bare que recibe el push (receive-pack). Mezclarlos es el error clásico: un pre-commit nunca corre en GitHub. La política de “no force-push a main” en el servidor es update / pre-receive o branch protection, no un script en .git/hooks del laptop.

Cliente que un agente va a chocar:

HookQuién lo llama--no-verifyEfecto si exit ≠ 0
pre-commitgit commitaborta antes de crear el commit
prepare-commit-msggit commitnoaborta; edita el archivo del mensaje
commit-msggit commit / git mergeaborta; puede editar el mensaje
post-commitgit commitn/ano cambia el resultado
pre-pushgit pushno (es otro flag)no empuja nada

pre-commit no recibe parámetros. El sample aborta trailing whitespace y, con hooks.allownonascii unset o false, filenames no-ASCII. commit-msg recibe un path: el archivo del mensaje. El sample aborta Signed-off-by duplicados.

prepare-commit-msg corre después de armar el mensaje default y antes del editor. El man: it is not suppressed by the --no-verify option y should not be used as a replacement for the pre-commit hook. Un agente que “pasa -n para no molestar” sigue disparando este hook.

pre-push recibe dos args (nombre y URL del remoto) y por stdin líneas local-ref SP local-oid SP remote-ref SP remote-oid LF. Exit ≠ 0 = push aborta sin empujar nada. No es --force.

Servidor: pre-receive una vez por operación (stdin: old-oid new-oid ref); si falla, ningún ref se actualiza. update una vez por ref. post-receive después del trabajo: no puede deshacer. Un coding agent en un clone no instala estos.

Flujo cliente: pre-commit, prepare-commit-msg y commit-msg alrededor de git commit

--no-verify no es un atajo de agente

El man de pre-commit y de commit-msg: se bypassan con --no-verify. El de commit ya lo prohíbe para agentes. Aquí el matiz operativo:

  1. -n / --no-verify no apaga prepare-commit-msg.
  2. -n no apaga pre-push. El push tiene su propio --no-verify (man de git-push): un agente no lo usa.
  3. -n no es equivalente a core.hooksPath=/dev/null. El segundo mata todos los hooks del proceso, incluido el que el humano puso para no commitear secretos.
  4. Un hook que falla con exit 1 es una señal. Releer stderr, corregir el index, repetir el comando sin -n. No “reintentar con verify off”.

core.hooksPath=/dev/null (man de git-config(1)): You can also disable all hooks entirely. Lo marca para expert users y on a per-command basis con git -c core.hooksPath=/dev/null. Un agente no lo pone en --local, no lo exporta, no lo deja en GIT_CONFIG_COUNT.

git hook run no sustituye el comando

git-hook(1) (git-scm.com/docs/git-hook HTTP 200; man local 2025-07-22): A command interface for running git hooks, for use by other scripted git commands. SYNOPSIS: git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>].

Es la API interna para que commit / am / receive-pack disparen el programa. Un agente no la usa para “probar” pre-commit y luego git commit --no-verify, ni para simular pre-push con stdin inventado, ni para invocar update / pre-receive en un clone no-bare.

--ignore-missing quietly returning zero si el hook no existe. Un agente autónomo no lo usa para tragar un miss. --to-stdin=<path> manda un archivo entero al stdin; el path lo arma Git, no un dump del working tree.

Qué hace un agente (y qué no)

Permitido, en este orden:

git config get --show-origin --show-scope -- core.hooksPath
ls -l "$(git rev-parse --git-path hooks)"

Si core.hooksPath está set, lista ese directorio, no .git/hooks. Sin bit x, Git ignora el archivo aunque se llame pre-commit. No lo actives.

Prohibido:

  • chmod +x .git/hooks/pre-commit.sample ni copiar el sample quitando .sample.
  • Escribir un hook nuevo en .git/hooks o en core.hooksPath (side effect persistente en la máquina del humano).
  • git config set core.hooksPath /dev/null ni git -c core.hooksPath=/dev/null commit.
  • git commit --no-verify, git commit -n, git push --no-verify.
  • git hook run pre-receive / update / proc-receive desde un worktree de feature.
  • Borrar hooks “porque fallan en CI”. El fallo es el contrato.

Si pre-commit aborta: lee stderr, arregla paths ya staged (add por path, nunca git add .), vuelve a git commit -m. Si el hook exige un linter que no está en el PATH, bloquea y reporta. No lo desinstalas.

post-checkout no deshace el switch: su exit status se propaga a git switch / git checkout, pero no revierte HEAD. Un agente no reintenta switch para silenciar un hook de notificación.

Relación con el resto del flujo

pre-rebase puede impedir un rebase. Un agente no lo bypassa. post-rewrite corre después de --amend o rebase. Como amend y force-push ya están vetados, casi no debería dispararse.

reference-transaction solo mira el exit en prepared: ≠ 0 aborta. Un agente no lo instala para auditar refs.

El hub de comparativas y decisiones es el lugar de esta pieza: contrato Git nativo, no tutorial de Husky. Si el repo usa un framework, el humano lo instaló; el agente lo respeta.

Un bypass de hooksPath o --no-verify apaga la compuerta; el agente no toca ese interruptor

Checklist

  1. git config get --show-origin --show-scope -- core.hooksPath — no lo sets.
  2. Lista el directorio real; *.sample no es hook.
  3. Sin bit ejecutable = ignorado. No chmod +x.
  4. git commit -m con index ya staged. Cero -n / --no-verify.
  5. Si el hook falla: stderr → fix → repetir. No apagar hooks.
  6. git hook run no sustituye commit / push.
  7. Hooks de servidor no se instalan en un clone de feature.
  8. core.hooksPath=/dev/null está vetado, incluso one-shot con -c.

FAQ

¿Husky / lefthook / pre-commit.com son lo mismo? No. Instalan un pre-commit (o setean core.hooksPath) que llama su runner. El contrato de Git no cambia. Un agente no npm i husky ni pre-commit install.

¿Puedo versionar los hooks en el repo? Sí, con core.hooksPath a un path del working tree. Setearlo es del humano. El agente, como mucho, lee.

¿git commit -n es más rápido? Salta pre-commit y commit-msg. No salta prepare-commit-msg. Es más ciego. Vetado.

¿Y si el hook se cuelga en la red? Reporta el bloqueo. Un timeout del agente no es --no-verify.

La siguiente lectura práctica es commit (el comando que dispara tres de estos hooks) y el curso si todavía no tienes el loop local.