26 de marzo de 2026

Claude Code: Hooks — De sugerencias a garantías

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.


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:

MecanismoEjemploNivel 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:

ArchivoAlcance¿Se versiona en Git?
~/.claude/settings.jsonGlobal — todos tus proyectosNo
.claude/settings.jsonProyecto — todo el equipo
.claude/settings.local.jsonProyecto — 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.

PreToolUse
puede bloquear

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.

PostToolUse
feedback al agente

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.

PostToolUseFailure
diagnóstico

Cuando una herramienta falla. Útil para logging de errores, diagnóstico automático y notificaciones de fallo.

PermissionRequest
puede aprobar/denegar

Cuando Claude va a mostrar un diálogo de permisos al usuario. Permite aprobar o denegar automáticamente sin intervención humana.

Stop
puede forzar continuar

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.

SubagentStop
cleanup

Cuando un subagente termina su tarea. Los hooks de seguridad del proyecto aplican recursivamente a subagentes también.

UserPromptSubmit
pre-procesado

Cuando el usuario envía un prompt. Permite agregar contexto adicional automáticamente antes de que el agente lo procese.

SessionStart
inicialización

Al inicio de cada sesión (arranque, resume, clear). Útil para cargar contexto dinámico: rama de Git activa, estado del build, issues abiertos.

SessionEnd
async

Al terminar la sesión. Para cleanup, logging de métricas, backups de la sesión o notificaciones de cierre.

Notification
enrutamiento

Cuando el agente genera una notificación. Para redirigir alertas a Slack, correo, o el sistema de notificaciones de tu sistema operativo.

PreCompact
pre-compactado

Justo antes de que el agente compacte el contexto. Permite guardar información importante antes de que se resuma.

Elicitation
intercepta MCP

Intercepta solicitudes de elicitación de servidores MCP antes de mostrarlas al usuario. Para pre-validación o pre-relleno de respuestas.

Tip — Verbose mode: Para ver en tiempo real qué hooks se están disparando y su output, activa el modo verbose con Ctrl+O durante una sesión. Muestra el stdout/stderr de PreToolUse, PostToolUse y demás eventos conforme ocurren.

Los 4 tipos de handlers

command

Ejecuta un script de shell local. Recibe el JSON del evento por stdin.

Úsalo para: formateo, linting, validaciones locales, tests, git hooks.

http

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.

prompt

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.

agent

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 codeSignificadoCuá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
El error más común al escribir hooks de seguridad: usar 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 para Stop y SessionStart
  • 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
Nota: Este hook usa 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
Nota sobre el webhook de Slack: El hook está configurado en 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.

HookEventoAplica 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": true si 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:

  1. Formateo automático (PostToolUse sobre Edit|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.
  2. Gate de tests antes de commit (PreToolUse sobre Bash con exit 2) — el hook más importante para calidad. Ningún commit sale si los tests están rotos, sin excepciones.
  3. 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