En el artículo anterior de esta serie hablamos de cómo el CLAUDE.md le da instrucciones persistentes al agente. Y mencionamos una limitación importante: esas instrucciones son sugerencias. El agente las sigue la mayor parte del tiempo, pero en sesiones largas, bajo un contexto muy denso, o simplemente cuando considera que una excepción es razonable, puede saltarse una instrucción.
Los hooks resuelven exactamente eso. Son comandos que Claude Code ejecuta automáticamente en puntos específicos de su ciclo de vida, sin importar lo que diga el agente, sin importar el estado del contexto, sin que nadie tenga que acordarse de activarlos. Mientras el agente puede olvidarse de formatear un archivo o de correr los tests antes de un commit, un hook no olvida nada.
En este artículo veremos todo el sistema de hooks desde cero: los 12 eventos del ciclo de vida, los 4 tipos de handlers, el sistema de exit codes, los hooks que pueden modificar el input de una herramienta antes de ejecutarla, los HTTP hooks para integraciones con servicios externos, y 5 ejemplos prácticos — 3 específicos para proyectos Java con Gradle y 2 aplicables a cualquier tecnología.
Sin más, vamos al tema.
Contenido
- El concepto: prompts vs. hooks
- Arquitectura del sistema de hooks
- Los 12 eventos del ciclo de vida
- Los 4 tipos de handlers
- Exit codes: el lenguaje de control
- Configuración: estructura del settings.json
- Modificación de inputs: interceptar antes de ejecutar
- 5 ejemplos prácticos
- Rendimiento y buenas prácticas
- Conclusión
El concepto: prompts vs. hooks
Antes de ver la sintaxis, vale la pena tener clara la distinción conceptual. Hay tres formas de decirle al agente cómo comportarse, y cada una tiene un nivel diferente de garantía:
| Mecanismo | Ejemplo | Nivel de garantía |
|---|---|---|
| Prompt en la sesión | "No olvides correr los tests antes del commit" | Baja — el agente puede olvidarlo en la misma sesión |
| Instrucción en CLAUDE.md | - Corre ./gradlew test antes de hacer commit |
Media — se sigue la mayor parte del tiempo, pero no siempre |
| Hook | PreToolUse que intercepta git commit y bloquea si los tests fallan |
Alta — se ejecuta siempre, determinísticamente, independiente del agente |
Un hook no le pide al agente que haga algo. El hook simplemente ocurre, como un interceptor en un framework web: siempre se ejecuta cuando se cumple la condición, sin necesidad de que nadie lo invoque.
Esta distinción tiene una implicación práctica importante: los hooks son código tuyo, que tú controlas, que se ejecuta en tu máquina o en tu servidor. El agente no puede ignorarlos, no puede desactivarlos en la sesión, y no puede modificar su comportamiento mediante prompts.
Arquitectura del sistema de hooks
El sistema de hooks de Claude Code tiene tres capas conceptuales:
- Hook Event: El punto del ciclo de vida donde se puede disparar un hook (por ejemplo:
PostToolUse) - Matcher: Un filtro regex que determina para qué herramientas específicas se activa el hook dentro de ese evento (por ejemplo:
"Edit|Write"— solo cuando el agente edita o escribe archivos) - Hook Handler: El comando, script, petición HTTP o subagente que se ejecuta cuando el matcher coincide
Los hooks se configuran en el settings.json de Claude Code. Dependiendo de dónde los pongas, tienen diferente alcance:
| Archivo | Alcance | ¿Se versiona en Git? |
|---|---|---|
~/.claude/settings.json | Global — todos tus proyectos | No |
.claude/settings.json | Proyecto — todo el equipo | Sí |
.claude/settings.local.json | Proyecto — solo tú | No (añadir a .gitignore) |
Los hooks de equipo que definen estándares de calidad (tests antes de commit, linter automático) van en .claude/settings.json y se versionan junto con el código. Los hooks personales (notificaciones, atajos de tu flujo de trabajo) van en el global o en el local.
Los 12 eventos del ciclo de vida
Claude Code expone 12 puntos de interceptación a lo largo del ciclo de vida completo de una sesión. Los más importantes para el trabajo diario son los primeros cuatro; los demás son útiles para automatizaciones más avanzadas.
Antes de que el agente ejecute cualquier herramienta. El único evento que puede bloquear la acción. Úsalo para gates de seguridad, protección de archivos y validaciones obligatorias.
Después de que una herramienta completa con éxito. Úsalo para formateo automático, linting y retroalimentación al agente sobre el resultado de la operación.
Cuando una herramienta falla. Útil para logging de errores, diagnóstico automático y notificaciones de fallo.
Cuando Claude va a mostrar un diálogo de permisos al usuario. Permite aprobar o denegar automáticamente sin intervención humana.
Cuando el agente termina su respuesta. Exit code 2 fuerza al agente a continuar trabajando. Útil para checklists de calidad al final de cada tarea.
Cuando un subagente termina su tarea. Los hooks de seguridad del proyecto aplican recursivamente a subagentes también.
Cuando el usuario envía un prompt. Permite agregar contexto adicional automáticamente antes de que el agente lo procese.
Al inicio de cada sesión (arranque, resume, clear). Útil para cargar contexto dinámico: rama de Git activa, estado del build, issues abiertos.
Al terminar la sesión. Para cleanup, logging de métricas, backups de la sesión o notificaciones de cierre.
Cuando el agente genera una notificación. Para redirigir alertas a Slack, correo, o el sistema de notificaciones de tu sistema operativo.
Justo antes de que el agente compacte el contexto. Permite guardar información importante antes de que se resuma.
Intercepta solicitudes de elicitación de servidores MCP antes de mostrarlas al usuario. Para pre-validación o pre-relleno de respuestas.
PreToolUse, PostToolUse y demás eventos conforme ocurren.
Los 4 tipos de handlers
Ejecuta un script de shell local. Recibe el JSON del evento por stdin.
Úsalo para: formateo, linting, validaciones locales, tests, git hooks.
Hace un POST a una URL con el JSON del evento como body. Lanzado en febrero 2026.
Úsalo para: políticas de equipo centralizadas, auditoría remota, integración con Slack/Jira.
Envía un prompt a un modelo de Claude para evaluación de turno único. Usa $ARGUMENTS como placeholder.
Úsalo para: decisiones que requieren razonamiento (¿este cambio afecta a producción?), evaluaciones de impacto.
Lanza un subagente con acceso a herramientas como Read, Grep y Glob para verificación profunda.
Úsalo para: code reviews automáticos complejos, análisis de impacto multi-archivo, auditorías de seguridad.
La recomendación para empezar es escalar gradualmente: empieza con command para formateo y gates simples, avanza a prompt cuando necesites razonamiento, y usa agent solo para verificaciones que requieren navegar múltiples archivos. Los handlers command son los más rápidos y predecibles.
Cualquier handler puede ejecutarse en modo asíncrono con "async": true, lo que significa que Claude Code no espera su resultado antes de continuar. Úsalo para logging, notificaciones y backups donde no necesitas bloquear el flujo:
{
"type": "command",
"command": "echo \"$(date): sesión completada\" >> ~/.claude/session.log",
"async": true
}
Exit codes: el lenguaje de control
Los hooks se comunican con Claude Code a través de exit codes y JSON en stdout/stderr. Entender este sistema es fundamental para escribir hooks que funcionen correctamente.
| Exit code | Significado | Cuándo usarlo |
|---|---|---|
0 |
Éxito — la acción puede proceder | La validación pasó, no hay nada que bloquear |
2 |
Bloqueo (en PreToolUse) o forzar continuar (en Stop) | El gate de seguridad falló; los tests están rotos; no se puede hacer commit |
1 (u otro) |
Error no bloqueante — se muestra al usuario pero la acción continúa | Un warning, una advertencia que no debe detener el flujo |
exit 1 cuando querías exit 2. Con exit 1 el hook muestra un warning pero la acción procede de todas formas. Si tu intención es bloquear, siempre usa exit 2. Sin exit 2, el hook no proporciona ninguna garantía real.
Cuando un PreToolUse bloquea con exit 2, lo que escribas en stderr le llega al agente como contexto para que entienda por qué fue bloqueado y ajuste su plan. Es el canal de feedback del hook hacia el modelo:
#!/bin/bash
# Si el build está roto, el agente recibirá el mensaje de stderr
# y sabrá que necesita corregir los errores de compilación primero
if ! ./gradlew compileJava -q 2>/dev/null; then
echo "Build broken: fix compilation errors before committing" >&2
exit 2
fi
exit 0
Para PreToolUse, el bloqueo también puede expresarse como JSON estructurado usando hookSpecificOutput, lo que permite incluir una razón más formal y opcionalmente modificar el input de la herramienta:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Los archivos de migración Flyway son inmutables una vez aplicados"
}
}
Configuración: estructura del settings.json
La estructura básica del settings.json para configurar hooks es la siguiente:
{
"hooks": {
"NombreDelEvento": [
{
"matcher": "regex-de-herramientas",
"hooks": [
{
"type": "command",
"command": "tu-script-aqui",
"timeout": 30
}
]
}
]
}
}
Algunos detalles importantes sobre el matcher:
- Es una expresión regular que se compara contra el nombre de la herramienta:
Bash,Edit,Write,Read,Glob,Grep,Agent,WebFetch,WebSearch, y cualquier herramienta MCP - Puedes usar el pipe para múltiples herramientas:
"Edit|Write|MultiEdit" - Un matcher vacío (
"") coincide con todos los eventos del tipo, útil paraStopySessionStart - Múltiples hooks en el array del mismo evento se ejecutan en paralelo
Para desactivar todos los hooks temporalmente sin borrar la configuración:
{
"disableAllHooks": true,
"hooks": { /* tu configuración normal */ }
}
Modificación de inputs: interceptar antes de ejecutar
Una de las capacidades más poderosas de PreToolUse — y de las menos conocidas — es la posibilidad de modificar el input de una herramienta antes de que se ejecute. En lugar de bloquear y forzar al agente a reintentar, el hook intercepta, corrige el input, y deja que la ejecución proceda con los parámetros modificados. El agente no ve la modificación.
El caso de uso más claro: el agente quiere ejecutar git commit -m "fix bug" pero tus convenciones de equipo requieren el prefijo del número de ticket. En lugar de bloquearlo, el hook reformatea el mensaje automáticamente:
#!/bin/bash
# Lee el input del comando desde stdin
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# Si es un git commit sin prefijo de ticket, lo agrega automáticamente
if echo "$COMMAND" | grep -q "git commit" && ! echo "$COMMAND" | grep -qE "\[#[0-9]+\]"; then
TICKET=$(git branch --show-current | grep -oE "[0-9]+" | head -1)
if [ -n "$TICKET" ]; then
NEW_CMD=$(echo "$COMMAND" | sed "s/git commit -m \"/git commit -m \"[#$TICKET] /")
echo "$INPUT" | jq --arg cmd "$NEW_CMD" '.tool_input.command = $cmd'
exit 0
fi
fi
# Si no hay que modificar nada, pasar el input original
echo "$INPUT"
exit 0
La salida del hook debe ser el JSON del input (posiblemente modificado). Claude Code usa ese JSON para la ejecución real. Si el hook no imprime nada y sale con exit 0, el input original se usa sin cambios.
5 ejemplos prácticos
Ejemplo 1 (Java / Gradle): Checkstyle automático después de cada edición
El hook más útil para proyectos Java: cada vez que el agente edita o crea un archivo .java, Checkstyle corre automáticamente y su output vuelve al agente como feedback para que corrija las violaciones antes de continuar.
// .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/checkstyle.sh",
"timeout": 30
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/checkstyle.sh
set -euo pipefail
FILE=$(cat | jq -r '.tool_input.file_path // empty')
# Solo actuar sobre archivos Java
if [[ -z "$FILE" ]] || [[ "$FILE" != *.java ]]; then
exit 0
fi
# Correr Checkstyle en el archivo modificado
OUTPUT=$(./gradlew checkstyleMain --include="$FILE" -q 2>&1 | tail -30)
if [ $? -ne 0 ]; then
# El output va al agente como feedback para que corrija
echo "Checkstyle violations in $FILE:"
echo "$OUTPUT"
fi
exit 0
PostToolUse porque el objetivo es dar feedback al agente, no bloquear la escritura. El agente verá las violaciones inmediatamente y las corregirá en el siguiente turno.
Ejemplo 2 (Java / Gradle): Bloquear commit si los tests fallan
El hook de calidad más crítico para cualquier proyecto. Intercepta cualquier git commit y lo bloquea si ./gradlew test falla. El agente recibe el output de los tests fallidos como contexto para saber qué corregir.
// .claude/settings.json — añadir dentro de "hooks"
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/test-gate.sh",
"timeout": 120
}
]
}
]
}
#!/bin/bash
# .claude/hooks/test-gate.sh
set -uo pipefail
COMMAND=$(cat | jq -r '.tool_input.command // empty')
# Solo actuar sobre git commit
if ! echo "$COMMAND" | grep -q "git commit"; then
exit 0
fi
# Correr los tests
TEST_OUTPUT=$(./gradlew test -q 2>&1 | tail -40)
TEST_EXIT=$?
if [ $TEST_EXIT -ne 0 ]; then
# stderr llega al agente; debe corregir los tests antes de poder hacer commit
echo "Tests failing. Fix before committing:" >&2
echo "$TEST_OUTPUT" >&2
exit 2
fi
exit 0
Ejemplo 3 (Java / Gradle): Detectar anti-patrones en código recién escrito
Un hook que detecta patrones problemáticos comunes en proyectos Java — System.out.println, @Autowired en campos, números mágicos — y los señala inmediatamente después de que el agente escribe el código, antes de que lleguen a un PR.
#!/bin/bash
# .claude/hooks/antipatterns.sh
set -uo pipefail
FILE=$(cat | jq -r '.tool_input.file_path // empty')
if [[ -z "$FILE" ]] || [[ "$FILE" != *.java ]]; then
exit 0
fi
WARNINGS=()
# System.out.println en código no-test
if [[ "$FILE" != *Test* ]] && grep -q "System\.out\.println" "$FILE"; then
WARNINGS+=("⚠ System.out.println found — use a logger instead (SLF4J/Logback)")
fi
# @Autowired en campo (debe ser inyección por constructor)
if grep -qP "^\s+@Autowired" "$FILE"; then
WARNINGS+=("⚠ Field @Autowired detected — use constructor injection instead")
fi
# Números mágicos en código no-test (excepto 0 y 1)
if [[ "$FILE" != *Test* ]] && grep -qP "(?<![A-Za-z0-9_])[2-9][0-9]+(?![A-Za-z0-9_])" "$FILE"; then
WARNINGS+=("⚠ Possible magic number — consider extracting a named constant")
fi
if [ ${#WARNINGS[@]} -gt 0 ]; then
echo "Anti-patterns detected in $FILE:"
for w in "${WARNINGS[@]}"; do
echo " $w"
done
fi
exit 0
Ejemplo 4 (Genérico): Checkpoint automático antes de refactorizaciones grandes
Este hook crea un commit temporal de checkpoint cada vez que el agente va a editar más de un archivo en un solo turno. Funciona como seguro de rollback adicional al sistema nativo de checkpoints de Claude Code.
#!/bin/bash
# .claude/hooks/auto-checkpoint.sh
# Guarda un checkpoint en git antes de ediciones potencialmente grandes
set -uo pipefail
TOOL=$(cat | jq -r '.tool_name // empty')
# Solo para herramientas de escritura
if [[ "$TOOL" != "Edit" ]] && [[ "$TOOL" != "Write" ]] && [[ "$TOOL" != "MultiEdit" ]]; then
exit 0
fi
# Solo si hay cambios sin committear (hay algo que proteger)
if ! git diff --quiet 2>/dev/null; then
TIMESTAMP=$(date +%H%M%S)
git stash push -m "claude-checkpoint-$TIMESTAMP" --include-untracked -q 2>/dev/null || true
git stash pop -q 2>/dev/null || true
# Commit temporal marcado para fácil identificación y limpieza posterior
git add -A && git commit -m "chore: claude checkpoint $TIMESTAMP [auto]" -q 2>/dev/null || true
fi
exit 0
Ejemplo 5 (Genérico): Notificación a Slack cuando el agente necesita atención
Usando HTTP hooks (disponibles desde febrero 2026): cuando el agente genera una notificación — normalmente para avisar que terminó una tarea larga o que necesita aprobación para continuar — se envía un mensaje a Slack automáticamente. Útil cuando inicias una tarea larga y te alejas del teclado.
// .claude/settings.local.json (no versionar — contiene tu webhook)
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "http",
"url": "https://hooks.slack.com/services/TU_WEBHOOK_AQUI",
"timeout": 10,
"headers": {
"Content-Type": "application/json"
}
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/notify-done.sh",
"async": true
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/notify-done.sh
WEBHOOK_URL="${SLACK_CLAUDE_WEBHOOK:-}"
[ -z "$WEBHOOK_URL" ] && exit 0
PROJECT=$(basename "$PWD")
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
curl -s -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"text\": \"✅ Claude Code terminó una tarea en *$PROJECT* (rama: \`$BRANCH\`)\"}" \
> /dev/null
exit 0
settings.local.json, que no se versiona en Git. Nunca pongas URLs de webhooks, tokens o credenciales en settings.json del proyecto, ya que ese archivo va a Git y es visible para todo el equipo.
Otras ideas para Hooks útiles
Además de los 5 hooks que ya te mostré, te dejo otras 10 ideas de hooks útiles. El código de todos ellos, los 15, los dejo en el repositorio al final del tutorial.
| Hook | Evento | Aplica a |
|---|---|---|
| Detectar secrets hardcodeados — bloquea cualquier escritura de archivo que contenga patrones de API keys, tokens JWT o passwords en texto plano | PreToolUse |
Genérico |
| Proteger archivos inmutables — bloquea ediciones a archivos de migración Flyway/Liquibase ya aplicados en producción, archivos de schema firmados o configuración de producción | PreToolUse |
Genérico |
Formateo automático con google-java-format — formatea cada .java recién escrito sin pedírselo al agente, garantizando estilo consistente en todo lo que genera |
PostToolUse |
Java |
| Verificar cobertura de tests — después de que el agente escribe una clase nueva, verifica que exista un archivo de test correspondiente y avisa si no | PostToolUse |
Genérico |
| Log de sesión por proyecto — al terminar cada sesión registra en un archivo local: duración, tokens usados, rama de Git y resumen de la tarea — historial persistente de lo que hizo el agente | SessionEnd |
Genérico |
| Contexto dinámico al arrancar — al iniciar cada sesión inyecta automáticamente: rama activa, último commit, estado de CI y issues abiertos en Jira/GitHub asignados a ti | SessionStart |
Genérico |
Detectar dependencias vulnerables — después de que el agente modifica build.gradle, corre ./gradlew dependencyCheckAnalyze y reporta CVEs antes de continuar |
PostToolUse |
Java |
Validar OpenAPI spec — cuando el agente modifica un archivo openapi.yml o swagger.json, corre un validador de schema automáticamente y bloquea si el contrato está mal formado |
PostToolUse |
Genérico |
Bloquear push a ramas protegidas — intercepta cualquier git push a main, master o release/* y lo bloquea con un mensaje que explica que debe ir por PR |
PreToolUse |
Genérico |
| Resumen de cambios antes de compactar — justo antes de que el agente compacte el contexto, extrae todos los archivos modificados en la sesión y los guarda en un archivo de log para tener trazabilidad completa de qué tocó el agente | PreToolUse |
PreCompact |
Rendimiento y buenas prácticas
El problema de los hooks lentos
Los hooks síncronos corren en cada evento que coincida con su matcher. Un hook de PostToolUse sobre "Edit|Write" se ejecuta después de cada edición de archivo. Si ese hook tarda 3 segundos, cada edición del agente tarda 3 segundos más. En una sesión de refactorización donde el agente edita 20 archivos, eso es un minuto perdido.
La regla práctica: un PostToolUse no debería tardar más de 500ms. Un PreToolUse de gate de seguridad no más de 200ms. Si tu validación necesita más tiempo, hay dos opciones:
- Usar
"async": truesi el resultado no necesita bloquear el flujo - Filtrar con precisión el matcher para que el hook solo corra en los archivos donde realmente importa, no en todos
Estructura recomendada de scripts
En lugar de poner la lógica directamente en el "command" del JSON — que rápidamente se vuelve ilegible — es mejor tener un script dedicado en .claude/hooks/:
.claude/
├── settings.json ← solo la configuración (matchers y referencias a scripts)
├── hooks/
│ ├── checkstyle.sh ← lógica de Checkstyle
│ ├── test-gate.sh ← gate de tests antes de commit
│ ├── antipatterns.sh ← detección de anti-patrones
│ └── notify-done.sh ← notificaciones
└── commands/ ← custom commands (tema del siguiente artículo)
Todos los scripts deben tener permisos de ejecución:
chmod +x .claude/hooks/*.sh
Diagnóstico y debugging
# Ver los hooks configurados y cuántos hay por evento
/hooks
# Ver el output en tiempo real (verbose mode)
Ctrl+O
# Desactivar todos los hooks temporalmente para descartar interferencias
# Editar settings.json y añadir:
"disableAllHooks": true
Conclusión
Los hooks son el mecanismo que transforma Claude Code de una herramienta colaborativa en una herramienta disciplinada. La colaboración está en los prompts y el CLAUDE.md; la disciplina está en los hooks.
Los tres patrones de uso con los que te recomiendo empezar, en orden de menor a mayor complejidad:
- Formateo automático (
PostToolUsesobreEdit|Write) — el hook más sencillo y con mayor retorno inmediato. Todo el código generado por el agente queda formateado sin que tengas que pedírselo. - Gate de tests antes de commit (
PreToolUsesobreBashcon exit 2) — el hook más importante para calidad. Ningún commit sale si los tests están rotos, sin excepciones. - Notificaciones en Notification/Stop — el hook que mejora más la ergonomía diaria. Inicias una tarea larga, te alejas del teclado, y recibes una notificación cuando el agente necesita tu atención.
En el próximo artículo de la serie veremos Skills: la capa de conocimiento modular que complementa a los hooks. Si los hooks garantizan cómo trabaja el agente, las Skills definen qué sabe el agente sobre dominios específicos de tu proyecto.
Descarga los archivos de este tutorial desde mi repositorio en GitHub:© javatutoriales.com – Serie: Desarrollo Asistido por IA