Capítulo 6: Hooks: Automatización por Eventos
1. ¿Qué son los Hooks en Claude Code?
Un hook es un script que se ejecuta automáticamente cuando Claude Code dispara un evento. La mecánica es simple: evento ocurre → el matcher evalúa si aplica → si coincide, se ejecuta el comando del hook.
flowchart LR
A[Evento Claude Code] --> B{Matcher evalúa}
B -->|No coincide| C[Herramienta continúa]
B -->|Coincide| D[Script de hook ejecuta]
D -->|Exit 0| C
D -->|Exit 1-2| E[Herramienta bloqueada]
D -->|Mensaje en stderr| F[Claude recibe feedback]
Los hooks viven en el archivo de configuración ~/.claude/settings.json (global) o en .claude/settings.json (por proyecto). Se definen bajo la clave hooks y agrupados por tipo de evento.
Hooks vs Rules vs Skills
Estos tres mecanismos se confunden fácilmente pero tienen propósitos distintos:
| Mecanismo | Cuándo aplica | Quién lo ejecuta | Puede bloquear |
|---|---|---|---|
| Rules | Siempre, en todo momento | Claude (LLM) como instrucción | No directamente |
| Skills | Cuando el usuario llama /skill | Claude ejecuta el skill | No |
| Hooks | Al ocurrir un evento específico | El sistema operativo (shell) | Sí, con exit codes |
La diferencia crítica: los hooks son código real que corre en el OS, no instrucciones para el LLM. Un hook que retorna exit code 2 bloquea físicamente la operación, sin importar lo que Claude “quiera” hacer.
Casos de uso propios de hooks:
- Validaciones que deben ser inviolables (bloquear
--no-verify) - Operaciones de sistema (formatear archivos, correr linters)
- Captura de telemetría y métricas
- Integración con herramientas externas (Slack, APIs)
Casos de uso propios de rules:
- Guías de estilo (“escribe en español”)
- Convenciones de código (“usa async/await, no callbacks”)
- Comportamiento del agente (“pregunta antes de borrar”)
2. El Sistema de Eventos de Claude Code
Claude Code expone seis puntos de extensión en su ciclo de vida. Cada evento corresponde a un momento preciso:
sequenceDiagram
participant U as Usuario
participant CC as Claude Code
participant H as Hook System
participant T as Tool
U->>CC: Inicia sesión
CC->>H: SessionStart
H-->>CC: OK / contexto cargado
U->>CC: "edita este archivo"
CC->>H: PreToolUse(Write)
H-->>CC: OK / bloqueo
CC->>T: Ejecuta Write
T-->>CC: Resultado
CC->>H: PostToolUse(Write)
H-->>CC: OK / feedback
U->>CC: Termina sesión
CC->>H: SessionEnd
H-->>CC: estado guardado
Tabla completa de eventos
| Evento | Fase | Input disponible | Puede bloquear |
|---|---|---|---|
SessionStart | Inicio de sesión | Session ID, working dir | No |
PreToolUse | Antes de ejecutar tool | Nombre tool, parámetros completos | Sí (exit 1-2) |
PostToolUse | Después de ejecutar tool | Nombre tool, parámetros, resultado | No directamente |
PreCompact | Antes de compactar contexto | Resumen del contexto | Sí (puede agregar info) |
Stop | Cuando el agente termina | Motivo de parada | No |
SessionEnd | Cierre de sesión | Session summary | No |
PreToolUse — El más poderoso
PreToolUse es el evento de mayor utilidad porque:
- Recibe el input completo antes de que la herramienta actúe
- Puede bloquear la operación completamente
- Puede modificar el comportamiento de Claude enviando mensajes via stderr
El input que recibe un hook PreToolUse tiene esta estructura JSON en stdin:
{
"tool": "Bash",
"tool_input": {
"command": "git push --no-verify origin main"
},
"session_id": "abc-123",
"cwd": "/home/user/proyecto"
}
PostToolUse — Para reaccionar
PostToolUse recibe adicionalmente el resultado de la herramienta:
{
"tool": "Edit",
"tool_input": {
"file_path": "src/index.ts",
"old_string": "const x = 1",
"new_string": "const x = 2"
},
"tool_result": {
"success": true,
"content": ""
},
"session_id": "abc-123",
"cwd": "/home/user/proyecto"
}
SessionStart — Para cargar contexto
SessionStart es el único evento que se dispara antes de cualquier interacción. Es ideal para:
- Cargar estado guardado de sesiones anteriores
- Detectar el entorno (package manager, OS, git branch)
- Inyectar contexto relevante al inicio
PreCompact — Para preservar estado crítico
Claude Code compacta el contexto automáticamente cuando se acerca al límite de tokens. PreCompact permite guardar información que de otro modo se perdería en la compactación.
3. Anatomía de un Hook
Cada entrada en el array de hooks tiene esta estructura:
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node \"/path/to/script.js\"",
"async": false,
"timeout": 10
}
],
"description": "Descripción legible del hook"
}
Campos explicados
matcher — Define a qué herramientas aplica este hook. Acepta:
- Nombre exacto:
"Bash","Edit","Write" - Pipe para OR:
"Edit|Write|MultiEdit" - Wildcard:
"*"(todas las herramientas)
hooks — Array de comandos a ejecutar. Se ejecutan en orden secuencial.
type — Actualmente solo existe "command". Ejecuta un comando shell.
command — El comando a ejecutar. Puede ser:
- Un binario:
"npx [email protected]" - Un script Node.js:
"node \"/ruta/script.js\"" - Un script bash:
"bash \"/ruta/script.sh\"" - Con variables de entorno:
"node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/script.js\""
async — Booleano opcional (default false). Si es true, el hook corre en background sin bloquear la herramienta. Útil para telemetría y análisis que no deben añadir latencia.
timeout — Segundos máximos de ejecución (default: 60). El hook se mata si supera el límite. Valores recomendados: 5s para validaciones, 15s para linters, 30s para builds.
description — Texto libre, solo documentación. No afecta el comportamiento.
4. El Sistema de Matchers
El matcher determina si un hook aplica a una herramienta específica. Claude Code evalúa los matchers antes de ejecutar cada herramienta.
Matchers disponibles en Claude Code
| Matcher | Herramienta que coincide |
|---|---|
Bash | Comandos de terminal |
Read | Lectura de archivos |
Write | Escritura de archivos (nuevo o sobreescritura) |
Edit | Edición de strings en archivo existente |
MultiEdit | Múltiples ediciones en un archivo |
Glob | Búsqueda de archivos por patrón |
Grep | Búsqueda de contenido en archivos |
LS | Listado de directorios |
TodoRead | Leer lista de tareas |
TodoWrite | Escribir lista de tareas |
WebSearch | Búsqueda web |
WebFetch | Fetch de URL |
Task | Lanzar sub-agente |
* | Todas las herramientas (wildcard) |
Operador OR con pipe
Para múltiples herramientas, usa | sin espacios:
"matcher": "Edit|Write|MultiEdit"
Esto coincide con cualquier operación que modifique archivos.
El wildcard *
El matcher "*" coincide con todas las herramientas. Se usa cuando el hook debe correr en cada operación, como los hooks de observabilidad:
{
"matcher": "*",
"hooks": [{"type": "command", "command": "bash observe.sh", "async": true}],
"description": "Capturar toda actividad del agente"
}
Cómo Claude Code evalúa los matchers
flowchart TD
A[Tool va a ejecutarse] --> B[Buscar hooks del evento]
B --> C[Para cada hook entry]
C --> D{matcher == tool?}
D -->|matcher es *| E[Siempre coincide]
D -->|matcher contiene pipe| F{tool en lista}
F -->|Sí| G[Ejecutar hook command]
F -->|No| H[Siguiente hook entry]
D -->|matcher exacto| I{tool == matcher}
I -->|Sí| G
I -->|No| H
G --> J{exit code?}
J -->|0| H
J -->|1 o 2| K[Bloquear herramienta]
E --> G
5. El run-with-flags.js Explicado
run-with-flags.js es un wrapper central en ECC (Everything Claude Code) que implementa un sistema de perfiles de activación. Antes de ejecutar el script real del hook, decide si debe correr según el perfil configurado.
Por qué existe
Sin run-with-flags.js, todos los hooks correrían siempre. En instalaciones mínimas (un laptop lento, o alguien que solo quiere features básicos), ejecutar 11 hooks en cada tool use es demasiado. El wrapper permite activar/desactivar hooks por perfil.
Los 3 perfiles
graph LR
A[minimal] --> B[standard]
B --> C[strict]
A:::profile -->|solo esenciales| D[SessionStart\nPreCompact]
B:::profile -->|balance| E[quality-gate\nsuggest-compact\nconfig-protection]
C:::profile -->|máxima seguridad| F[auto-format\ntypecheck\ngit-push-reminder]
classDef profile fill:#4a90d9,color:white
| Perfil | Variable | Descripción |
|---|---|---|
minimal | ECC_HOOK_PROFILE=minimal | Solo hooks críticos de infraestructura |
standard | ECC_HOOK_PROFILE=standard (default) | Hooks de productividad y calidad |
strict | ECC_HOOK_PROFILE=strict | Todos los hooks, máxima validación |
Sintaxis de run-with-flags.js
node "run-with-flags.js" "HOOK_ID" "scripts/hooks/mi-hook.js" "minimal,standard,strict"
- Argumento 1: ID único del hook (para logs y ECC_DISABLED_HOOKS)
- Argumento 2: Ruta relativa al script real
- Argumento 3: Lista de perfiles donde debe activarse
Cómo el wrapper decide si ejecutar
flowchart TD
A[run-with-flags.js invocado] --> B{ECC_DISABLED_HOOKS contiene ID?}
B -->|Sí| C[Exit 0 silencioso]
B -->|No| D{Leer ECC_HOOK_PROFILE}
D --> E{perfil en lista de perfiles del hook?}
E -->|No| C
E -->|Sí| F[Ejecutar script real]
F --> G[Pasar stdin y env al script]
G --> H[Retornar exit code del script]
Perfiles en la práctica
Un hook con "minimal,standard,strict" corre en todos los perfiles. Uno con "strict" solo corre cuando ECC_HOOK_PROFILE=strict. Configurar en ~/.bashrc o ~/.zshrc:
export ECC_HOOK_PROFILE=standard # Recomendado para desarrollo diario
6. PreToolUse Hooks — Análisis Completo
ECC configura 11 hooks en PreToolUse. Cada uno cumple un rol distinto en el ciclo de protección y guía del agente.
6.1 block-no-verify — Protección de Git Hooks
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "npx [email protected]"}],
"description": "Block git hook-bypass flag"
}
Qué hace: Intercepta cada comando Bash y analiza si contiene --no-verify. Si lo encuentra, retorna exit code 2 con un mensaje explicativo, bloqueando el comando.
Por qué existe: Claude Code tiene la tendencia de usar git commit --no-verify cuando los pre-commit hooks fallan. Esta es una solución peligrosa que silencia validaciones de seguridad y calidad. El hook bloquea este escape hatch.
Casos bloqueados:
git commit --no-verify -m "fix"git push --no-verifygit commit --no-verify --amend
Cómo funciona internamente: Lee stdin, parsea tool_input.command, busca la flag --no-verify con regex, retorna exit 2 si la encuentra.
Versión: Se usa via npx para siempre tener la última versión sin instalación local.
6.2 auto-tmux-dev.js — Servidores de Desarrollo en Tmux
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/auto-tmux-dev.js\""}],
"description": "Auto-start dev servers in tmux"
}
Qué hace: Detecta comandos de servidor de desarrollo (bun dev, npm run dev, yarn start, etc.) y los redirige para ejecutarse en una sesión tmux con nombre basado en el directorio del proyecto.
Por qué existe: Si Claude corre bun dev directamente, el proceso bloquea la terminal y Claude no puede continuar. En tmux, el servidor corre en background y Claude puede seguir trabajando.
Lógica de detección:
flowchart TD
A[Comando Bash recibido] --> B{Es comando de dev server?}
B -->|bun dev / npm run dev / etc| C{tmux disponible?}
B -->|Otro comando| D[Exit 0, no intervenir]
C -->|No| D
C -->|Sí| E[Calcular session name del directorio]
E --> F{Session ya existe?}
F -->|Sí| G[Attach a session existente]
F -->|No| H[Crear nueva session tmux]
H --> I[Ejecutar comando en tmux]
I --> J[Exit 2 para bloquear ejecución directa]
Naming de sesiones: Usa el nombre del directorio de trabajo para crear sesiones con nombres predecibles como proyecto-api o siemprelisto_web.
6.3 pre-bash-tmux-reminder.js — Recordatorio de Tmux
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "node ... \"pre:bash:tmux-reminder\" \"scripts/hooks/pre-bash-tmux-reminder.js\" \"strict\""}]
}
Qué hace: Detecta comandos long-running que no están siendo ejecutados en tmux y envía un recordatorio a Claude via stderr.
Por qué existe: Algunos comandos (tests que tardan, builds, etc.) deberían correr en tmux para no bloquear la conversación. Este hook educa al agente sobre este patrón.
Perfil: Solo strict, porque es un recordatorio no crítico.
Output a stderr: El mensaje va a stderr, que Claude lee como contexto adicional. Claude no está bloqueado pero recibe la sugerencia.
6.4 pre-bash-git-push-reminder.js — Revisión Antes de Push
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "node ... \"pre:bash:git-push-reminder\" \"scripts/hooks/pre-bash-git-push-reminder.js\" \"strict\""}]
}
Qué hace: Cuando detecta git push, muestra un resumen de los cambios que se van a pushear y pide confirmación implícita al agente.
Por qué existe: Claude puede hacer push automáticamente sin que el usuario haya revisado los cambios. Este hook inyecta un checkpoint de revisión.
Perfil: strict — solo para equipos con procesos de revisión estrictos.
6.5 doc-file-warning.js — Advertencia de Archivos de Documentación
{
"matcher": "Write",
"hooks": [{"type": "command", "command": "node ... \"pre:write:doc-file-warning\" \"scripts/hooks/doc-file-warning.js\" \"standard,strict\""}]
}
Qué hace: Cuando Claude va a crear o sobreescribir un archivo Markdown (.md), evalúa si el nombre/ruta es “estándar” (como README.md, CHANGELOG.md) o no estándar (como ANALYSIS.md, NOTES.md).
Por qué existe: Claude tiende a crear archivos de documentación temporales que ensucian el repositorio. Este hook advierte cuando el archivo no sigue convenciones del proyecto.
No bloquea: Envía la advertencia a stderr pero permite la operación (exit 0). Claude recibe el feedback y puede reconsiderar.
6.6 suggest-compact.js — Sugerir Compactación
{
"matcher": "Edit|Write",
"hooks": [{"type": "command", "command": "node ... \"pre:edit-write:suggest-compact\" \"scripts/hooks/suggest-compact.js\" \"standard,strict\""}]
}
Qué hace: Mantiene un contador de operaciones de escritura. Cuando supera un umbral (configurable), sugiere a Claude que considere una compactación manual del contexto.
Por qué existe: Las compactaciones automáticas de Claude Code pueden perder contexto importante. Este hook sugiere compactar en momentos “lógicos” (después de completar un bloque de trabajo) en lugar de que ocurra sorpresivamente.
Estado persistente: Guarda el contador en un archivo temporal por session ID para mantener el estado entre llamadas al hook.
6.7 observe.sh — Captura para Aprendizaje Continuo
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "bash ... \"skills/continuous-learning-v2/hooks/observe.sh\" \"standard,strict\"",
"async": true,
"timeout": 10
}]
}
Qué hace: Captura cada uso de herramienta como un evento JSONL en un archivo de log. Estos logs son procesados por el sistema de Continuous Learning para identificar patrones y generar insights.
Async con timeout 10: Se ejecuta en background. Si tarda más de 10 segundos (anomalía), se mata automáticamente. No debe añadir latencia perceptible.
Formato de captura:
{"timestamp": "2024-12-17T10:30:00Z", "tool": "Bash", "command": "git status", "session_id": "abc-123"}
6.8 insaits-security-wrapper.js — Monitor de Seguridad AI
{
"matcher": "Bash|Write|Edit|MultiEdit",
"hooks": [{
"type": "command",
"command": "node ... \"pre:insaits-security\" \"scripts/hooks/insaits-security-wrapper.js\" \"standard,strict\"",
"timeout": 15
}]
}
Qué hace: Integración opcional con InsAIts, un servicio que analiza operaciones de agentes AI en busca de comportamientos maliciosos o riesgosos. Envía el contexto de la operación a la API de InsAIts para análisis.
Condicional: Solo corre si ECC_ENABLE_INSAITS=1 está definido. Si la variable no existe, el script hace exit 0 inmediatamente sin conectarse a ninguna API.
Timeout 15s: Las llamadas de red tienen latencia. 15 segundos es suficiente para la mayoría de condiciones de red, sin bloquear indefinidamente.
6.9 config-protection.js — Protección de Configuraciones
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{
"type": "command",
"command": "node ... \"pre:config-protection\" \"scripts/hooks/config-protection.js\" \"standard,strict\"",
"timeout": 5
}]
}
Qué hace: Protege archivos de configuración de linters y formatters de ser modificados por el agente. Cuando Claude va a editar .eslintrc, .prettierrc, biome.json, tsconfig.json, etc., el hook lo bloquea.
Por qué existe: Cuando Claude no puede hacer pasar los checks de calidad, tiene la tendencia de “solucionar” el problema desactivando las reglas en la configuración. Esto es técnicamente incorrecto: el código debería corregirse, no las reglas relajarse.
Archivos protegidos (típicamente):
.eslintrc*,.eslintignore.prettierrc*,.prettierignorebiome.json,biome.jsonctsconfig*.json.stylelintrc*
Mensaje de bloqueo: Informa a Claude que debe corregir el código, no la configuración.
6.10 mcp-health-check.js — Salud de MCP Servers
{
"matcher": "*",
"hooks": [{"type": "command", "command": "node ... \"pre:mcp-health-check\" \"scripts/hooks/mcp-health-check.js\" \"standard,strict\""}]
}
Qué hace: Antes de ejecutar cualquier herramienta, verifica que los MCP servers configurados estén respondiendo. Si un MCP server está caído y el hook lo detecta, puede advertir a Claude antes de que intente usar una herramienta MCP y falle.
Caché de estado: Para no agregar latencia en cada tool use, el hook cachea el estado de los MCP servers y solo revalida cada N segundos.
7. PostToolUse Hooks — Análisis Completo
Los hooks PostToolUse no pueden bloquear operaciones (la herramienta ya ejecutó), pero pueden:
- Ejecutar acciones consecuentes (formatear el archivo que se editó)
- Inyectar feedback a Claude (calidad del código, warnings)
- Capturar métricas y telemetría
7.1 post-bash-pr-created.js — Detectar PR Creado
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "node ... \"post:bash:pr-created\" \"scripts/hooks/post-bash-pr-created.js\" \"standard,strict\""}]
}
Qué hace: Analiza el output de comandos Bash. Si detecta que el comando era gh pr create y el output contiene una URL de PR, extrae la URL y la muestra prominentemente con el comando para hacer review.
Input: Lee tool_result.content (output del bash) y tool_input.command.
Output útil para Claude:
PR creado exitosamente: https://github.com/user/repo/pull/123
Para revisar: gh pr view 123 --web
7.2 post-bash-build-complete — Análisis de Build (Async)
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "node ... \"post:bash:build-complete\" ... \"standard,strict\"",
"async": true,
"timeout": 30
}]
}
Qué hace: Detecta cuando un comando de build completó (bun build, npm run build, etc.) y analiza el output en busca de warnings, bundle sizes, y métricas de performance. Genera un resumen que va a los logs.
Async obligatorio: El análisis puede tomar varios segundos si el output del build es extenso. Al ser async, Claude no espera el resultado.
Timeout 30s: Generoso para builds que generan mucho output.
7.3 quality-gate.js — Gate de Calidad (Async)
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{
"type": "command",
"command": "node ... \"post:quality-gate\" \"scripts/hooks/quality-gate.js\" \"standard,strict\"",
"async": true,
"timeout": 30
}]
}
Qué hace: Después de cada edición de archivos, corre checks de calidad sobre el archivo modificado. Detecta el tipo de archivo y corre las verificaciones apropiadas:
flowchart TD
A[Archivo editado] --> B{Tipo de archivo}
B -->|.ts .tsx| C[TypeScript check]
B -->|.js .jsx .ts .tsx| D[ESLint / Biome lint]
B -->|.css .scss| E[Stylelint]
B -->|.json| F[JSON validate]
C --> G[Reporte de issues]
D --> G
E --> G
F --> G
G --> H[Output a stderr para Claude]
Async con feedback: A pesar de ser async, el output va a stderr y eventualmente llega a Claude como contexto adicional en la siguiente interacción.
7.4 post-edit-format.js — Auto-Formato de Archivos
{
"matcher": "Edit",
"hooks": [{"type": "command", "command": "node ... \"post:edit:format\" \"scripts/hooks/post-edit-format.js\" \"strict\""}]
}
Qué hace: Después de cada edición a archivos JS/TS, ejecuta el formatter configurado en el proyecto para normalizar el estilo.
Auto-detección del formatter:
flowchart LR
A[post-edit-format.js] --> B{biome.json existe?}
B -->|Sí| C[Ejecutar Biome]
B -->|No| D{.prettierrc existe?}
D -->|Sí| E[Ejecutar Prettier]
D -->|No| F[Skip, sin formatter]
Solo strict: Formatear automáticamente puede ser intrusivo. En proyectos donde el desarrollador quiere controlar el formato manualmente, standard no activa este hook.
Por qué Edit y no Write: Edit modifica archivos existentes (cambios de Claude al código). Write crea archivos nuevos o sobreescribe. El formato se aplica principalmente a ediciones incrementales.
7.5 post-edit-typecheck.js — TypeScript Check Post-Edición
{
"matcher": "Edit",
"hooks": [{"type": "command", "command": "node ... \"post:edit:typecheck\" \"scripts/hooks/post-edit-typecheck.js\" \"strict\""}]
}
Qué hace: Solo para archivos .ts y .tsx. Corre tsc --noEmit sobre el archivo modificado para detectar errores de tipos inmediatamente después de la edición.
Feedback inmediato: Claude recibe los errores de TypeScript como parte del contexto y puede corregirlos en la misma sesión, sin esperar a que el usuario compile.
Solo strict: TypeScript check en cada edición puede ser lento en proyectos grandes. Queda reservado para equipos que priorizan type safety estricta.
8. SessionStart Hook
{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "bash -lc '...run-with-flags.js session:start scripts/hooks/session-start.js minimal,standard,strict...'"}],
"description": "Load previous context and detect package manager on new session"
}
El hook session-start.js es el primer código que corre en cada sesión de Claude Code. Activo en todos los perfiles (minimal,standard,strict).
Qué carga session-start.js
flowchart TD
A[SessionStart] --> B[Leer session_id del input]
B --> C[Buscar estado previo en ~/.claude/sessions/]
C --> D{Estado previo existe?}
D -->|Sí| E[Leer last_task, last_branch, last_files]
D -->|No| F[Primera sesión, estado vacío]
E --> G[Detectar package manager]
F --> G
G --> H{package.json existe?}
H -->|Sí| I{bun.lockb existe?}
I -->|Sí| J[PM = bun]
I -->|No| K{yarn.lock existe?}
K -->|Sí| L[PM = yarn]
K -->|No| M[PM = npm]
H -->|No| N[Sin package manager]
J --> O[Escribir contexto a stderr]
L --> O
M --> O
N --> O
O --> P[Exit 0]
Output de sesión
El hook construye un mensaje de contexto que Claude recibe al inicio:
[SessionStart] Contexto de sesión:
- Package Manager: bun
- Última tarea: implementar autenticación JWT
- Branch activo: feat/auth
- Archivos recientes: src/auth/jwt.ts, src/middleware/auth.ts
Detección de package manager
La detección es jerárquica:
- Busca
bun.lockb→ Bun - Busca
yarn.lock→ Yarn - Busca
package-lock.json→ npm - Busca
pnpm-lock.yaml→ pnpm - Sin lockfile → npm (default)
El resultado se almacena como variable de entorno para que otros hooks lo usen: ECC_PACKAGE_MANAGER=bun.
9. PreCompact Hook
{
"matcher": "*",
"hooks": [{"type": "command", "command": "node ... \"pre:compact\" \"scripts/hooks/pre-compact.js\" \"standard,strict\""}],
"description": "Save state before context compaction"
}
El problema que resuelve
Claude Code compacta el contexto cuando se acerca al límite de tokens. La compactación resume la conversación, pero puede perder:
- Nombres específicos de archivos mencionados
- Decisiones de diseño tomadas durante la sesión
- Lista de tareas pendientes
- Errores que se encontraron y resolvieron
Qué guarda pre-compact.js
Antes de cada compactación, el script:
- Lee el estado actual del contexto (lo recibe via stdin)
- Extrae entidades clave: archivos modificados, comandos ejecutados, errores encontrados
- Serializa a JSON en
~/.claude/sessions/{session_id}/pre-compact-state.json - Escribe un resumen a stderr para que Claude lo incluya en el contexto compactado
Por qué es importante
Sin pre-compact.js, después de una compactación Claude puede “olvidar” que:
- Ya intentó cierto approach y falló
- Cierto archivo tiene una particularidad que requiere cuidado
- El usuario mencionó una restricción importante al inicio de la sesión
Con el hook, ese contexto se preserva en disco y puede ser cargado por session-start.js en la siguiente sesión.
Estado que se pierde sin el hook
graph LR
subgraph "Sin pre-compact.js"
A1[Decisiones] -->|compactación| B1[Perdido]
A2[Archivos visitados] -->|compactación| B1
A3[Errores resueltos] -->|compactación| B1
end
subgraph "Con pre-compact.js"
C1[Decisiones] -->|guardado pre-compactación| D1[Preservado en disco]
C2[Archivos visitados] -->|guardado pre-compactación| D1
C3[Errores resueltos] -->|guardado pre-compactación| D1
D1 -->|session-start carga| E1[Disponible en nueva sesión]
end
10. Hooks Async vs Sync
La elección entre async y sync es una decisión de arquitectura que afecta directamente la experiencia del usuario.
Hooks síncronos (default)
{"type": "command", "command": "node script.js"}
Claude Code espera a que el hook termine antes de ejecutar la herramienta (PreToolUse) o retornar el resultado (PostToolUse). Cada milisegundo del hook es latencia perceptible.
Usar sync cuando:
- El hook puede bloquear (solo PreToolUse puede bloquear, debe ser sync)
- El feedback es inmediato y crítico (errores de TypeScript, config-protection)
- El script es rápido (< 500ms)
Hooks asíncronos
{"type": "command", "command": "node script.js", "async": true, "timeout": 30}
El hook inicia en background y Claude Code continúa inmediatamente. El resultado del hook no bloquea ni es retornado en tiempo real.
Usar async cuando:
- El hook es PostToolUse (no puede bloquear de todas formas)
- La operación es lenta (análisis de build, llamadas HTTP, linting complejo)
- Es telemetría u observabilidad (no necesita bloquear)
- El timeout es necesario para evitar bloqueos
Tabla de decisión
| Hook | Sync/Async | Razón |
|---|---|---|
block-no-verify | Sync | Debe bloquear el comando |
config-protection | Sync | Debe bloquear la escritura |
mcp-health-check | Sync | Debe advertir antes de la operación |
observe.sh | Async | Telemetría, no debe añadir latencia |
quality-gate.js | Async | Linting puede ser lento |
post-edit-format.js | Sync | El resultado (archivo formateado) importa inmediatamente |
build-complete | Async | Análisis de output largo |
Timeout óptimos
| Tipo de operación | Timeout recomendado |
|---|---|
| Validación de string | 2-5s |
| Lectura de archivo local | 5s |
| Linting de archivo | 10-15s |
| Llamada HTTP | 15s |
| Build/compile | 30-60s |
| Análisis complejo | 30s |
11. Exit Codes y Control de Flujo
Los exit codes son el mecanismo de comunicación entre hooks y Claude Code.
Tabla de exit codes
| Exit Code | Significado | Aplica a |
|---|---|---|
0 | Éxito, continuar normalmente | PreToolUse, PostToolUse |
1 | Error general, bloquear con mensaje | PreToolUse |
2 | Bloqueo explícito, no retryable | PreToolUse |
Diferencia entre exit 1 y exit 2
Exit 1 — Error general: Claude Code muestra el stderr del hook como error y puede intentar remediar la situación. Claude recibe el mensaje y puede intentar un approach diferente.
Exit 2 — Bloqueo duro: La operación se cancela definitivamente. Claude recibe el mensaje pero Claude Code no ofrece reintentar.
Cómo escribir el mensaje de bloqueo
El mensaje para Claude va en stderr (no stdout). Stdout puede tener otros usos en el futuro; stderr es el canal convencional para mensajes de diagnóstico.
// En un hook PreToolUse
const input = JSON.parse(await readStdin());
if (input.tool_input.command.includes('--no-verify')) {
process.stderr.write(
'BLOQUEADO: --no-verify está prohibido en este proyecto.\n' +
'Los pre-commit hooks son obligatorios. Corrige los errores del hook.\n'
);
process.exit(2);
}
process.exit(0);
Mensajes en stdout vs stderr
| Canal | Quién lo ve | Uso |
|---|---|---|
stdout | Claude Code (interno) | Datos estructurados (futuro) |
stderr | Claude como contexto | Mensajes, warnings, feedback |
12. Hooks de Seguridad
ECC implementa una capa de seguridad compuesta por múltiples hooks que trabajan juntos.
El modelo de seguridad por capas
graph TD
A[Operación del agente] --> B[Capa 1: block-no-verify]
B --> C[Capa 2: config-protection]
C --> D[Capa 3: insaits-security opcional]
D --> E[Capa 4: governance-capture]
E --> F[Operación permitida]
B -->|--no-verify detectado| G[BLOQUEADO]
C -->|config file detectado| H[BLOQUEADO]
D -->|riesgo detectado| I[BLOQUEADO]
block-no-verify — La Primera Línea
El hook más simple pero más crítico. Su lógica es:
// Pseudocódigo de [email protected]
const input = JSON.parse(stdin);
const command = input.tool_input?.command || '';
const noVerifyPatterns = [
/--no-verify/i,
/-n\s+(?=.*git\s+(commit|push))/ // -n es alias de --no-verify en algunos contextos
];
if (noVerifyPatterns.some(p => p.test(command))) {
process.stderr.write('Error: --no-verify está bloqueado...');
process.exit(2);
}
config-protection.js — Guardia de Configuraciones
Este hook implementa una lista blanca de archivos protegidos. Si la ruta del archivo a editar coincide con algún patrón, bloquea:
// Patrones de archivos protegidos
const PROTECTED_PATTERNS = [
/\.eslintrc(\.(js|json|yml|yaml|cjs))?$/,
/\.prettierrc(\.(js|json|yml|yaml|cjs))?$/,
/biome\.(json|jsonc)$/,
/tsconfig(\.[a-z]+)?\.json$/,
/\.stylelintrc(\.(js|json|yml|yaml))?$/,
];
// El mensaje de bloqueo es clave: redirige a Claude
const message = `
BLOQUEADO: No se puede modificar archivos de configuración de linter/formatter.
El agente debe corregir el código que falla, no debilitar las reglas.
Opciones:
1. Corregir el error de código reportado
2. Agregar un disable comment inline (// eslint-disable-next-line)
3. Solicitar al desarrollador que actualice la configuración manualmente
`;
governance-capture.js — Auditoría de Decisiones
governance-capture.js no bloquea nada. Su función es registrar todas las operaciones significativas del agente en un log de auditoría:
{"timestamp":"2024-12-17T10:00:00Z","event":"PreToolUse","tool":"Bash","command":"git push origin main","session":"abc-123"}
{"timestamp":"2024-12-17T10:00:05Z","event":"PreToolUse","tool":"Write","file":"src/config.ts","session":"abc-123"}
Este log permite:
- Auditoría post-facto de lo que hizo el agente
- Detección de patrones de comportamiento inusuales
- Evidencia para compliance y regulaciones
13. Hooks de Productividad
Los hooks de productividad no bloquean sino que guían y facilitan el trabajo del agente.
suggest-compact.js — Gestión del Context Window
El hook mantiene estado en memoria (por sesión) y en disco:
// Lógica simplificada de suggest-compact.js
const STATE_FILE = `${os.tmpdir()}/ecc-compact-${sessionId}.json`;
// Leer contador actual
let state = { count: 0, lastSuggestedAt: 0 };
try { state = JSON.parse(fs.readFileSync(STATE_FILE)); } catch {}
state.count++;
const SUGGEST_THRESHOLD = 10; // Cada 10 operaciones de escritura
const MIN_INTERVAL = 5; // Mínimo 5 operaciones entre sugerencias
if (state.count - state.lastSuggestedAt >= SUGGEST_THRESHOLD) {
process.stderr.write(
`💡 Sugerencia: Han ocurrido ${state.count} operaciones de escritura. ` +
`Considera ejecutar /compact para optimizar el contexto.\n`
);
state.lastSuggestedAt = state.count;
}
fs.writeFileSync(STATE_FILE, JSON.stringify(state));
tmux-reminder.js — Educación sobre Procesos Long-Running
El hook detecta patrones de comandos que típicamente son long-running:
const LONG_RUNNING_PATTERNS = [
/^bun\s+(dev|start|run\s+dev)/,
/^npm\s+run\s+(dev|start|serve)/,
/^yarn\s+(dev|start)/,
/^python\s+-m\s+http\.server/,
/^node\s+server\./,
/^\.\/(server|app|main)\.(js|ts)/,
];
Si el comando coincide y no hay tmux en el path del comando, el hook envía:
Recordatorio: Este parece ser un servidor de desarrollo long-running.
Considera ejecutarlo en tmux: tmux new -s dev 'bun dev'
O usa el comando directamente; el hook auto-tmux-dev lo manejará.
doc-file-warning.js — Archivos de Documentación
Los archivos Markdown “estándar” están en una lista blanca y no generan warning:
const STANDARD_DOC_FILES = new Set([
'README.md', 'CHANGELOG.md', 'CONTRIBUTING.md', 'LICENSE.md',
'SECURITY.md', 'CODE_OF_CONDUCT.md', 'CLAUDE.md', 'AGENTS.md',
]);
const filePath = input.tool_input.file_path;
const fileName = path.basename(filePath);
if (filePath.endsWith('.md') && !STANDARD_DOC_FILES.has(fileName)) {
// Verificar si está en una carpeta de docs conocida
const isInDocsFolder = /\/(docs?|documentation|content)\//i.test(filePath);
if (!isInDocsFolder) {
process.stderr.write(
`Advertencia: "${fileName}" no es un archivo de documentación estándar.\n` +
`Considera si realmente necesitas crear este archivo.\n`
);
}
}
14. Hooks de Aprendizaje Continuo
El sistema de Continuous Learning de ECC usa observe.sh como recolector de datos.
observe.sh — El Recolector
El script captura cada uso de herramienta en formato JSONL:
#!/bin/bash
# observe.sh - Captura observaciones para continuous learning
OBSERVATIONS_DIR="${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/data"
OBSERVATIONS_FILE="${OBSERVATIONS_DIR}/observations.jsonl"
mkdir -p "$OBSERVATIONS_DIR"
# Leer input de stdin
INPUT=$(cat)
# Extraer campos relevantes
TOOL=$(echo "$INPUT" | jq -r '.tool // "unknown"')
SESSION=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Crear observación
OBSERVATION=$(jq -n \
--arg ts "$TIMESTAMP" \
--arg tool "$TOOL" \
--arg session "$SESSION" \
--argjson input "$INPUT" \
'{timestamp: $ts, tool: $tool, session_id: $session, input: $input}')
# Append a JSONL file
echo "$OBSERVATION" >> "$OBSERVATIONS_FILE"
Formato JSONL de observaciones
{"timestamp":"2024-12-17T10:00:00Z","tool":"Bash","session_id":"abc-123","input":{"command":"git status"}}
{"timestamp":"2024-12-17T10:00:02Z","tool":"Edit","session_id":"abc-123","input":{"file_path":"src/index.ts"}}
{"timestamp":"2024-12-17T10:00:05Z","tool":"Bash","session_id":"abc-123","input":{"command":"bun test"}}
Procesamiento de observaciones
Los datos crudos son procesados por el sistema de Continuous Learning para:
- Identificar secuencias comunes:
git status → Edit → git add → git commit - Detectar patrones de error: Comandos que fallan frecuentemente
- Generar insights: “Este proyecto usa bun, sugiere bun en lugar de npm”
- Alimentar skills: Los insights generan nuevas reglas o skills automáticamente
flowchart LR
A[observe.sh] -->|JSONL| B[observations.jsonl]
B -->|procesar| C[Pattern Analyzer]
C -->|insights| D[Insight Store]
D -->|generar| E[Nuevas Rules]
D -->|generar| F[Nuevos Skills]
E -->|aplicar| G[Próximas sesiones]
F -->|aplicar| G
15. Hooks de Calidad de Código
La triada de calidad es: formatear → linter → typecheck. ECC implementa los tres como hooks PostToolUse.
quality-gate.js — El Orquestador
quality-gate.js es el hook más complejo de calidad. Orquesta múltiples verificaciones:
// quality-gate.js - estructura simplificada
const input = JSON.parse(await readStdin());
const filePath = input.tool_input?.file_path;
if (!filePath) process.exit(0);
const ext = path.extname(filePath);
const checks = [];
// Determinar qué checks correr según la extensión
if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
checks.push(runBiomeOrEslint(filePath));
}
if (['.ts', '.tsx'].includes(ext)) {
checks.push(runTypecheck(filePath));
}
if (['.css', '.scss', '.less'].includes(ext)) {
checks.push(runStylelint(filePath));
}
// Ejecutar todos los checks en paralelo
const results = await Promise.allSettled(checks);
// Reportar issues encontrados
const issues = results
.filter(r => r.status === 'fulfilled' && r.value.issues.length > 0)
.flatMap(r => r.value.issues);
if (issues.length > 0) {
process.stderr.write(`Quality Gate: ${issues.length} issue(s) encontrados:\n`);
issues.forEach(issue => process.stderr.write(` ${issue}\n`));
}
process.exit(0); // Post hook: no bloquea, solo reporta
post-edit-format.js — Auto-Formato
La lógica de detección de formatter:
async function detectFormatter(projectRoot) {
// Prioridad: Biome > Prettier > ninguno
const biomeConfig = path.join(projectRoot, 'biome.json');
const biomeConfigC = path.join(projectRoot, 'biome.jsonc');
const prettierConfig = ['.prettierrc', '.prettierrc.js', '.prettierrc.json']
.map(f => path.join(projectRoot, f));
if (await fileExists(biomeConfig) || await fileExists(biomeConfigC)) {
return { type: 'biome', command: 'biome format --write' };
}
for (const config of prettierConfig) {
if (await fileExists(config)) {
return { type: 'prettier', command: 'prettier --write' };
}
}
return null;
}
// Ejecutar formatter sobre el archivo editado
const formatter = await detectFormatter(process.cwd());
if (formatter) {
const { stdout, stderr } = await exec(`${formatter.command} "${filePath}"`);
// El archivo queda formateado en disco
}
post-edit-typecheck.js — TypeScript Inmediato
// Solo para archivos TypeScript
const filePath = input.tool_input?.file_path;
if (!['.ts', '.tsx'].some(ext => filePath?.endsWith(ext))) {
process.exit(0);
}
// Correr tsc sobre el archivo específico
try {
const { stderr } = await exec(`npx tsc --noEmit "${filePath}" 2>&1`);
if (stderr) {
process.stderr.write(`TypeScript errors en ${filePath}:\n${stderr}\n`);
}
} catch (error) {
// tsc retorna exit code != 0 cuando hay errores de tipos
process.stderr.write(`TypeScript check falló:\n${error.stderr}\n`);
}
16. MCP Health Check Hook
{
"matcher": "*",
"hooks": [{"type": "command", "command": "node ... \"pre:mcp-health-check\" \"scripts/hooks/mcp-health-check.js\" \"standard,strict\""}]
}
Por qué verificar salud de MCPs
Los MCP (Model Context Protocol) servers son procesos externos que Claude Code lanza para extender sus capacidades. Si un MCP server se cae (crash, timeout, OOM), las herramientas MCP quedan en estado inconsistente. Claude puede intentar usarlas y recibir errores crípticos.
Cómo verifica la salud
// mcp-health-check.js - estructura simplificada
const MCP_SOCKET_DIR = `${os.tmpdir()}/claude-mcp-sockets/`;
const CACHE_FILE = `${os.tmpdir()}/mcp-health-cache.json`;
const CACHE_TTL = 30000; // 30 segundos
async function checkMCPHealth() {
// Verificar caché primero
const cache = loadCache();
if (cache && Date.now() - cache.timestamp < CACHE_TTL) {
return cache.healthy;
}
// Listar sockets MCP activos
const sockets = await listMCPSockets();
const healthResults = await Promise.all(
sockets.map(socket => pingMCPSocket(socket))
);
const allHealthy = healthResults.every(r => r.healthy);
saveCache({ timestamp: Date.now(), healthy: allHealthy, results: healthResults });
return allHealthy;
}
Qué bloquea
Si se detecta un MCP server caído y el tool que va a ejecutarse es un tool MCP (no Bash, Edit, etc.), el hook puede advertir:
Advertencia: MCP server "filesystem" no está respondiendo.
Las herramientas de filesystem MCP pueden fallar.
Considera reiniciar Claude Code o verificar el MCP server.
17. El DRY Adapter para Cursor
ECC implementa el principio DRY a nivel de hooks: los mismos scripts que funcionan en Claude Code se reusan en Cursor IDE a través de un adapter.
El problema del código duplicado
Sin adapter, cada script de hook necesitaría dos versiones:
- Una para Claude Code (lee stdin JSON de Claude)
- Una para Cursor (lee eventos de Cursor)
Con el adapter, existe una sola versión de cada script y el adapter traduce los eventos.
Estructura del adapter
flowchart LR
subgraph "Claude Code"
A1[PreToolUse] -->|stdin JSON| B1[run-with-flags.js]
B1 --> C1[script.js]
end
subgraph "Cursor IDE"
A2[Cursor Event] -->|adapter| B2[adapter.js]
B2 -->|stdin JSON traducido| C2[script.js]
end
C1 -.->|mismo script| C2
Mapeo de eventos Cursor → Claude Code
| Evento Cursor | Evento Claude Code equivalente |
|---|---|
onBeforeToolUse | PreToolUse |
onAfterToolUse | PostToolUse |
onSessionStart | SessionStart |
onSessionEnd | SessionEnd |
adapter.js — Traducción de formato
// adapter.js - Traduce eventos Cursor al formato Claude Code
function adaptCursorEvent(cursorEvent) {
return {
tool: cursorEvent.toolName,
tool_input: cursorEvent.parameters,
session_id: cursorEvent.sessionId,
cwd: cursorEvent.workspaceRoot,
// Campos adicionales de Cursor mapeados
cursor_specific: {
editor_version: cursorEvent.editorVersion,
model: cursorEvent.model,
}
};
}
18. Hooks en OpenCode
OpenCode es una terminal IDE alternativa que también implementa un sistema de hooks compatible con el paradigma de ECC.
Los 11 eventos de OpenCode
OpenCode expone más puntos de extensión que Claude Code:
| Evento OpenCode | Equivalente Claude Code | Diferencia |
|---|---|---|
session.start | SessionStart | Misma semántica |
session.end | SessionEnd | Misma semántica |
tool.before | PreToolUse | Misma semántica |
tool.after | PostToolUse | Misma semántica |
message.before | No existe | Antes de enviar mensaje al LLM |
message.after | No existe | Después de recibir respuesta |
compact.before | PreCompact | Antes de compactar |
compact.after | No existe | Después de compactar |
file.read | No existe | Al leer archivo |
file.write | PreToolUse(Write) | Al escribir archivo |
error | No existe | Al ocurrir un error |
Compatibilidad con ECC
Los scripts de hooks de ECC son compatibles con OpenCode cuando:
- Solo leen stdin (no usan APIs específicas de Claude Code)
- Usan exit codes estándar (0, 1, 2)
- Envían feedback a stderr
Los scripts que usan ${CLAUDE_PLUGIN_ROOT} necesitan que OpenCode defina esa variable de entorno, lo cual se puede configurar en la sesión de shell.
19. Crear un Hook Propio — Paso a Paso
Paso 1: Definir el propósito
Antes de escribir código, responde:
- ¿Qué evento dispara el hook? (PreToolUse, PostToolUse, etc.)
- ¿Qué herramienta quiero interceptar?
- ¿El hook debe bloquear o solo informar?
- ¿Qué datos necesito del input?
Ejemplo: Queremos bloquear commits con mensajes vacíos.
Paso 2: Crear el script
// scripts/hooks/block-empty-commit.js
import { readFileSync } from 'fs';
import { execSync } from 'child_process';
// Leer input de Claude Code (stdin)
async function readStdin() {
return new Promise((resolve) => {
let data = '';
process.stdin.on('data', chunk => data += chunk);
process.stdin.on('end', () => resolve(data));
});
}
async function main() {
const rawInput = await readStdin();
let input;
try {
input = JSON.parse(rawInput);
} catch {
// Input inválido, no bloquear
process.exit(0);
}
// Solo aplica a comandos Bash
if (input.tool !== 'Bash') process.exit(0);
const command = input.tool_input?.command || '';
// Detectar git commit
const commitMatch = command.match(/git\s+commit(?:\s+[^-]|\s+-[^m])*(?:-m\s+['"]([^'"]*)['"]/);
if (commitMatch) {
const message = commitMatch[1]?.trim();
if (!message || message.length < 5) {
process.stderr.write(
'BLOQUEADO: Mensaje de commit vacío o demasiado corto.\n' +
'El mensaje debe tener al menos 5 caracteres.\n' +
'Formato sugerido: "feat: descripción del cambio"\n'
);
process.exit(2);
}
}
process.exit(0);
}
main().catch(err => {
process.stderr.write(`Error en hook: ${err.message}\n`);
process.exit(0); // No bloquear en caso de error del hook
});
Paso 3: Registrar en settings.json
Editar ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "node \"/home/user/.claude/scripts/hooks/block-empty-commit.js\""
}],
"description": "Bloquear commits con mensajes vacíos o muy cortos"
}
]
}
}
Paso 4: Probar en aislamiento
Antes de activar el hook en Claude Code, probarlo directamente:
# Simular input de PreToolUse
echo '{"tool":"Bash","tool_input":{"command":"git commit -m \"\""},"session_id":"test"}' | \
node scripts/hooks/block-empty-commit.js
echo "Exit code: $?"
# Probar con mensaje válido
echo '{"tool":"Bash","tool_input":{"command":"git commit -m \"feat: agregar autenticación\""},"session_id":"test"}' | \
node scripts/hooks/block-empty-commit.js
echo "Exit code: $?"
Paso 5: Verificar con Claude Code
Reiniciar Claude Code para que cargue los nuevos hooks. Luego pedir a Claude que intente un commit vacío y verificar que se bloquea.
Template completo reutilizable
// template-hook.js — Base para nuevos hooks
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
// === CONFIGURACIÓN ===
const HOOK_CONFIG = {
name: 'mi-hook',
targetTools: ['Bash'], // Herramientas que este hook maneja
event: 'PreToolUse', // Para documentación
canBlock: true, // ¿Puede retornar exit 2?
};
// === LEER INPUT ===
async function readStdin() {
return new Promise((resolve) => {
let data = '';
if (process.stdin.isTTY) { resolve('{}'); return; }
process.stdin.on('data', chunk => data += chunk);
process.stdin.on('end', () => resolve(data || '{}'));
});
}
// === LÓGICA PRINCIPAL ===
async function evaluate(input) {
// input.tool — nombre de la herramienta
// input.tool_input — parámetros de la herramienta
// input.session_id — ID de sesión
// input.cwd — directorio de trabajo
// Retornar null para continuar, string para bloquear
return null;
}
// === MAIN ===
async function main() {
let input = {};
try {
const raw = await readStdin();
input = JSON.parse(raw);
} catch {
process.exit(0); // No bloquear en caso de input inválido
}
// Verificar que aplica a esta herramienta
if (!HOOK_CONFIG.targetTools.includes(input.tool)) {
process.exit(0);
}
try {
const blockReason = await evaluate(input);
if (blockReason) {
process.stderr.write(`[${HOOK_CONFIG.name}] BLOQUEADO: ${blockReason}\n`);
process.exit(2);
}
} catch (err) {
// Los hooks no deben fallar silenciosamente
process.stderr.write(`[${HOOK_CONFIG.name}] Error: ${err.message}\n`);
// Por defecto: no bloquear en caso de error del hook
}
process.exit(0);
}
main();
20. Variables de Entorno en Hooks
Los hooks tienen acceso a variables de entorno del proceso de Claude Code y variables específicas del sistema ECC.
Variables de ECC
| Variable | Descripción | Ejemplo |
|---|---|---|
CLAUDE_PLUGIN_ROOT | Directorio raíz de ECC | /home/user/.claude |
ECC_HOOK_PROFILE | Perfil activo | standard |
ECC_DISABLED_HOOKS | Hooks desactivados | pre:bash:tmux-reminder,post:quality-gate |
ECC_PACKAGE_MANAGER | PM detectado por session-start | bun |
ECC_ENABLE_INSAITS | Activar monitor InsAIts | 1 o no definida |
ECC_DEBUG_HOOKS | Modo debug de hooks | 1 o no definida |
Variables de Claude Code
| Variable | Descripción |
|---|---|
CLAUDE_SESSION_ID | ID único de la sesión actual |
CLAUDE_WORKING_DIR | Directorio de trabajo actual |
CLAUDE_MODEL | Modelo de Claude en uso |
Variables del sistema disponibles
Todos los hooks heredan el entorno completo del proceso Claude Code, incluyendo:
PATH— Para ejecutar binarios del proyecto (biome, eslint, etc.)HOME— Directorio home del usuarioNODE_ENV— Ambiente de Node.js si está definido- Variables de proyecto definidas en
.envsi Claude Code las carga
Configurar variables en el entorno
# ~/.bashrc o ~/.zshrc
export ECC_HOOK_PROFILE=standard
export CLAUDE_PLUGIN_ROOT="${HOME}/.claude"
export ECC_ENABLE_INSAITS=0 # Desactivar InsAIts
# Para debug temporal
ECC_DEBUG_HOOKS=1 claude # Solo para esta sesión
Acceder a variables en Node.js
// En un hook Node.js
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || path.join(os.homedir(), '.claude');
const profile = process.env.ECC_HOOK_PROFILE || 'standard';
const sessionId = process.env.CLAUDE_SESSION_ID || 'unknown';
const debugMode = process.env.ECC_DEBUG_HOOKS === '1';
if (debugMode) {
process.stderr.write(`[DEBUG] Hook ejecutándose con perfil: ${profile}\n`);
}
21. Debugging de Hooks
Los hooks son código externo que corre fuera del proceso principal de Claude Code. Debuggear requiere técnicas específicas.
Ver output de hooks en tiempo real
Claude Code muestra el stderr de los hooks en la consola. Para activar output verbose:
# Modo debug: todos los hooks imprimen su estado
export ECC_DEBUG_HOOKS=1
claude
Probar un hook manualmente
La forma más eficiente de debuggear es ejecutar el hook directamente con input simulado:
# Crear archivo de input de prueba
cat > /tmp/hook-test-input.json << 'EOF'
{
"tool": "Bash",
"tool_input": {
"command": "git commit -m 'test'"
},
"session_id": "debug-session",
"cwd": "/home/user/proyecto"
}
EOF
# Ejecutar el hook con ese input
node scripts/hooks/block-empty-commit.js < /tmp/hook-test-input.json
echo "Exit code: $?"
Agregar logging temporal
// En el hook, temporalmente:
const DEBUG = process.env.ECC_DEBUG_HOOKS === '1';
function debug(msg) {
if (DEBUG) process.stderr.write(`[DEBUG hook-name] ${msg}\n`);
}
debug(`Input recibido: ${JSON.stringify(input, null, 2)}`);
debug(`Tool: ${input.tool}`);
debug(`Command: ${input.tool_input?.command}`);
Errores comunes y diagnóstico
Error: “Hook timeout”
- El hook tardó más que el timeout configurado
- Diagnóstico: Ejecutar el script manualmente y medir tiempo
- Solución: Aumentar timeout o hacer el hook más eficiente
Error: “Hook script not found”
- La ruta en
commandno existe - Diagnóstico: Verificar que
${CLAUDE_PLUGIN_ROOT}resuelve correctamente - Solución: Usar rutas absolutas con
${CLAUDE_PLUGIN_ROOT}o verificar la instalación
Error: “Permission denied”
- El script no tiene permisos de ejecución
- Solución:
chmod +x scripts/hooks/mi-hook.jso usarnode script.jsen lugar de./script.js
El hook no se dispara
- El matcher no coincide con la herramienta
- El hook está en el perfil incorrecto
ECC_DISABLED_HOOKSincluye el hook ID- Diagnóstico: Activar
ECC_DEBUG_HOOKS=1
El hook bloquea cuando no debería
- La condición de bloqueo tiene un falso positivo
- Diagnóstico: Ejecutar manualmente con el input exacto que lo bloquea
- Solución: Refinar la regex o condición de detección
Inspeccionar hooks registrados
# Ver hooks configurados en settings.json
cat ~/.claude/settings.json | jq '.hooks'
# Ver hooks del proyecto
cat .claude/settings.json | jq '.hooks'
22. Hook Runtime Controls
ECC_DISABLED_HOOKS — Desactivar hooks individualmente
Para desactivar un hook sin eliminarlo de settings.json:
# Desactivar un hook específico
export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder"
# Desactivar múltiples hooks
export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:quality-gate,pre:edit-write:suggest-compact"
El wrapper run-with-flags.js lee esta variable antes de ejecutar el script. Si el ID del hook está en la lista, hace exit 0 silencioso.
Cambiar perfil en tiempo de ejecución
# En .env del proyecto (si Claude Code lo carga)
ECC_HOOK_PROFILE=strict
# O en la sesión actual
export ECC_HOOK_PROFILE=minimal
claude # Sesión con mínimos hooks
Qué pasa si un hook falla inesperadamente
Si el script del hook lanza una excepción no manejada:
- Si el exit code resultante es 0 → Claude Code continúa normalmente
- Si el exit code resultante es no-0 → Claude Code puede tratar como bloqueo
La mejor práctica es envolver la lógica principal en try/catch y hacer exit 0 en caso de error del hook mismo (no del código del usuario):
async function main() {
try {
// lógica del hook
} catch (err) {
// Error en el hook mismo, no en la operación del usuario
process.stderr.write(`[hook-name] Error interno: ${err.message}\n`);
process.exit(0); // No bloquear por error del hook
}
}
Tabla de comportamientos por exit code y evento
| Evento | Exit 0 | Exit 1 | Exit 2 |
|---|---|---|---|
PreToolUse | Continuar | Bloquear (retryable) | Bloquear (definitivo) |
PostToolUse | OK | Señal de error (no bloquea) | Señal de error (no bloquea) |
SessionStart | OK | Advertencia | Advertencia |
PreCompact | OK | N/A | N/A |
23. Patrones Avanzados
Hook que lee archivos del proyecto
// Hook que verifica que el archivo a editar tiene tests asociados
import fs from 'fs';
import path from 'path';
async function main() {
const input = JSON.parse(await readStdin());
if (input.tool !== 'Edit') process.exit(0);
const filePath = input.tool_input?.file_path;
if (!filePath) process.exit(0);
// Solo para archivos de implementación, no tests
const isImpl = /src\/(?!.*\.test\.).*\.(ts|js)$/.test(filePath);
if (!isImpl) process.exit(0);
// Buscar archivo de test correspondiente
const testPath = filePath.replace('src/', 'src/').replace(/\.(ts|js)$/, '.test.$1');
const altTestPath = filePath.replace('src/', '__tests__/').replace(/\.(ts|js)$/, '.test.$1');
const testExists = fs.existsSync(testPath) || fs.existsSync(altTestPath);
if (!testExists) {
process.stderr.write(
`Recordatorio: No existe test para ${path.basename(filePath)}.\n` +
`Considera crear ${path.basename(testPath)}\n`
);
}
process.exit(0);
}
Hook que llama a una API externa
// Hook que registra operaciones en un sistema de auditoría externo
import https from 'https';
async function postToAuditAPI(event) {
return new Promise((resolve) => {
const data = JSON.stringify(event);
const options = {
hostname: 'audit.empresa.com',
port: 443,
path: '/api/events',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.AUDIT_API_TOKEN}`,
'Content-Length': data.length,
},
timeout: 5000, // 5 segundos máximo
};
const req = https.request(options, resolve);
req.on('error', () => resolve(null)); // Fallar silenciosamente
req.on('timeout', () => { req.destroy(); resolve(null); });
req.write(data);
req.end();
});
}
async function main() {
const input = JSON.parse(await readStdin());
// Este hook es async, no bloquea la operación
await postToAuditAPI({
timestamp: new Date().toISOString(),
tool: input.tool,
session: input.session_id,
project: path.basename(input.cwd || ''),
});
process.exit(0);
}
Hook con estado persistente entre llamadas
// Hook que detecta ciclos: si el mismo comando falla 3 veces, bloquea
import fs from 'fs';
import os from 'os';
import crypto from 'crypto';
const STATE_DIR = path.join(os.tmpdir(), 'ecc-cycle-detection');
fs.mkdirSync(STATE_DIR, { recursive: true });
async function main() {
const input = JSON.parse(await readStdin());
if (input.tool !== 'Bash') process.exit(0);
const command = input.tool_input?.command;
const commandHash = crypto.createHash('md5').update(command).digest('hex');
const stateFile = path.join(STATE_DIR, `${input.session_id}-${commandHash}.json`);
// Leer historial de este comando en esta sesión
let state = { count: 0, firstSeen: Date.now() };
try {
state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
} catch {}
state.count++;
state.lastSeen = Date.now();
// Si el mismo comando se intenta más de 3 veces en 5 minutos
const WINDOW_MS = 5 * 60 * 1000;
const isInWindow = (Date.now() - state.firstSeen) < WINDOW_MS;
if (state.count >= 3 && isInWindow) {
process.stderr.write(
`Advertencia: Este comando se ha ejecutado ${state.count} veces en los últimos 5 minutos.\n` +
`¿Estás en un ciclo? Considera un approach diferente.\n`
);
}
fs.writeFileSync(stateFile, JSON.stringify(state));
process.exit(0);
}
Hook condicional por OS
// Hook que adapta su comportamiento según el sistema operativo
import os from 'os';
const platform = os.platform();
async function main() {
const input = JSON.parse(await readStdin());
const command = input.tool_input?.command || '';
if (platform === 'win32') {
// En Windows, detectar comandos Unix que no funcionan
const unixOnlyCommands = ['grep', 'sed', 'awk', 'find'];
const usesUnix = unixOnlyCommands.some(cmd =>
new RegExp(`\\b${cmd}\\b`).test(command)
);
if (usesUnix) {
process.stderr.write(
'Advertencia: Este comando usa herramientas Unix que pueden no funcionar en Windows.\n' +
'Considera usar PowerShell o instalar Git Bash/WSL.\n'
);
}
}
process.exit(0);
}
24. Seguridad en Hooks
Los hooks son código que corre con los privilegios del usuario. Una vulnerabilidad en un hook puede tener consecuencias graves.
Inyección de comandos
El riesgo más crítico: usar datos de la herramienta de Claude directamente en un exec.
// PELIGROSO — Inyección de comandos
const command = input.tool_input.command;
exec(`echo "${command}" | wc -l`); // Si command contiene "; rm -rf /", desastre
// SEGURO — Usar arrays en exec, no strings
const { execFile } = require('child_process');
execFile('wc', ['-l'], { input: command }); // Seguro
Rutas relativas vs absolutas
// PELIGROSO — Ruta relativa, cambia según CWD
require('./utils/helper');
// SEGURO — Ruta absoluta con __dirname o import.meta.url
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
require(path.join(__dirname, 'utils/helper'));
${CLAUDE_PLUGIN_ROOT} para portabilidad
Usar ${CLAUDE_PLUGIN_ROOT} en las rutas de los hooks hace la configuración portátil entre máquinas:
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/mi-hook.js\""
En lugar de:
"command": "node \"/home/artiko/.claude/scripts/hooks/mi-hook.js\""
La segunda versión falla si se comparte el settings.json con otro usuario o si cambia el home directory.
Validación del input
Siempre validar que el input del hook tiene la estructura esperada:
function validateInput(input) {
if (!input || typeof input !== 'object') return false;
if (!input.tool || typeof input.tool !== 'string') return false;
if (!input.tool_input || typeof input.tool_input !== 'object') return false;
return true;
}
const input = JSON.parse(rawInput);
if (!validateInput(input)) process.exit(0); // Input inválido, no bloquear
No almacenar secretos en hooks
Los scripts de hooks pueden estar en repositorios públicos (como ECC mismo). Nunca incluir tokens, contraseñas o API keys directamente:
// MAL — Token hardcodeado
const token = 'ghp_abc123secrettoken';
// BIEN — Token desde variable de entorno
const token = process.env.AUDIT_API_TOKEN;
if (!token) {
process.stderr.write('AUDIT_API_TOKEN no configurado, saltando hook\n');
process.exit(0);
}
25. Performance
Los hooks tienen impacto directo en la latencia de cada operación de Claude Code. Un hook lento hace que el agente parezca lento.
Medir el tiempo de un hook
time echo '{"tool":"Bash","tool_input":{"command":"git status"}}' | \
node scripts/hooks/block-no-verify.js
Optimizaciones comunes
1. Salir temprano cuando no aplica:
// En el comienzo del hook, salir si no es relevante
if (input.tool !== 'Bash') process.exit(0); // Salida inmediata
const command = input.tool_input?.command;
if (!command) process.exit(0); // Sin comando, salir
2. Evitar require/import pesados cuando no son necesarios:
// MAL — Importa todo al inicio aunque no se use
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
// BIEN — Importar solo lo necesario, y solo si se necesita
const command = input.tool_input?.command;
if (!command.includes('git')) process.exit(0); // No usa ningún import
// Solo si llegamos aquí necesitamos el import pesado
const { execSync } = await import('child_process');
3. Caché de estado:
Para hooks que leen archivos de configuración o estado, cachear en memoria o con TTL:
// Cache simple en módulo (persiste entre llamadas en la misma sesión si Node reutiliza)
let configCache = null;
let cacheTime = 0;
const CACHE_TTL = 10000; // 10 segundos
function getConfig() {
if (configCache && Date.now() - cacheTime < CACHE_TTL) {
return configCache;
}
configCache = JSON.parse(fs.readFileSync('config.json'));
cacheTime = Date.now();
return configCache;
}
Timeouts como protección de performance
Configurar timeouts en todos los hooks es una buena práctica defensiva:
{
"type": "command",
"command": "node script.js",
"timeout": 10
}
Sin timeout, un hook que entra en un loop infinito o espera indefinidamente bloquea a Claude Code para siempre.
Benchmarks de referencia
| Tipo de hook | Tiempo esperado | Acción si supera |
|---|---|---|
| Validación de string | < 50ms | Revisar regex |
| Lectura de archivo local | < 100ms | Usar caché |
| Ejecución de CLI (biome, eslint) | 200ms-2s | Usar async |
| Llamada HTTP | 100ms-5s | Usar async + timeout |
| Build/compile | > 5s | Siempre async |
26. Compatibilidad Cross-Platform
ECC usa Node.js para la mayoría de hooks precisamente por su portabilidad cross-platform. Sin embargo, hay consideraciones a tener en cuenta.
Node.js vs Bash
Los hooks en bash (observe.sh) solo funcionan en Unix. Para hooks que deben funcionar en Windows:
// MAL para Windows
"command": "bash scripts/hooks/mi-hook.sh"
// BIEN para Windows, macOS, Linux
"command": "node scripts/hooks/mi-hook.js"
Separadores de ruta
// MAL — Solo funciona en Unix
const configPath = `${pluginRoot}/config/settings.json`;
// BIEN — Cross-platform con path.join
const configPath = path.join(pluginRoot, 'config', 'settings.json');
Rutas con espacios
Si el path contiene espacios (común en Windows: C:\Users\John Doe\), las rutas en el JSON deben estar entre comillas:
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/script.js\""
Las comillas escapadas en el JSON aseguran que el comando funcione incluso si CLAUDE_PLUGIN_ROOT contiene espacios.
Diferencias en stdin/stdout
En Windows, los saltos de línea son \r\n (CRLF) en lugar de \n (LF). Al leer stdin:
// Normalizar saltos de línea
const rawInput = data.toString().replace(/\r\n/g, '\n').replace(/\r/g, '\n');
Variables de entorno
En Windows, las variables de entorno se referencian con %VARIABLE% en cmd, pero con $env:VARIABLE en PowerShell. Los hooks Node.js usan process.env.VARIABLE que es consistente en todas las plataformas.
27. Hooks y el Ciclo de Tokens
Los hooks interactúan con el context window de Claude de formas que pueden ser beneficiosas o perjudiciales.
Cómo el output de hooks llega al contexto
sequenceDiagram
participant H as Hook
participant CC as Claude Code
participant C as Claude (LLM)
H->>CC: stderr: "TypeScript error en src/index.ts:10"
CC->>C: [system context] Hook output: "TypeScript error..."
C->>C: Considera el error para su respuesta
El stderr de un hook se convierte en contexto adicional para Claude. Esto usa tokens del context window.
Hooks que generan demasiado contexto
Un hook verbose puede generar cientos de tokens por ejecución. Si se dispara en "*" (todas las herramientas), el impacto es multiplicativo.
Ejemplo problemático: un hook que hace log de todo el contenido de cada archivo editado. Si Claude edita 20 archivos de 500 líneas cada uno, el hook genera 10,000 líneas de contexto adicional → miles de tokens desperdiciados.
Principio: Los hooks deben ser concisos en su output. Solo lo necesario para que Claude tome acción.
// MAL — Output excesivo
process.stderr.write(`Archivo completo procesado:\n${fullFileContent}\n`);
// BIEN — Output conciso
process.stderr.write(`3 issues encontrados en src/index.ts (líneas 10, 25, 47)\n`);
El hook suggest-compact y los tokens
suggest-compact.js existe exactamente por esta razón: después de muchas operaciones, el contexto acumulado (incluyendo outputs de hooks) se vuelve tan grande que compactar es beneficioso. El hook detecta este momento y lo sugiere.
Hooks async y el contexto
Los hooks async no contribuyen al contexto inmediato. Su output llega después, como contexto en la siguiente interacción. Esto es útil para no “contaminar” la respuesta actual con información que el usuario no pidió.
28. Ejercicios Prácticos
Ejercicio 1: Hook que bloquea commits con “WIP”
Objetivo: Bloquear cualquier commit cuyo mensaje contenga “WIP” o “wip”.
Pistas:
- Evento:
PreToolUse - Matcher:
Bash - Detectar:
git commitcon mensaje que contiene “WIP” - Exit: 2 con mensaje explicativo
Verificar con: git commit -m "WIP: trabajo en progreso"
Ejercicio 2: Hook que detecta archivos sensibles
Objetivo: Advertir (sin bloquear) cuando Claude va a escribir o editar un archivo que podría contener datos sensibles.
Pistas:
- Evento:
PreToolUse - Matcher:
Write|Edit - Detectar: Archivos
.env,*-secret*,*-key*,credentials* - Exit: 0 con mensaje de advertencia en stderr
Ejercicio 3: Hook de auto-commit post-tarea
Objetivo: Después de que Claude ejecuta ciertos comandos de “fin de tarea” (como bun test exitoso), sugerir hacer commit.
Pistas:
- Evento:
PostToolUse - Matcher:
Bash - Detectar: Exit code 0 en
bun testonpm test - Output: Sugerencia de commit con mensaje propuesto
Ejercicio 4: Hook de métricas de sesión
Objetivo: Contar cuántos archivos editó Claude en la sesión y reportarlo en SessionEnd.
Pistas:
- Dos hooks: uno en
PostToolUsepara contar, uno enSessionEndpara reportar - Estado persistente: archivo temporal por session ID
- Métricas: archivos únicos editados, total de operaciones
Ejercicio 5: Hook de verificación de package manager
Objetivo: Si el proyecto usa bun (detectado por bun.lockb) pero Claude intenta correr npm install o yarn install, bloquearlo y sugerir el equivalente con bun.
Pistas:
- Evento:
PreToolUse - Matcher:
Bash - Detectar:
npm installoyarn installcuando existebun.lockb - Bloquear con: Mensaje explicativo + comando correcto (
bun install)
29. Troubleshooting
Error 1: Hook no se ejecuta en absoluto
Síntoma: La operación pasa sin que el hook haga nada.
Diagnóstico:
# Verificar que el hook está en settings.json
cat ~/.claude/settings.json | jq '.hooks.PreToolUse'
# Verificar que el perfil incluye el hook
echo "Perfil actual: $ECC_HOOK_PROFILE"
# Verificar que el hook no está disabled
echo "Hooks disabled: $ECC_DISABLED_HOOKS"
Soluciones comunes:
- El matcher no coincide: verificar nombre exacto de la herramienta
- Perfil incorrecto: el hook solo corre en
strictpero perfil esstandard - ID en
ECC_DISABLED_HOOKS: remover el ID de la variable
Error 2: “node: command not found”
Síntoma: El hook falla con error sobre node no encontrado.
Causa: El PATH disponible en el hook no incluye la ruta de Node.js.
Solución: Usar la ruta absoluta de node:
which node # Obtener la ruta completa
# /home/user/.nvm/versions/node/v20.0.0/bin/node
Luego en settings.json:
"command": "/home/user/.nvm/versions/node/v20.0.0/bin/node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/script.js\""
O mejor, configurar el PATH completo en el comando:
"command": "bash -l -c 'node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/script.js\"'"
bash -l carga el .bashrc/.zshrc completo incluyendo nvm.
Error 3: JSON.parse falla en stdin
Síntoma: El hook falla con SyntaxError: Unexpected end of JSON input.
Causa: stdin está vacío o no contiene JSON válido.
Solución:
let input = {};
try {
const raw = await readStdin();
if (raw.trim()) {
input = JSON.parse(raw);
}
} catch (err) {
process.stderr.write(`[hook] Input inválido: ${err.message}\n`);
process.exit(0); // No bloquear
}
Error 4: El hook bloquea todas las operaciones
Síntoma: Cada operación de Claude Code falla con un bloqueo del hook.
Causa probable: El hook tiene un bug que siempre retorna exit 2, o hay una condición demasiado amplia.
Diagnóstico:
# Simular input básico que NO debería ser bloqueado
echo '{"tool":"Read","tool_input":{"file_path":"README.md"}}' | node script.js
echo "Exit: $?"
Solución temporal: Desactivar el hook mientras se investiga:
export ECC_DISABLED_HOOKS="nombre-del-hook-problemático"
Error 5: El hook tarda demasiado
Síntoma: Cada operación de Claude Code espera varios segundos.
Diagnóstico:
time echo '{}' | node scripts/hooks/mi-hook.js
Soluciones:
- Hacer el hook
async: truesi es PostToolUse - Agregar
timeoutcon valor bajo - Agregar salidas tempranas al inicio del hook
- Optimizar la lógica interna (evitar calls síncronos pesados)
Error 6: Variables de entorno no disponibles
Síntoma: ${CLAUDE_PLUGIN_ROOT} resuelve a string literal en lugar del path.
Causa: La expansión de variables en el campo command la hace el shell que lanza el proceso, y si Claude Code usa un subshell sin el entorno completo, la variable no está definida.
Solución: Definir la variable explícitamente:
export CLAUDE_PLUGIN_ROOT="${HOME}/.claude"
Y verificar:
echo $CLAUDE_PLUGIN_ROOT
# /home/artiko/.claude
Error 7: Hook funciona local pero falla en CI
Síntoma: En el servidor de CI, los hooks retornan error.
Causa probable: Las herramientas que el hook requiere (biome, eslint, etc.) no están instaladas en CI.
Solución: Detectar si las herramientas están disponibles antes de usarlas:
const { execSync } = require('child_process');
function commandExists(cmd) {
try {
execSync(`which ${cmd}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
if (!commandExists('biome')) {
process.exit(0); // No hacer nada si biome no está instalado
}
Error 8: Hook modifica estado global y causa problemas en parallel runs
Síntoma: Dos sesiones de Claude Code simultáneas interfieren entre sí a través de un hook.
Causa: El hook escribe a un archivo sin considerar concurrencia (ej: state.json compartido).
Solución: Usar session ID para aislar el estado:
const STATE_FILE = `/tmp/ecc-hook-state-${input.session_id}.json`;
// Cada sesión tiene su propio archivo de estado
Error 9: stderr de hook aparece en lugares inesperados
Síntoma: Los mensajes del hook aparecen en la terminal del usuario en lugar de en el chat de Claude.
Causa: El stderr del hook va a la terminal si Claude Code no lo captura correctamente.
Nota: Este comportamiento depende de cómo Claude Code maneja los file descriptors. En general, stderr de hooks va al contexto de Claude, no a la terminal del usuario. Si aparece en la terminal, puede ser un bug de Claude Code o una configuración específica.
Error 10: Hook no recibe el input completo
Síntoma: input.tool_input está vacío o incompleto.
Causa: Algunos tools no tienen tool_input (herramientas sin parámetros) o el formato cambió entre versiones de Claude Code.
Solución: Siempre acceder con optional chaining:
const command = input?.tool_input?.command ?? '';
const filePath = input?.tool_input?.file_path ?? '';
30. Referencia Completa de Hooks ECC
Todos los hooks de ECC con detalles
| Hook ID | Evento | Matcher | Perfil mínimo | Puede bloquear | Descripción |
|---|---|---|---|---|---|
| (npx block-no-verify) | PreToolUse | Bash | minimal | Sí | Bloquea --no-verify en git |
| (auto-tmux-dev) | PreToolUse | Bash | minimal | Sí (redirige) | Auto-inicia dev servers en tmux |
pre:bash:tmux-reminder | PreToolUse | Bash | strict | No | Recuerda usar tmux para long-running |
pre:bash:git-push-reminder | PreToolUse | Bash | strict | No | Revisión antes de git push |
pre:write:doc-file-warning | PreToolUse | Write | standard | No | Advierte sobre docs no estándar |
pre:edit-write:suggest-compact | PreToolUse | Edit|Write | standard | No | Sugiere compactar contexto |
pre:observe | PreToolUse | * | standard | No (async) | Captura observaciones para aprendizaje |
pre:insaits-security | PreToolUse | Bash|Write|Edit|MultiEdit | standard | Sí (si habilitado) | Monitor de seguridad InsAIts |
pre:config-protection | PreToolUse | Write|Edit|MultiEdit | standard | Sí | Protege configs de linter/formatter |
pre:mcp-health-check | PreToolUse | * | standard | No (advierte) | Verifica salud de MCP servers |
pre:compact | PreCompact | * | standard | No | Guarda estado antes de compactar |
session:start | SessionStart | * | minimal | No | Carga contexto y detecta PM |
post:bash:pr-created | PostToolUse | Bash | standard | No | Detecta PR y muestra URL |
post:bash:build-complete | PostToolUse | Bash | standard | No (async) | Analiza output de build |
post:quality-gate | PostToolUse | Edit|Write|MultiEdit | standard | No (async) | Corre linter y quality checks |
post:edit:format | PostToolUse | Edit | strict | No | Auto-formatea archivos JS/TS |
post:edit:typecheck | PostToolUse | Edit | strict | No | TypeScript check post-edición |
Flujo completo de una sesión típica
sequenceDiagram
participant U as Usuario
participant CC as Claude Code
participant H as Hooks
CC->>H: SessionStart → session-start.js
H-->>CC: Contexto previo + package manager
U->>CC: "edita src/auth.ts"
CC->>H: PreToolUse(Edit) → config-protection
H-->>CC: OK (no es config)
CC->>H: PreToolUse(Edit) → suggest-compact
H-->>CC: OK (contador bajo)
CC->>H: PreToolUse(Edit) → observe.sh (async)
CC->>CC: Ejecuta Edit
CC->>H: PostToolUse(Edit) → quality-gate (async)
CC->>H: PostToolUse(Edit) → post-edit-format (strict)
CC->>H: PostToolUse(Edit) → post-edit-typecheck (strict)
U->>CC: "haz commit"
CC->>H: PreToolUse(Bash) → block-no-verify
H-->>CC: OK (no --no-verify)
CC->>H: PreToolUse(Bash) → git-push-reminder (strict)
H-->>CC: OK
CC->>CC: Ejecuta git commit
CC->>H: PostToolUse(Bash) → post-bash-pr-created
H-->>CC: No PR detectado, OK
U->>CC: /compact
CC->>H: PreCompact → pre-compact.js
H-->>CC: Estado guardado
CC->>CC: Compacta contexto
Configuración mínima de hooks.json
Para un proyecto que solo quiere las protecciones críticas:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "npx [email protected]"}],
"description": "Bloquear --no-verify"
},
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/config-protection.js\"",
"timeout": 5
}],
"description": "Proteger configuraciones"
}
]
}
}
Configuración recomendada para desarrollo diario (standard)
Añadir sobre la mínima:
{
"hooks": {
"PreToolUse": [
"...mínima...",
{
"matcher": "Edit|Write",
"hooks": [{"type": "command", "command": "node ... \"pre:edit-write:suggest-compact\" ... \"standard,strict\""}],
"description": "Sugerir compactación"
},
{
"matcher": "*",
"hooks": [{"type": "command", "command": "node ... \"pre:mcp-health-check\" ... \"standard,strict\""}],
"description": "MCP health check"
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{"type": "command", "command": "node ... \"post:quality-gate\" ... \"standard,strict\"", "async": true, "timeout": 30}],
"description": "Quality gate asíncrono"
}
],
"PreCompact": [
{
"matcher": "*",
"hooks": [{"type": "command", "command": "node ... \"pre:compact\" ... \"standard,strict\""}],
"description": "Guardar estado antes de compactar"
}
],
"SessionStart": [
{
"matcher": "*",
"hooks": [{"type": "command", "command": "bash -lc 'node ... session:start ... minimal,standard,strict'"}],
"description": "Cargar contexto de sesión"
}
]
}
}
Los hooks de ECC representan el enfoque más pragmático para automatizar y proteger el trabajo de agentes AI: código real que corre en el OS, con exit codes que controlan el flujo, y un sistema de perfiles que permite escalar la complejidad según las necesidades del proyecto. Dominar los hooks es dominar la capa de control del agente.