15. Hooks y automatización del ciclo de vida

Objetivo del tema

Automatizar reacciones deterministas ante eventos de Claude Code: cargar contexto, examinar llamadas antes de ejecutarlas, formatear o validar cambios, registrar actividad y verificar criterios de finalización. Aprenderemos a seleccionar el evento correcto, procesar su entrada JSON y devolver una respuesta segura sin convertir cada turno en una cadena lenta o frágil.

15.1 Qué problema resuelve un Hook

Una instrucción como “ejecuta el formateador después de editar Python” depende de que el modelo la recuerde y decida aplicarla. Un Hook registra una reacción en un punto concreto del ciclo de vida; cuando ocurre el evento y coincide el filtro, Claude Code ejecuta el manejador. Esta diferencia vuelve al Hook apropiado para acciones repetitivas, observabilidad y controles verificables.

Instrucción

Explica una convención o preferencia al modelo. Es flexible, pero no garantiza ejecución.

Skill

Empaqueta un procedimiento que el usuario o Claude invoca cuando corresponde.

Hook

Reacciona automáticamente a un evento con código, HTTP, MCP o evaluación mediante modelo.

Permiso

Establece una frontera de capacidad. Una regla deny sigue siendo preferible para prohibiciones simples.

Criterio: usa un Hook cuando puedas completar la frase “cada vez que ocurra X, comprueba o ejecuta Y”. Si la acción depende de la intención del usuario, probablemente corresponda a una Skill; si solo comunica una convención, a CLAUDE.md.

15.2 Anatomía, ubicación y resolución

Una configuración tiene tres niveles: el evento selecciona el punto del ciclo de vida, el grupo con matcher filtra los casos y cada manejador define qué se ejecuta. Todos los manejadores coincidentes se ejecutan en paralelo.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/format_file.py\"",
            "timeout": 20
          }
        ]
      }
    ]
  }
}
Dónde puede declararse un Hook
UbicaciónAlcanceVersionable
~/.claude/settings.jsonTodos los proyectos del usuarioNo con el proyecto
.claude/settings.jsonProyecto
.claude/settings.local.jsonProyecto y usuario actualesNormalmente no
Configuración administradaOrganizaciónGestionada centralmente
hooks/hooks.json de un pluginDonde se habilite el pluginDentro del plugin
Frontmatter de Skill o agenteMientras el componente esté activo

No existe un archivo independiente .claude/hooks.json para la configuración normal: los Hooks del proyecto viven bajo la clave hooks de settings.json. Los scripts auxiliares sí pueden organizarse libremente en .claude/hooks/.

15.3 Mapa del ciclo de vida

SesiónSessionStart
InstructionsLoaded
EntradaUserPromptSubmit
UserPromptExpansion
HerramientasPreToolUse
PermissionRequest
PostToolUse
AgentesSubagentStart
SubagentStop
CierreStop
SessionEnd
Eventos de uso frecuente
EventoMomentoUso típico¿Puede bloquear?
SessionStartInicio, reanudación, limpieza o compactaciónInyectar contexto dinámico y preparar entornoNo
UserPromptSubmitAntes de procesar el promptAgregar contexto o rechazar entradas
PreToolUseAntes de una herramientaPermitir, preguntar, negar o modificar entrada
PermissionRequestCuando aparecería un diálogoResolver aprobaciones mediante política externa
PostToolUseDespués de una herramienta exitosaFormatear, registrar o aportar feedbackNo revierte lo ejecutado
PostToolUseFailureDespués de un fallo de herramientaDiagnóstico y telemetríaNo
StopCuando Claude intenta finalizarVerificar criterios antes de detenerse
NotificationAl emitir una notificaciónAvisos de escritorio o integracionesNo
ConfigChangeCuando cambia configuración durante la sesiónAuditar o rechazar modificacionesSí, salvo política administrada
SessionEndAl terminar una sesiónLimpieza y cierre de registrosNo

