Claude Code Hooks: calidad determinista para garantizar proyectos mantenibles

Cómo usar Claude Code Hooks para añadir gates de calidad que el modelo no puede saltarse: lint, tests, OpenAPI y seguridad antes de cada commit.

Colaboradores: Ivan Garcia Villar

He visto a developers generar una feature completa con IA en 15 minutos. Funciona perfectamente en el momento. Tres meses después, nadie quiere tocar ese módulo porque se ha convertido en un nudo de condicionales anidados y dependencias cruzadas. Aquí explico cómo evitar ese patrón usando Claude Code Hooks: controles de calidad que se ejecutan en puntos deterministas del ciclo y que el modelo no puede ignorar, aunque quiera.

Por qué la IA genera complejidad accidental

Hay una diferencia importante entre “fácil” y “simple”. Un agente puede generar un endpoint que resuelve cinco casos de negocio en un bloque monolítico de 200 líneas. Es fácil: funciona inmediatamente, los tests pasan, el usuario está contento. Pero no es simple: cada nuevo requisito obliga a tocar el mismo bloque, los efectos laterales se multiplican, y quien llega al código seis meses después tiene que entender todo para cambiar cualquier cosa.

El problema no es solo el modelo. En la mayoría de los proyectos donde he visto esta degradación, hay tres causas concretas: el agente no tiene suficiente contexto sobre la estructura existente, el código previo ya acumula malas prácticas que el agente sigue como referencia, o simplemente el modelo no encuentra los archivos relevantes y opta por duplicar lógica en lugar de reutilizarla. La IA optimiza para resolver el prompt actual. Tu trabajo es optimizar para el proyecto a largo plazo.

Los hooks son una parte de esa respuesta. No la única, pero sí la más infrautilizada.

Qué son los Claude Code Hooks

Los hooks son comandos de shell, endpoints HTTP, prompts o agentes que se ejecutan automáticamente en puntos específicos del ciclo de Claude Code. La diferencia fundamental con “decirle a Claude que haga algo”: los hooks no dependen de que el modelo decida ejecutarlos. Son reglas inflexibles que corren independientemente de lo que el modelo haya planeado.

Se configuran en .claude/settings.json (con scope de proyecto, se puede commitear al repo) o en ~/.claude/settings.json (scope global, aplica a todos tus proyectos). También puedes gestionarlos interactivamente con el comando /hooks dentro de Claude Code.

La estructura base es esta:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-changed-file.sh"
          }
        ]
      }
    ]
  }
}

Tres niveles de anidamiento: el evento (PostToolUse), el matcher group que filtra cuándo aplica (Edit|Write), y el handler que define qué ejecutar. El matcher es una expresión regular contra el nombre de la herramienta. $CLAUDE_PROJECT_DIR es una variable de entorno que Claude Code inyecta con la ruta raíz del proyecto.

Los eventos más útiles para calidad de código son PreToolUse (antes de ejecutar una herramienta, puede bloquear), PostToolUse (después de que se ejecuta con éxito) y Stop (cuando Claude termina de responder). El ciclo completo incluye muchos más, desde SessionStart hasta WorktreeCreate, pero estos tres cubren el 90% de los casos de calidad.

Los tres tipos de hook que uso en producción

Hooks de comando: la base determinista

El tipo command ejecuta un script de shell. El script recibe el contexto del evento como JSON en stdin y comunica el resultado vía exit code: 0 permite continuar, 2 bloquea con un mensaje de error que Claude ve y puede usar para corregirse, cualquier otro código falla silenciosamente sin bloquear.

Un ejemplo concreto: ejecutar lint solo sobre el archivo que Claude acaba de modificar.

#!/bin/bash
# .claude/hooks/lint-changed-file.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Solo lintear archivos TypeScript/JavaScript
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.js ]]; then
  exit 0
fi

if ! npx eslint "$FILE_PATH" --max-warnings 0 2>&1; then
  echo "Lint falló en $FILE_PATH. Corrige los errores antes de continuar." >&2
  exit 2
fi

exit 0

