Capítulo 6: Hooks: Automatización por Eventos

Por: Artiko
claude-codehooksautomatizacioneventos

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:

MecanismoCuándo aplicaQuién lo ejecutaPuede bloquear
RulesSiempre, en todo momentoClaude (LLM) como instrucciónNo directamente
SkillsCuando el usuario llama /skillClaude ejecuta el skillNo
HooksAl ocurrir un evento específicoEl 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:

Casos de uso propios de rules:

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

EventoFaseInput disponiblePuede bloquear
SessionStartInicio de sesiónSession ID, working dirNo
PreToolUseAntes de ejecutar toolNombre tool, parámetros completosSí (exit 1-2)
PostToolUseDespués de ejecutar toolNombre tool, parámetros, resultadoNo directamente
PreCompactAntes de compactar contextoResumen del contextoSí (puede agregar info)
StopCuando el agente terminaMotivo de paradaNo
SessionEndCierre de sesiónSession summaryNo

PreToolUse — El más poderoso

PreToolUse es el evento de mayor utilidad porque:

  1. Recibe el input completo antes de que la herramienta actúe
  2. Puede bloquear la operación completamente
  3. 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:

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:

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:

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

MatcherHerramienta que coincide
BashComandos de terminal
ReadLectura de archivos
WriteEscritura de archivos (nuevo o sobreescritura)
EditEdición de strings en archivo existente
MultiEditMúltiples ediciones en un archivo
GlobBúsqueda de archivos por patrón
GrepBúsqueda de contenido en archivos
LSListado de directorios
TodoReadLeer lista de tareas
TodoWriteEscribir lista de tareas
WebSearchBúsqueda web
WebFetchFetch de URL
TaskLanzar 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
PerfilVariableDescripción
minimalECC_HOOK_PROFILE=minimalSolo hooks críticos de infraestructura
standardECC_HOOK_PROFILE=standard (default)Hooks de productividad y calidad
strictECC_HOOK_PROFILE=strictTodos 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"

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:

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

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:

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:

  1. Busca bun.lockb → Bun
  2. Busca yarn.lock → Yarn
  3. Busca package-lock.json → npm
  4. Busca pnpm-lock.yaml → pnpm
  5. 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:

Qué guarda pre-compact.js

Antes de cada compactación, el script:

  1. Lee el estado actual del contexto (lo recibe via stdin)
  2. Extrae entidades clave: archivos modificados, comandos ejecutados, errores encontrados
  3. Serializa a JSON en ~/.claude/sessions/{session_id}/pre-compact-state.json
  4. 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:

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:

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:

Tabla de decisión

HookSync/AsyncRazón
block-no-verifySyncDebe bloquear el comando
config-protectionSyncDebe bloquear la escritura
mcp-health-checkSyncDebe advertir antes de la operación
observe.shAsyncTelemetría, no debe añadir latencia
quality-gate.jsAsyncLinting puede ser lento
post-edit-format.jsSyncEl resultado (archivo formateado) importa inmediatamente
build-completeAsyncAnálisis de output largo

Timeout óptimos

Tipo de operaciónTimeout recomendado
Validación de string2-5s
Lectura de archivo local5s
Linting de archivo10-15s
Llamada HTTP15s
Build/compile30-60s
Análisis complejo30s

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 CodeSignificadoAplica a
0Éxito, continuar normalmentePreToolUse, PostToolUse
1Error general, bloquear con mensajePreToolUse
2Bloqueo explícito, no retryablePreToolUse

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

CanalQuién lo veUso
stdoutClaude Code (interno)Datos estructurados (futuro)
stderrClaude como contextoMensajes, 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:

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:

  1. Identificar secuencias comunes: git status → Edit → git add → git commit
  2. Detectar patrones de error: Comandos que fallan frecuentemente
  3. Generar insights: “Este proyecto usa bun, sugiere bun en lugar de npm”
  4. 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:

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 CursorEvento Claude Code equivalente
onBeforeToolUsePreToolUse
onAfterToolUsePostToolUse
onSessionStartSessionStart
onSessionEndSessionEnd

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 OpenCodeEquivalente Claude CodeDiferencia
session.startSessionStartMisma semántica
session.endSessionEndMisma semántica
tool.beforePreToolUseMisma semántica
tool.afterPostToolUseMisma semántica
message.beforeNo existeAntes de enviar mensaje al LLM
message.afterNo existeDespués de recibir respuesta
compact.beforePreCompactAntes de compactar
compact.afterNo existeDespués de compactar
file.readNo existeAl leer archivo
file.writePreToolUse(Write)Al escribir archivo
errorNo existeAl ocurrir un error

