Add hooks and CLAUDE.md resource types with install/uninstall scripts
Introduces two new resource types (hooks, claude-md) with full CRUD, visual hook config editor, section-delimited CLAUDE.md installs, uninstall endpoints, and shell injection hardening in sync scripts.
This commit is contained in:
368
PLAN-HOOKS.md
Normal file
368
PLAN-HOOKS.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# Plan: Hooks como nuevo tipo de recurso
|
||||
|
||||
## Contexto
|
||||
|
||||
Claude Code soporta **hooks**: comandos que se ejecutan automaticamente en respuesta a eventos del ciclo de vida del agente. Se configuran en `.claude/settings.json` bajo la clave `hooks`.
|
||||
|
||||
### Estructura de hooks en Claude Code
|
||||
|
||||
```json
|
||||
// .claude/settings.json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "echo 'About to run bash'" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "./scripts/lint.sh $CLAUDE_FILE_PATHS" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Eventos disponibles (14)
|
||||
|
||||
| Evento | Cuando se dispara |
|
||||
|--------|-------------------|
|
||||
| `PreToolUse` | Antes de ejecutar una herramienta (puede bloquear) |
|
||||
| `PostToolUse` | Despues de ejecutar una herramienta |
|
||||
| `Notification` | Cuando Claude envia una notificacion |
|
||||
| `Stop` | Cuando Claude termina de responder |
|
||||
| `SubagentStop` | Cuando un subagente termina |
|
||||
| `PreCompact` | Antes de compactar contexto |
|
||||
| `PostCompact` | Despues de compactar contexto |
|
||||
|
||||
### Tipos de handler
|
||||
|
||||
| Tipo | Descripcion |
|
||||
|------|-------------|
|
||||
| `command` | Ejecuta un comando shell. Variables de entorno disponibles: `$CLAUDE_TOOL_NAME`, `$CLAUDE_FILE_PATHS`, `$CLAUDE_TOOL_INPUT`, etc. |
|
||||
| `prompt` | Inyecta un prompt adicional a Claude |
|
||||
| `agent` | Invoca un subagente para evaluar |
|
||||
|
||||
### Variables de entorno en hooks
|
||||
|
||||
- `CLAUDE_TOOL_NAME` — nombre de la herramienta
|
||||
- `CLAUDE_TOOL_INPUT` — JSON con los parametros de la herramienta
|
||||
- `CLAUDE_FILE_PATHS` — rutas de archivos separadas por newline
|
||||
- `CLAUDE_TOOL_OUTPUT` — salida de la herramienta (solo en PostToolUse)
|
||||
- `CLAUDE_NOTIFICATION` — texto de notificacion (solo en Notification)
|
||||
- `CLAUDE_TRANSCRIPT` — transcripcion de la conversacion (solo en Stop/SubagentStop)
|
||||
|
||||
## Modelo de datos
|
||||
|
||||
Un hook en Grimoired se almacena como **carpeta** en `data/hooks/<slug>/`:
|
||||
|
||||
```
|
||||
my-hook/
|
||||
├── HOOK.md # Documentacion del hook (frontmatter + descripcion)
|
||||
├── hooks.json # Configuracion JSON del hook (lo que se inyecta en settings)
|
||||
└── scripts/ # Scripts opcionales referenciados por el hook
|
||||
├── lint.sh
|
||||
└── validate.py
|
||||
```
|
||||
|
||||
### HOOK.md (frontmatter)
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: Auto Lint
|
||||
description: Ejecuta linting automatico despues de cada escritura de archivo
|
||||
tags:
|
||||
- linting
|
||||
- quality
|
||||
author: alex
|
||||
author-email: alex@example.com
|
||||
events:
|
||||
- PostToolUse
|
||||
---
|
||||
|
||||
# Auto Lint
|
||||
|
||||
Este hook ejecuta automaticamente el linter despues de que Claude escribe o edita un archivo...
|
||||
```
|
||||
|
||||
### hooks.json
|
||||
|
||||
```json
|
||||
{
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": ".claude/hooks/auto-lint/scripts/lint.sh $CLAUDE_FILE_PATHS"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **Nota**: Los paths en `command` se reescriben durante la instalacion para apuntar a la ubicacion correcta en `.claude/hooks/<slug>/`.
|
||||
|
||||
## Archivos a modificar
|
||||
|
||||
| Archivo | Cambio |
|
||||
|---------|--------|
|
||||
| `src/lib/registry.ts` | Agregar tipo `hooks` al registro con sus campos |
|
||||
| `src/lib/resources.ts` | Sin cambios — ya soporta formato carpeta generico |
|
||||
| `src/pages/[type]/[slug].astro` | Renderizar preview de hooks.json y info de eventos |
|
||||
| `src/pages/[type]/[slug]/i.ts` | Script de instalacion especial para hooks |
|
||||
| `src/pages/[type]/[slug]/gi.ts` | Script de instalacion global para hooks |
|
||||
| `src/lib/sync.ts` | Sin cambios mayores — sync/push generico ya funciona |
|
||||
|
||||
## Archivos nuevos
|
||||
|
||||
| Archivo | Proposito |
|
||||
|---------|-----------|
|
||||
| `src/components/HookConfigPreview.astro` | Preview visual del hooks.json en pagina de detalle |
|
||||
| `src/components/HookConfigEditor.vue` | Editor visual de hooks.json (formulario, no JSON crudo) |
|
||||
|
||||
## Fases de implementacion
|
||||
|
||||
### Fase 1: Registry — agregar tipo `hooks`
|
||||
|
||||
Agregar `'hooks'` a `RESOURCE_TYPES` y su configuracion al `REGISTRY`:
|
||||
|
||||
```typescript
|
||||
export const RESOURCE_TYPES = ['skills', 'agents', 'output-styles', 'rules', 'hooks'] as const;
|
||||
|
||||
// En REGISTRY:
|
||||
hooks: {
|
||||
slug: 'hooks',
|
||||
label: 'Hooks',
|
||||
labelSingular: 'Hook',
|
||||
claudeDir: 'hooks',
|
||||
dataDir: path.resolve(process.env.HOOKS_DIR || `${DATA_ROOT}/hooks`),
|
||||
color: '#a78bfa', // violet
|
||||
mainFileName: 'HOOK.md',
|
||||
fields: [
|
||||
{
|
||||
key: 'description',
|
||||
label: 'Description',
|
||||
type: 'text',
|
||||
placeholder: 'Brief description of what this hook does',
|
||||
},
|
||||
{
|
||||
key: 'events',
|
||||
label: 'Events',
|
||||
type: 'tags',
|
||||
hint: 'Hook events this resource handles',
|
||||
placeholder: 'Add event...',
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
**Nota**: Los hooks siempre son formato `folder` porque necesitan `hooks.json` + scripts opcionales. El campo `events` en frontmatter es informativo (para filtrado/busqueda); la configuracion real esta en `hooks.json`.
|
||||
|
||||
### Fase 2: Formato obligatorio carpeta
|
||||
|
||||
En `src/pages/[type]/new.astro` y `src/components/ResourceEditor.vue`:
|
||||
|
||||
- Para tipo `hooks`: forzar `format: 'folder'` sin mostrar toggle.
|
||||
- Al crear un hook, generar automaticamente un `hooks.json` vacio (`{}`) como archivo inicial en el directorio.
|
||||
|
||||
En `src/lib/resources.ts` — `createResource`:
|
||||
- Despues de crear la carpeta y el mainFile, si el tipo es `hooks`, escribir `hooks.json` con `{}`.
|
||||
|
||||
### Fase 3: API — lectura de hooks.json
|
||||
|
||||
El endpoint existente `/api/resources/[type]/[slug]/files/[...filePath].ts` ya permite GET de `hooks.json`.
|
||||
|
||||
Agregar un endpoint auxiliar o logica al GET de `/api/resources/[type]/[slug].ts` para incluir el contenido de `hooks.json` parseado en la respuesta JSON cuando el tipo es `hooks`:
|
||||
|
||||
```typescript
|
||||
// En GET de [slug].ts, si type === 'hooks':
|
||||
const hooksJsonBuf = await getResourceFile(type, slug, 'hooks.json');
|
||||
if (hooksJsonBuf) {
|
||||
resource.hooksConfig = JSON.parse(hooksJsonBuf.toString('utf8'));
|
||||
}
|
||||
```
|
||||
|
||||
### Fase 4: Pagina de detalle — preview de hooks.json
|
||||
|
||||
**`HookConfigPreview.astro`**: componente que recibe el JSON parseado de hooks.json y lo renderiza visualmente:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ PostToolUse │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ Matcher: Write | Edit │ │
|
||||
│ │ ⚡ command │ │
|
||||
│ │ └ ./scripts/lint.sh $CLAUDE_FILE... │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Cada evento como seccion con su nombre como titulo
|
||||
- Cada matcher como sub-bloque
|
||||
- Cada handler con icono segun tipo (command/prompt/agent)
|
||||
- Truncar comandos largos con tooltip
|
||||
|
||||
En `[type]/[slug].astro`:
|
||||
- Leer `hooks.json` del recurso
|
||||
- Si existe, renderizar `HookConfigPreview` entre la descripcion y el arbol de archivos
|
||||
- Si no existe, mostrar un aviso sutil
|
||||
|
||||
### Fase 5: Editor — HookConfigEditor.vue
|
||||
|
||||
Componente Vue interactivo para editar hooks.json visualmente:
|
||||
|
||||
**Estructura del formulario:**
|
||||
|
||||
1. **Selector de evento** — dropdown con los 7 eventos
|
||||
2. **Lista de matchers** por evento — cada uno con:
|
||||
- Campo `matcher` (texto, patron regex)
|
||||
- Lista de handlers:
|
||||
- Tipo: `command` | `prompt` | `agent`
|
||||
- Valor: texto del comando, prompt, o nombre del agente
|
||||
3. **Botones** — Agregar evento, agregar matcher, agregar handler, eliminar
|
||||
|
||||
**Integracion en ResourceEditor.vue:**
|
||||
- Detectar tipo `hooks`
|
||||
- Cargar `hooks.json` existente via API
|
||||
- Renderizar `HookConfigEditor` debajo del editor de markdown
|
||||
- Al guardar, enviar PUT al endpoint de files para actualizar `hooks.json`
|
||||
|
||||
**Alternativa simplificada (fase inicial):**
|
||||
- Usar el campo `json` existente del registry para editar hooks.json como JSON crudo
|
||||
- Agregar al registry.ts un campo:
|
||||
```typescript
|
||||
{
|
||||
key: 'hooks-config',
|
||||
label: 'Hook Configuration',
|
||||
type: 'json',
|
||||
placeholder: '{ "PostToolUse": [{ "matcher": "Write", "hooks": [{ "type": "command", "command": "echo done" }] }] }',
|
||||
hint: 'JSON config that gets installed as hooks.json',
|
||||
},
|
||||
```
|
||||
- **Pros**: Implementacion rapida, reutiliza componente existente
|
||||
- **Contras**: UX menos amigable, propenso a errores de sintaxis
|
||||
- **Recomendacion**: Empezar con JSON crudo, iterar al editor visual despues
|
||||
|
||||
### Fase 6: Scripts de instalacion — logica especial para hooks
|
||||
|
||||
Los hooks se instalan de forma diferente a skills/agents. No van a `.claude/hooks/` (ese directorio no existe en Claude Code). Se instalan asi:
|
||||
|
||||
1. **Scripts** se copian a `.claude/hooks/<slug>/scripts/`
|
||||
2. **hooks.json** se lee y su contenido se **mergea** en `.claude/settings.local.json` bajo la clave `hooks`
|
||||
3. **Los paths en commands** se reescriben para apuntar a la ubicacion de scripts
|
||||
|
||||
**Script de instalacion (bash):**
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SLUG="my-hook"
|
||||
HOOKS_DIR=".claude/hooks/$SLUG"
|
||||
SETTINGS=".claude/settings.local.json"
|
||||
|
||||
# 1. Crear directorio y descargar archivos
|
||||
mkdir -p "$HOOKS_DIR/scripts"
|
||||
curl -fsSL "$ORIGIN/api/resources/hooks/$SLUG/files/hooks.json" -o "$HOOKS_DIR/hooks.json"
|
||||
# ... descargar scripts ...
|
||||
chmod +x "$HOOKS_DIR/scripts/"*
|
||||
|
||||
# 2. Mergear hooks.json en settings
|
||||
if [ ! -f "$SETTINGS" ]; then
|
||||
echo '{}' > "$SETTINGS"
|
||||
fi
|
||||
|
||||
# Usar jq para mergear
|
||||
jq --slurpfile new "$HOOKS_DIR/hooks.json" '
|
||||
.hooks //= {} |
|
||||
.hooks = (.hooks * $new[0])
|
||||
' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
|
||||
|
||||
echo "✓ Installed hook $SLUG"
|
||||
echo " Config merged into $SETTINGS"
|
||||
echo " Scripts at $HOOKS_DIR/scripts/"
|
||||
```
|
||||
|
||||
**Reescritura de paths**: Antes del merge, los paths en `command` que empiezan con `./scripts/` o `scripts/` se reescriben a `.claude/hooks/<slug>/scripts/`.
|
||||
|
||||
**Modificaciones en `i.ts` y `gi.ts`:**
|
||||
- Detectar `type === 'hooks'`
|
||||
- En vez del flujo generico de copiar a `.claude/<claudeDir>/<slug>`, usar el flujo especial:
|
||||
1. Descargar hooks.json y scripts a `.claude/hooks/<slug>/`
|
||||
2. Reescribir paths en hooks.json
|
||||
3. Mergear en settings.local.json con jq
|
||||
- Para `gi.ts`: lo mismo pero con `~/.claude/settings.json` (global)
|
||||
|
||||
**PowerShell**: Equivalente usando `ConvertFrom-Json`/`ConvertTo-Json` en vez de jq.
|
||||
|
||||
### Fase 7: Desinstalacion
|
||||
|
||||
Agregar endpoint o logica para generar script de desinstalacion:
|
||||
|
||||
**`src/pages/[type]/[slug]/uninstall.ts`** (nuevo, solo para hooks):
|
||||
- Genera script que:
|
||||
1. Lee settings.local.json
|
||||
2. Elimina las entradas de hooks que coincidan con el slug
|
||||
3. Elimina `.claude/hooks/<slug>/`
|
||||
4. Escribe settings.local.json actualizado
|
||||
|
||||
Esto es mas importante para hooks que para otros tipos porque la instalacion modifica un archivo compartido (settings).
|
||||
|
||||
## Consideraciones especiales
|
||||
|
||||
### Diferencias con otros tipos de recursos
|
||||
|
||||
| Aspecto | Skills/Agents/Rules | Hooks |
|
||||
|---------|-------------------|-------|
|
||||
| Formato | file o folder | siempre folder |
|
||||
| Destino instalacion | `.claude/<type>/<slug>.md` o carpeta | `.claude/hooks/<slug>/` + merge en settings |
|
||||
| Desinstalacion | Borrar archivo/carpeta | Borrar carpeta + limpiar settings |
|
||||
| Archivo config | No tiene | `hooks.json` |
|
||||
| Claude Dir | `skills`, `agents`, etc. | `hooks` (para scripts, no para config) |
|
||||
|
||||
### Validacion de hooks.json
|
||||
|
||||
Validar estructura al guardar:
|
||||
- Claves raiz deben ser eventos validos
|
||||
- Cada entrada debe tener `matcher` (string) y `hooks` (array)
|
||||
- Cada handler debe tener `type` (command/prompt/agent) y el valor correspondiente
|
||||
|
||||
### Seguridad
|
||||
|
||||
- Los scripts de hooks ejecutan comandos arbitrarios — la pagina de detalle debe mostrar un aviso claro
|
||||
- En la instalacion, mostrar que comandos se van a registrar antes de ejecutar
|
||||
- Nunca ejecutar hooks desde el servidor — solo generar scripts de instalacion
|
||||
|
||||
## Orden de implementacion
|
||||
|
||||
1. **Fase 1** — Registry: agregar tipo hooks (5 min)
|
||||
2. **Fase 2** — Formato carpeta obligatorio + hooks.json inicial (15 min)
|
||||
3. **Fase 3** — API lectura hooks.json (10 min)
|
||||
4. **Fase 4** — Preview en pagina de detalle (30 min)
|
||||
5. **Fase 5** — Editor (JSON crudo primero, visual despues) (20 min)
|
||||
6. **Fase 6** — Scripts de instalacion especiales (45 min) — **mas complejo**
|
||||
7. **Fase 7** — Script de desinstalacion (20 min)
|
||||
|
||||
## Verificacion
|
||||
|
||||
1. Crear un hook via web — verificar que se crea como carpeta con HOOK.md + hooks.json vacio
|
||||
2. Editar hooks.json con configuracion real (PostToolUse + command)
|
||||
3. Subir script a scripts/ via FileManager
|
||||
4. Ver pagina de detalle — verificar preview de configuracion
|
||||
5. Ejecutar `curl .../hooks/my-hook/i | bash` — verificar:
|
||||
- Scripts descargados a `.claude/hooks/my-hook/scripts/`
|
||||
- hooks.json mergeado en `.claude/settings.local.json`
|
||||
- Paths reescritos correctamente
|
||||
6. Verificar que el hook funciona ejecutando Claude Code en un proyecto con el hook instalado
|
||||
7. Ejecutar script de desinstalacion — verificar limpieza de settings y archivos
|
||||
8. Crear hook sin scripts (solo command inline) — verificar instalacion simplificada
|
||||
Reference in New Issue
Block a user