El JSON que llega en stdin incluye tool_input.file_path para eventos de escritura, tool_input.command para Bash, y session_id, cwd, y hook_event_name en todos los eventos. Puedes extraer cualquiera de estos campos con jq para condicionar el comportamiento.

Hooks de prompt: aplicando IA para auto-evaluar

Para condiciones que un linter no puede evaluar, el tipo prompt envía un prompt a un modelo Claude (Haiku por defecto, configurable con el campo model) y espera una decisión JSON con formato {"ok": true} o {"ok": false, "reason": "..."}.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "¿Este comando bash podría afectar producción? Evalúa si el comando en $ARGUMENTS contiene operaciones destructivas, acceso a bases de datos en vivo, o cambios de permisos. Si hay riesgo, responde {\"ok\": false, \"reason\": \"Comando potencialmente peligroso: [detalle]\"}. Si es seguro, {\"ok\": true}.",
            "model": "claude-haiku-4-5"
          }
        ]
      }
    ]
  }
}

El placeholder $ARGUMENTS se reemplaza con el JSON completo del evento antes de enviarlo al modelo. Úsalo para darle contexto sobre qué herramienta se ejecutó y con qué parámetros.

Nota importante sobre qué NO hacer con hooks de prompt: Detectar niveles de anidamiento, líneas de código excesivas, o cualquier métrica algorítmica es algo que un linter o un script de command puede hacer de forma determinística y mucho más rápida (con una regex o un AST parser). Usar un modelo LLM para algo medible algorítmicamente es un desperdicio de latencia y tokens. Usa hooks de prompt solo para decisiones semánticas que requieren razonamiento: “¿este comando bash podría afectar producción?” es válido porque requiere entender el contexto de negocio. “¿esta función tiene más de 4 niveles de anidamiento?” no lo es: usa un script de shell o un linter.

Hooks de agente: verificación con acceso al código real

Cuando la verificación requiere inspeccionar archivos, buscar patrones o ejecutar comandos, el tipo agent lanza un subagente con acceso a herramientas (Read, Grep, Glob, Bash) que puede investigar el estado real del proyecto antes de devolver su decisión.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

El hook de tipo agent tiene un timeout por defecto de 60 segundos y puede hacer hasta 50 turns con herramientas. La diferencia con el hook de prompt: mientras el prompt evalúa solo el contexto del evento, el agente puede abrir archivos, buscar en el código y ejecutar comandos antes de decidir.

¿Por qué usar prompt y no agent? Esta es la pregunta clave, y la respuesta es coste y velocidad. Usa prompt cuando solo necesitas evaluar los datos del evento (el JSON que ya llega por $ARGUMENTS), la decisión es semántica pero no requiere inspeccionar el código fuente, y quieres latencia baja (una sola llamada LLM, típicamente Haiku = rápido y barato). Usa agent cuando necesitas verificar el estado real del codebase (leer archivos, ejecutar tests, buscar patrones). Los agentes usan el mismo formato de respuesta ok/reason que los prompts, pero con un timeout más largo de 60 segundos y hasta 50 turns de uso de herramientas.

La tabla siguiente resume las diferencias entre los tres tipos:

TipoQué ejecutaCuándo usarloCosteLatencia
commandScript de shellReglas deterministas: lint, formato, métricas algorítmicasMínimo1-3 seg
promptModelo LLM (Haiku)Decisiones semánticas sin inspeccionar código: “¿comando peligroso?”, “¿nombre claro?”Bajo2-5 seg
agentSubagente con toolsVerificación contra código real: tests, dependencias, análisis de seguridadMedio30-120 seg

Regla práctica: Si puedes responder la pregunta con un regex, un parser AST, o un linter existente → usa command. Si necesitas razonamiento semántico pero solo sobre los datos del evento → usa prompt. Si necesitas leer archivos, ejecutar comandos o explorar el proyecto → usa agent.

Un hook de comando que corre lint tarda 1-3 segundos. Un hook de agente que corre el suite de tests puede tardar 30-120 segundos. Ajusta los tipos según la velocidad que necesites en cada punto del ciclo.

Ejemplos prácticos de hooks de pre-commit