Compatibilidad con ECC

Los scripts de hooks de ECC son compatibles con OpenCode cuando:

  1. Solo leen stdin (no usan APIs específicas de Claude Code)
  2. Usan exit codes estándar (0, 1, 2)
  3. 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:

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

VariableDescripciónEjemplo
CLAUDE_PLUGIN_ROOTDirectorio raíz de ECC/home/user/.claude
ECC_HOOK_PROFILEPerfil activostandard
ECC_DISABLED_HOOKSHooks desactivadospre:bash:tmux-reminder,post:quality-gate
ECC_PACKAGE_MANAGERPM detectado por session-startbun
ECC_ENABLE_INSAITSActivar monitor InsAIts1 o no definida
ECC_DEBUG_HOOKSModo debug de hooks1 o no definida

Variables de Claude Code

VariableDescripción
CLAUDE_SESSION_IDID único de la sesión actual
CLAUDE_WORKING_DIRDirectorio de trabajo actual
CLAUDE_MODELModelo de Claude en uso

Variables del sistema disponibles

Todos los hooks heredan el entorno completo del proceso Claude Code, incluyendo:

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”

Error: “Hook script not found”

Error: “Permission denied”

El hook no se dispara

El hook bloquea cuando no debería

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:

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

EventoExit 0Exit 1Exit 2
PreToolUseContinuarBloquear (retryable)Bloquear (definitivo)
PostToolUseOKSeñal de error (no bloquea)Señal de error (no bloquea)
SessionStartOKAdvertenciaAdvertencia
PreCompactOKN/AN/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 hookTiempo esperadoAcción si supera
Validación de string< 50msRevisar regex
Lectura de archivo local< 100msUsar caché
Ejecución de CLI (biome, eslint)200ms-2sUsar async
Llamada HTTP100ms-5sUsar async + timeout
Build/compile> 5sSiempre 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:

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:

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:

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:

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:

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:

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:

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 IDEventoMatcherPerfil mínimoPuede bloquearDescripción
(npx block-no-verify)PreToolUseBashminimalBloquea --no-verify en git
(auto-tmux-dev)PreToolUseBashminimalSí (redirige)Auto-inicia dev servers en tmux
pre:bash:tmux-reminderPreToolUseBashstrictNoRecuerda usar tmux para long-running
pre:bash:git-push-reminderPreToolUseBashstrictNoRevisión antes de git push
pre:write:doc-file-warningPreToolUseWritestandardNoAdvierte sobre docs no estándar
pre:edit-write:suggest-compactPreToolUseEdit|WritestandardNoSugiere compactar contexto
pre:observePreToolUse*standardNo (async)Captura observaciones para aprendizaje
pre:insaits-securityPreToolUseBash|Write|Edit|MultiEditstandardSí (si habilitado)Monitor de seguridad InsAIts
pre:config-protectionPreToolUseWrite|Edit|MultiEditstandardProtege configs de linter/formatter
pre:mcp-health-checkPreToolUse*standardNo (advierte)Verifica salud de MCP servers
pre:compactPreCompact*standardNoGuarda estado antes de compactar
session:startSessionStart*minimalNoCarga contexto y detecta PM
post:bash:pr-createdPostToolUseBashstandardNoDetecta PR y muestra URL
post:bash:build-completePostToolUseBashstandardNo (async)Analiza output de build
post:quality-gatePostToolUseEdit|Write|MultiEditstandardNo (async)Corre linter y quality checks
post:edit:formatPostToolUseEditstrictNoAuto-formatea archivos JS/TS
post:edit:typecheckPostToolUseEditstrictNoTypeScript 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.