El momento importa: PreToolUse puede impedir una operación; PostToolUse solo puede informar después de que sucedió. Un formateador pertenece después de editar; una protección contra una ruta sensible debe actuar antes.

15.4 Matchers y filtros precisos

El campo matcher es una cadena, no una lista JSON. Edit|Write coincide con cualquiera de las dos herramientas y los nombres distinguen mayúsculas. Omitir el campo, usar una cadena vacía o * significa “todos” en los eventos que admiten matcher.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "if": "Edit(**/*.py)",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/format_file.py\""
          }
        ]
      }
    ]
  }
}

En eventos de herramientas, matcher compara el nombre y el campo opcional if usa sintaxis de permisos para afinar nombre y argumentos, por ejemplo Bash(git *) o Edit(**/*.ts). Para herramientas MCP usa su nombre completo, como mcp__github__create_issue.

UserPromptSubmit, PostToolBatch, Stop, TaskCreated, TaskCompleted, WorktreeCreate y otros eventos globales no admiten matcher; si lo agregas será ignorado. Consulta el evento concreto antes de asumir que el filtro se aplica.

15.5 Entrada, salida y códigos de proceso

Un Hook de comando recibe por stdin un objeto JSON. Todos los eventos incluyen datos comunes como session_id, cwd y hook_event_name; cada evento añade campos propios. Una llamada Bash previa a ejecutarse puede llegar así:

{
  "session_id": "abc123",
  "cwd": "/proyectos/tienda",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  },
  "tool_use_id": "toolu_01ABC"
}
Contrato básico de un Hook de comando
ResultadoSignificado generalDetalle
Código 0, sin JSONSin objeciónNo equivale a aprobar: continúa el flujo normal de permisos.
Código 2Bloqueo en eventos que lo permitenEscribe el motivo en stderr.
Código 0 y JSON válidoControl estructuradoPermite decidir, agregar contexto o modificar campos según el evento.
Otro códigoError no bloqueante en la mayoría de los eventosSu comportamiento tiene excepciones; no lo uses como política.

Para PreToolUse, una negación estructurada se devuelve dentro de hookSpecificOutput:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "La ruta pertenece a configuración de producción"
  }
}

No mezcles mecanismos sin necesidad: utiliza código 2 con un mensaje en stderr para un bloqueo sencillo, o salida JSON con código 0 cuando necesites control estructurado.

15.6 Primer control: proteger archivos de producción

Crearemos .claude/hooks/protect_production.py. El script normaliza la ruta recibida y bloquea ediciones directas dentro de config/production:

import json
import sys
from pathlib import Path

event = json.load(sys.stdin)
candidate = event.get("tool_input", {}).get("file_path")

if not candidate:
    raise SystemExit(0)

project = Path(event["cwd"]).resolve()
protected = (project / "config" / "production").resolve()
target = Path(candidate).resolve()

try:
    target.relative_to(protected)
except ValueError:
    raise SystemExit(0)

print(
    "Bloqueado: modifica configuración de producción mediante el pipeline aprobado.",
    file=sys.stderr,
)
raise SystemExit(2)

Lo registramos antes de Edit y Write:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/protect_production.py\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Este Hook cubre las herramientas indicadas, pero un proceso lanzado mediante Bash podría modificar el mismo archivo. Para una frontera fuerte combina permisos, sandbox, protecciones del repositorio y controles del sistema operativo. Un Hook no debe presentarse como sandbox universal.

15.7 Automatizar calidad después de una edición

Un PostToolUse recibe tanto tool_input como tool_response. El siguiente script formatea únicamente el archivo Python recién editado y evita construir una orden de shell con una ruta no confiable:

import json
import subprocess
import sys
from pathlib import Path

event = json.load(sys.stdin)
raw_path = event.get("tool_input", {}).get("file_path")

if not raw_path:
    raise SystemExit(0)

path = Path(raw_path)
if path.suffix != ".py" or not path.is_file():
    raise SystemExit(0)

result = subprocess.run(
    ["python", "-m", "ruff", "format", str(path)],
    text=True,
    capture_output=True,
    timeout=15,
)

if result.returncode != 0:
    print(result.stderr or "Ruff no pudo formatear el archivo", file=sys.stderr)
    raise SystemExit(2)

La configuración puede reutilizar el grupo Edit|Write:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/format_file.py\"",
            "timeout": 20
          }
        ]
      }
    ]
  }
}

Un PostToolUse con código 2 aporta feedback para que Claude corrija, pero la herramienta original ya terminó. Tampoco detecta archivos modificados mediante Bash o por procesos externos. Para cobertura completa, inspecciona el árbol con git status --porcelain al final del turno o utiliza FileChanged cuando corresponda.

15.8 Inicio, contexto, finalización y tareas asíncronas

SessionStart sirve para contexto dinámico que cambia entre sesiones: rama actual, incidencias asignadas o estado de dependencias. El texto escrito en stdout se incorpora al contexto. La información estática sigue perteneciendo a CLAUDE.md.

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume|compact",
        "hooks": [
          {
            "type": "command",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/session_context.py\"",
            "timeout": 10
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/check_completion.py\"",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Un Hook Stop puede impedir que Claude finalice si devuelve código 2, pero debe comprobar stop_hook_active, que indica si ya está continuando por un Hook de parada. Sin esa defensa puede provocar bucles. Define un criterio alcanzable y explica qué falta; Claude Code impone además un límite de continuaciones consecutivas.

Para telemetría, notificaciones o pruebas largas que no controlan el evento, los Hooks de comando admiten "async": true. Claude continúa sin esperar y la salida se entrega más adelante. Un Hook asíncrono no puede bloquear ni modificar la acción y cada activación crea un proceso independiente: evita dispararlo después de cada pulsación o edición pequeña sin deduplicación propia.

15.9 Cinco tipos de manejadores

Elegir el ejecutor adecuado
TipoQué haceUso recomendado
commandEjecuta un programa local que procesa JSON.Validaciones deterministas, formato, auditoría y notificaciones.
httpEnvía el evento por POST a un endpoint.Política o auditoría centralizada; limita URL, secretos y tiempo.
mcp_toolInvoca una herramienta de un servidor MCP conectado.Integración estructurada con servicios ya configurados.
promptSolicita una evaluación de un turno a un modelo.Decisiones semánticas breves que no se expresan con reglas.
agentInicia un subagente con herramientas para investigar.Verificaciones contextuales complejas; actualmente experimental.
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Decide si la respuesta afirma haber ejecutado pruebas sin aportar evidencia. Responde con {\"ok\": true} o {\"ok\": false, \"reason\": \"...\"}. $ARGUMENTS",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Los Hooks con modelo agregan latencia, costo y variabilidad. No los uses para comprobar algo que una expresión, un analizador o una prueba puede decidir. Para producción, la documentación recomienda preferir manejadores de comando frente a los Hooks de agente experimentales.

15.10 Caso integrador: puerta de calidad del proyecto

Diseñaremos una automatización por etapas, sin ejecutar la suite completa después de cada archivo:

PreToolUseProtege configuración de producción.
PostToolUseFormatea el archivo recién editado.
StopRevisa el conjunto de cambios y pruebas rápidas.
SessionEndCierra el registro de auditoría.
.claude/
├── settings.json
└── hooks/
    ├── protect_production.py
    ├── format_file.py
    ├── check_completion.py
    └── audit_session.py
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/protect_production.py\"",
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/format_file.py\"",
            "timeout": 20
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/check_completion.py\"",
            "timeout": 90
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "python \"${CLAUDE_PROJECT_DIR}/.claude/hooks/audit_session.py\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Prueba cada pieza por separado antes de unirla. Alimenta los scripts con muestras JSON guardadas, verifica sus códigos de salida y luego provoca el evento en una sesión descartable. El repositorio debe documentar dependencias como Python y Ruff; un Hook que solo funciona en la máquina de su autor no constituye una política de equipo.

15.11 Seguridad, rendimiento y portabilidad

Los Hooks ejecutan código con las credenciales y permisos del usuario que inicia Claude Code. Un archivo de configuración clonado puede incluir comandos; revisa el workspace antes de confiar en él. No registres prompts completos, contenidos de archivos, tokens o respuestas de herramientas sin una política de retención.

Riesgos y mitigaciones
RiesgoMitigación
Inyección de shell mediante rutas o texto del eventoParsea JSON y usa ejecución con lista de argumentos; no concatenes entrada en una orden.
Hook lento en cada prompt o herramientaFiltra pronto, fija timeout y reserva lo costoso para Stop o CI.
Procesos asíncronos duplicadosImplementa bloqueo o agrupación; no hay deduplicación entre activaciones.
Bucle de StopComprueba stop_hook_active y limita reintentos.
Solo se cubre Bash en WindowsSi inspeccionas comandos, considera Bash|PowerShell.
Dependencias ausentesFalla con diagnóstico claro y documenta versiones o usa entorno reproducible.
Logs con información sensibleMinimiza, redacta, limita acceso y define caducidad.
Falsa sensación de fronteraComplementa Hooks con permisos, sandbox, CI y protecciones del repositorio.

El filtro if ayuda a decidir cuándo ejecutar un Hook, pero no sustituye una regla de permisos para una prohibición estricta. Los comandos de shell pueden contener operadores, sustituciones y variables difíciles de clasificar; aplica defensa en profundidad.

15.12 Depuración, operación y checklist final

Usa /hooks para inspeccionar eventos, filtros, tipos y archivos de origen; el menú es de solo lectura. Activa modo verboso con Ctrl+O para ver ejecución y salida, y utiliza claude --debug o /debug cuando necesites el registro completo. Los cambios en settings suelen detectarse automáticamente.

/hooks
/debug
/status

claude --debug
Comprobación antes de compartir un Hook
ControlResultado esperado
EventoOcurre antes o después de la acción según la intención real.
MatcherEs una cadena, usa nombres exactos y no figura en eventos que lo ignoran.
EntradaEl script tolera campos ausentes, rutas con espacios y JSON inesperado.
SalidaCódigo, stdout y stderr respetan el contrato del evento.
SeguridadNo evalúa entrada como shell ni expone secretos.
TiempoTiene filtro, timeout y costo proporcional a su frecuencia.
PortabilidadRuntime, shell, dependencias y comportamiento en Windows están definidos.
PruebasCasos permitido, bloqueado, error y dato incompleto fueron comprobados.
EscapeExiste una forma documentada de desactivar y diagnosticar sin perder la política administrada.

Para desactivar temporalmente los Hooks configurables usa "disableAllHooks": true. Esto no puede desactivar Hooks impuestos mediante configuración administrada desde un nivel inferior. Para eliminar uno, borra su entrada del archivo correspondiente; no existe un interruptor individual estándar.

Principio central: un buen Hook es pequeño, predecible y observable. Recibe un evento preciso, realiza una sola comprobación, termina rápido y explica el resultado. Las políticas críticas se apoyan además en permisos y aislamiento.

Consulta la documentación oficial de automatización con Hooks, la referencia completa de eventos y salidas, la guía para depurar configuración y las recomendaciones de seguridad.

Conclusión: los Hooks convierten momentos del ciclo de vida en puntos de integración confiables. Seleccionar bien el evento, filtrar con precisión y tratar la entrada como no confiable permite automatizar calidad y observabilidad sin entregar control innecesario. En el próximo tema conectaremos Claude Code con herramientas externas mediante servidores MCP y Skills.