El caso donde más valor he sacado a los hooks es como gate antes de que Claude declare la tarea como completada. Usando el evento Stop con un agente verificador, puedo obligar a que se cumplan condiciones concretas antes de que el modelo diga “terminé”.

Estos son los tres checks que tengo activos en mis proyectos:

1. Tests obligatorios antes de finalizar

El más básico. Un hook Stop con type: "agent" que corre el suite de tests y bloquea si alguno falla.

2. Documentación OpenAPI actualizada

Lo uso para que compruebe si los cambios realizados son en las rutas de la API y, si lo son, hago que compruebe si los ficheros de documentación se han actualizado correctamente o faltan cosas por actualizar. No lo hago mirando la fecha, sino que hago que el modelo analice el código directamente.

Esto se puede hacer mediante un script shell, pero también mediante un hook que levanta un agente:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Check all the documents modified in the commit and verify the documentation is updated following the standards defined in the skill ....",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

De esta forma, el agente analiza el código de los cambios realizados en las rutas del API y verifica que la documentación correspondiente (OpenAPI spec, README, documentación de endpoints, etc.) ha sido actualizada correctamente, sin depender únicamente de metadatos de fechas de modificación.

3. Análisis de seguridad básico

Un hook de agente en Stop que busca patrones problemáticos antes de que Claude termine: credenciales hardcodeadas, queries SQL sin parametrizar, o endpoints sin autenticación añadidos en el diff. Este tipo de verificación requiere inspeccionar el código real, por lo que el tipo agent es el adecuado:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Revisa los archivos modificados en esta sesión buscando: 1) credenciales o secretos hardcodeados, 2) queries SQL con concatenación de strings en lugar de parámetros, 3) endpoints nuevos sin middleware de autenticación. Usa las herramientas para leer los archivos modificados. Si encuentras algún problema, responde {\"ok\": false, \"reason\": \"[descripción del problema y archivo\"}. Si todo está bien, {\"ok\": true}.",
            "timeout": 90
          }
        ]
      }
    ]
  }
}

El mínimo viable para cualquier proyecto que va a durar más de tres meses: hook de tests en Stop. Sin eso, cualquier cosa que Claude genere puede romper el código existente sin que nadie se entere hasta que alguien corra los tests manualmente.

El coste de tokens no es un problema, es un trade-off

Los hooks basados en modelos o agentes consumen tokens adicionales. Un hook de prompt con Haiku evaluando complejidad de función puede costar 200-400 tokens por invocación. Un hook de agente corriendo el suite de tests, 2000-5000 tokens. En proyectos con muchos cambios por sesión, esto se acumula.

El framing correcto no es “¿puedo permitirme este coste?” sino “¿qué me cuesta no tenerlo?”. Un bug de seguridad que llegó a producción porque nadie verificó el código generado por IA cuesta más en tiempo de debugging, daño de reputación e incidentes que semanas de uso de hooks. Para desarrollo serio a largo plazo, el coste de tokens es un peaje justificable.

Lo que sí tiene sentido optimizar es qué tipo de hook usar donde. No necesitas un agente para hacer lint; un script de shell es más rápido y predecible. Los hooks de agente deben reservarse para verificaciones que genuinamente requieren acceso al código: tests de integración, consistencia entre módulos, análisis de seguridad.

Errores comunes

Hooks demasiado estrictos desde el principio

El error más frecuente al empezar: configurar un PreToolUse hook que bloquea el 80% de las acciones de Claude porque las reglas son demasiado agresivas. El síntoma es Claude en un bucle, intentando hacer la misma acción de formas ligeramente distintas.

Empieza permisivo. Un PostToolUse hook que ejecuta lint pero no bloquea (tipo command con exit 0 siempre, mostrando advertencias en stderr) es un buen primer paso. Convierte las advertencias en bloqueos solo cuando hayas calibrado que las reglas generan cero falsos positivos en tu proyecto.

Tests completos como PostToolUse hook

Configurar un hook PostToolUse que ejecuta el suite completo después de cada cambio de archivo es el camino más directo a abandonar los hooks por completo. Cada edición menor tarda dos minutos esperando que pasen 300 tests.

