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.
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.
Explica una convención o preferencia al modelo. Es flexible, pero no garantiza ejecución.
Empaqueta un procedimiento que el usuario o Claude invoca cuando corresponde.
Reacciona automáticamente a un evento con código, HTTP, MCP o evaluación mediante modelo.
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.
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
}
]
}
]
}
}
| Ubicación | Alcance | Versionable |
|---|---|---|
~/.claude/settings.json | Todos los proyectos del usuario | No con el proyecto |
.claude/settings.json | Proyecto | Sí |
.claude/settings.local.json | Proyecto y usuario actuales | Normalmente no |
| Configuración administrada | Organización | Gestionada centralmente |
hooks/hooks.json de un plugin | Donde se habilite el plugin | Dentro del plugin |
| Frontmatter de Skill o agente | Mientras el componente esté activo | Sí |
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/.
SessionStartInstructionsLoadedUserPromptSubmitUserPromptExpansionPreToolUsePermissionRequestPostToolUseSubagentStartSubagentStopStopSessionEnd| Evento | Momento | Uso típico | ¿Puede bloquear? |
|---|---|---|---|
SessionStart | Inicio, reanudación, limpieza o compactación | Inyectar contexto dinámico y preparar entorno | No |
UserPromptSubmit | Antes de procesar el prompt | Agregar contexto o rechazar entradas | Sí |
PreToolUse | Antes de una herramienta | Permitir, preguntar, negar o modificar entrada | Sí |
PermissionRequest | Cuando aparecería un diálogo | Resolver aprobaciones mediante política externa | Sí |
PostToolUse | Después de una herramienta exitosa | Formatear, registrar o aportar feedback | No revierte lo ejecutado |
PostToolUseFailure | Después de un fallo de herramienta | Diagnóstico y telemetría | No |
Stop | Cuando Claude intenta finalizar | Verificar criterios antes de detenerse | Sí |
Notification | Al emitir una notificación | Avisos de escritorio o integraciones | No |
ConfigChange | Cuando cambia configuración durante la sesión | Auditar o rechazar modificaciones | Sí, salvo política administrada |
SessionEnd | Al terminar una sesión | Limpieza y cierre de registros | No |
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.
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.
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"
}
| Resultado | Significado general | Detalle |
|---|---|---|
Código 0, sin JSON | Sin objeción | No equivale a aprobar: continúa el flujo normal de permisos. |
Código 2 | Bloqueo en eventos que lo permiten | Escribe el motivo en stderr. |
Código 0 y JSON válido | Control estructurado | Permite decidir, agregar contexto o modificar campos según el evento. |
| Otro código | Error no bloqueante en la mayoría de los eventos | Su 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.
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.
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.
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.
| Tipo | Qué hace | Uso recomendado |
|---|---|---|
command | Ejecuta un programa local que procesa JSON. | Validaciones deterministas, formato, auditoría y notificaciones. |
http | Envía el evento por POST a un endpoint. | Política o auditoría centralizada; limita URL, secretos y tiempo. |
mcp_tool | Invoca una herramienta de un servidor MCP conectado. | Integración estructurada con servicios ya configurados. |
prompt | Solicita una evaluación de un turno a un modelo. | Decisiones semánticas breves que no se expresan con reglas. |
agent | Inicia 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.
Diseñaremos una automatización por etapas, sin ejecutar la suite completa después de cada archivo:
.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.
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.
| Riesgo | Mitigación |
|---|---|
| Inyección de shell mediante rutas o texto del evento | Parsea JSON y usa ejecución con lista de argumentos; no concatenes entrada en una orden. |
| Hook lento en cada prompt o herramienta | Filtra pronto, fija timeout y reserva lo costoso para Stop o CI. |
| Procesos asíncronos duplicados | Implementa bloqueo o agrupación; no hay deduplicación entre activaciones. |
Bucle de Stop | Comprueba stop_hook_active y limita reintentos. |
| Solo se cubre Bash en Windows | Si inspeccionas comandos, considera Bash|PowerShell. |
| Dependencias ausentes | Falla con diagnóstico claro y documenta versiones o usa entorno reproducible. |
| Logs con información sensible | Minimiza, redacta, limita acceso y define caducidad. |
| Falsa sensación de frontera | Complementa 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.
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
| Control | Resultado esperado |
|---|---|
| Evento | Ocurre antes o después de la acción según la intención real. |
| Matcher | Es una cadena, usa nombres exactos y no figura en eventos que lo ignoran. |
| Entrada | El script tolera campos ausentes, rutas con espacios y JSON inesperado. |
| Salida | Código, stdout y stderr respetan el contrato del evento. |
| Seguridad | No evalúa entrada como shell ni expone secretos. |
| Tiempo | Tiene filtro, timeout y costo proporcional a su frecuencia. |
| Portabilidad | Runtime, shell, dependencias y comportamiento en Windows están definidos. |
| Pruebas | Casos permitido, bloqueado, error y dato incompleto fueron comprobados. |
| Escape | Existe 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.