Divide responsabilidades según latencia esperada: usa command para lint y verificaciones deterministas en PostToolUse (1-3 segundos), usa agent para suite completo solo en el hook Stop (30-120 segundos si es necesario verificar antes de terminar). La velocidad del feedback loop importa para que los hooks sigan siendo útiles y no se perciban como fricción.

También puedes crear un SH que corra los tests de las partes afectadas en lugar de todos.

Prompts vagos en hooks de modelo

Un hook de prompt que dice “evalúa si el código es bueno y devuelve ok o no” genera respuestas inconsistentes. Haiku interpretará “bueno” de forma diferente en cada invocación. El resultado práctico son hooks que a veces bloquean, a veces no, por las mismas razones.

No uses prompts para preguntas que un linter o script puede responder algorítmicamente: “¿esta función tiene más de 30 líneas?” se verifica en O(n) con un simple contador, no necesita un LLM. “¿hay queries SQL construidas con concatenación de strings?” requiere un AST parser o regex, no razonamiento semántico. Una pregunta, una respuesta, resultado predecible — pero déjala a herramientas deterministas.

Checklist de implementación

  • Crear .claude/hooks/ en el proyecto y añadir los scripts al control de versiones
  • Configurar un PostToolUse hook con matcher Edit|Write para lint del archivo modificado
  • Configurar un Stop hook con type: "agent" para verificar que el suite de tests pasa
  • Ajustar el timeout del hook de tests al tiempo real del suite (default: 60s)
  • Hacer los scripts ejecutables con chmod +x .claude/hooks/*.sh
  • Verificar que los hooks aparecen en /hooks y se disparan correctamente
  • Añadir hook de seguridad para proyectos con datos sensibles o endpoints públicos
  • Documentar en CLAUDE.md qué hace cada hook y por qué existe

Fuentes

  1. Hooks reference — Claude Code documentation — esquemas de entrada/salida, tipos de hook, eventos del ciclo y ejemplos de configuración.
  2. Automate workflows with hooks — Claude Code guide — guía práctica con ejemplos listos para usar y troubleshooting común.

Preguntas Frecuentes

¿Por qué los hooks y no solo un buen CLAUDE.md?

El CLAUDE.md le dice al modelo cómo debería comportarse. Los hooks definen lo que ocurre independientemente de cómo el modelo decida comportarse. Para contexto y preferencias de estilo, CLAUDE.md es la herramienta correcta. Para garantías de calidad que no pueden romperse, los hooks son la respuesta. Úsalos juntos: CLAUDE.md para instrucciones, hooks para invariantes.

¿Los hooks funcionan con Claude Code en terminal, Cursor y otros editores?

Los hooks son scripts de shell que se ejecutan independientemente del editor. Se configuran en .claude/settings.json del proyecto y funcionan con cualquier herramienta que use Claude Code: terminal, integraciones en editores, o incluso pipelines automatizados vía API. La única excepción documentada: los hooks PermissionRequest no se disparan en modo no interactivo (-p). Para pipelines automatizados, usa PreToolUse en su lugar.

¿Qué pasa si el hook de verificación falla continuamente y bloquea todo?

Revisa el mensaje de error que aparece en el transcript de Claude. El texto que escribes en stderr en exit 2 llega directamente al modelo como feedback. Si el hook rechaza sistemáticamente, o las reglas son demasiado estrictas para el estado actual del proyecto, o hay un bug en el script. Puedes deshabilitar todos los hooks temporalmente con "disableAllHooks": true en el settings mientras depuras. Para debugging detallado, corre claude --debug o activa verbose mode con Ctrl+O.

¿Puedo compartir los hooks de un proyecto con todo el equipo?

Sí. Los hooks en .claude/settings.json se pueden commitear al repositorio y aplican a cualquier miembro del equipo que use Claude Code en ese proyecto. Los hooks en ~/.claude/settings.json son locales a tu máquina. Para hooks que contienen rutas absolutas o configuraciones específicas de cada desarrollador, usa .claude/settings.local.json, que está gitignoreado por defecto. La recomendación práctica: hooks de lint y tests en el proyecto (.claude/settings.json), notificaciones de escritorio en global (~/.claude/settings.json).