Skills, comandos y hooks: cómo organizar un plugin de Claude Code para un equipo

Un plugin reúne skills, comandos y hooks que el agente aplica solo. Cómo se estructura, qué hace cada pieza y por qué las reglas críticas van en un hook.

Cover for Skills, comandos y hooks: cómo organizar un plugin de Claude Code para un equipo

Un hook que pasa el linter cada vez que el agente edita un fichero cabe en este JSON (ejemplo simplificado):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/lint-on-edit.sh\"",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Cada vez que el agente edita o crea un fichero, Claude Code ejecuta un script del plugin. Si el linter encuentra errores, el script se los devuelve al agente, que los ve antes de seguir con la tarea. Nadie tiene que acordarse de pedirlo.

En mi equipo usamos un plugin interno con skills y comandos. No puedo publicar su contenido, así que en este artículo los ejemplos de hooks y comandos son genéricos y están marcados como tales, y la estructura de un plugin la enseño con otro: el de la skill de Illuminate que hemos desarrollado en eLearning Media, que empaqueta una skill para generar SQL de Blackboard Data / Illuminate. Lo que sí cuento con detalle es por qué cada pieza va donde va.

Qué hace el script que llama el hook

El hook solo declara cuándo se ejecuta algo. La lógica está en el script, y un script de este tipo tiene más decisiones de las que parece. Una versión mínima, como ejemplo:

#!/usr/bin/env bash
# Ejemplo: lint tras cada edición de un fichero Kotlin.
set -uo pipefail

# Claude Code pasa por stdin un JSON con la herramienta usada y sus parámetros.
FILE="$(jq -r '.tool_input.file_path // empty')"

# Solo actúa sobre Kotlin; cualquier otro fichero sale sin hacer nada.
case "$FILE" in
  *.kt|*.kts) ;;
  *) exit 0 ;;
esac

# Busca el gradlew más cercano; si no hay, el hook no aplica en este repo.
dir="$(dirname "$FILE")"
while [ "$dir" != "/" ] && [ ! -x "$dir/gradlew" ]; do dir="$(dirname "$dir")"; done
[ -x "$dir/gradlew" ] || exit 0

if ! OUT="$(cd "$dir" && ./gradlew -q ktlintCheck 2>&1)"; then
  echo "El linter ha fallado. Corrige estos errores:" >&2
  printf '%s\n' "$OUT" | tail -60 >&2
  exit 2
fi
exit 0

Tres decisiones están en esas líneas.

El script lee el fichero editado de la entrada estándar, donde Claude Code le pasa la herramienta usada y sus parámetros. Así puede actuar solo sobre lo que acaba de cambiar.

Sabe retirarse. Si el fichero no es Kotlin, o no hay un gradlew en el camino hacia la raíz, sale con 0 y no hace nada. Un plugin de equipo se usa en repos de lenguajes distintos y el hook se dispara en todos después de cada edición; si no supiera apartarse, sería un estorbo fuera de los proyectos Gradle.

Elige qué ve el agente con el código de salida. En un hook PostToolUse, la salida 2 hace que Claude Code le pase al modelo lo que el script ha escrito en stderr. La edición ya está hecha, así que el hook no la bloquea, pero el agente recibe los errores con una instrucción explícita de corregirlos. Con salida 0 no recibe nada y sigue trabajando. Es un principio que me parece sano para cualquier comprobación automática: el éxito es silencioso y solo los fallos hablan.

Por qué un hook y no una regla en la skill

Las convenciones de estilo y las reglas de arquitectura se pueden escribir en una skill, y en el plugin de mi equipo lo están: qué herramientas de calidad se usan, en qué orden y cómo corregir cada tipo de fallo.

El problema de una regla escrita en una skill es que depende de que el agente la cargue y de que se acuerde de aplicarla en el momento adecuado. Una skill se activa por su descripción, y la activación es probabilística (lo desarrollo en cómo crear una skill que funcione). Aunque esté cargada, nada obliga al agente a pasar el linter después de cada cambio; puede decidir hacerlo al final, o no hacerlo.

Un hook elimina esa dependencia. Se ejecuta en un evento del ciclo de vida del agente, siempre, y lo que devuelve entra en el contexto quiera el modelo o no. Es la diferencia entre pedirle a alguien que pase el linter y tenerlo en un pre-commit. La skill sigue haciendo falta, porque explica cómo corregir lo que el hook detecta, pero el disparo ya no depende de ella.

Esto es lo que se ha empezado a llamar harness engineering: en lugar de confiar en que el modelo recuerde las reglas, se construye alrededor del agente un entorno que comprueba su trabajo y le devuelve los fallos. El hook es la parte de ese entorno que se ejecuta sola.

Cómo está organizado un plugin

Un plugin es una carpeta con un manifiesto y las piezas en carpetas con nombre conocido. El de Illuminate está hecho para Codex y es mínimo:

blackboard-illuminate-sql/
├── .codex-plugin/plugin.json
├── assets/icon.svg
└── skills/
    └── illuminate-sql-support/
        ├── SKILL.md
        ├── agents/openai.yaml
        └── references/
            ├── reference-guide.md
            ├── database-structure.md
            ├── valid-fields.txt
            ├── illuminate-relations.csv
            ├── sql-examples.md
            └── tool-assignment-types-stage.md

El manifiesto, recortado:

{
  "name": "blackboard-illuminate-sql",
  "version": "1.0.2",
  "description": "Create, validate, explain, and debug schema-safe Snowflake SQL for Blackboard Data / Illuminate.",
  "author": { "name": "eLearning Media", "url": "https://www.elearningmedia.es" },
  "skills": "./skills/",
  "interface": {
    "displayName": "Illuminate SQL Assistant",
    "shortDescription": "Schema-safe Snowflake SQL",
    "capabilities": ["SQL generation", "SQL validation", "SQL debugging", "Query explanation"],
    "defaultPrompt": [
      "Create an Illuminate SQL query for my reporting goal.",
      "Validate this Illuminate query against the current schema.",
      "Explain and fix this Snowflake SQL error."
    ]
  }
}

La parte funcional son tres campos: nombre, versión y dónde están las skills. El bloque interface es presentación: cómo aparece el plugin en la herramienta, con qué descripción larga, qué capacidades anuncia y qué prompts sugiere a quien lo instala. La skill tiene además su propio agents/openai.yaml, con un nombre visible y un prompt por defecto que la invoca explícitamente con $illuminate-sql-support.

Lo interesante es lo que no cambia. La carpeta skills/illuminate-sql-support/ es una skill del estándar abierto de Agent Skills: un SKILL.md con name y description y sus references. Para Claude Code, el manifiesto iría en .claude-plugin/plugin.json y la carpeta de la skill podría ser la misma. Lo que se adapta a cada herramienta es el envoltorio, no el conocimiento.

Un plugin de Claude Code para un equipo añade dos carpetas que el de Illuminate no tiene (estructura de ejemplo):

plugin-del-equipo/
├── .claude-plugin/plugin.json   nombre, versión, descripción
├── skills/<nombre>/SKILL.md     + references/ y scripts/ según la skill
├── commands/<nombre>.md         un fichero por comando
├── hooks/hooks.json             hooks como el del principio
└── scripts/                     lo que ejecutan los hooks y los comandos

Las skills se descubren solas a partir de la carpeta, sin declararlas en el manifiesto.

La diferencia entre los dos plugins responde a para quién son. El de Illuminate está pensado para alguien que pide una consulta SQL en una conversación: solo necesita conocimiento, reglas y límites, y todo eso cabe en una skill. Un plugin de equipo se usa mientras el agente modifica repositorios, y ahí importan también los procedimientos con pasos que no se pueden saltar y las comprobaciones que no pueden depender de la memoria del modelo.

Tres piezas, tres disparadores

Cada pieza se dispara de forma distinta, y eso es lo que decide dónde va cada cosa:

PiezaQuién la disparaQué va ahíEjemplo
SkillEl agente, cuando la descripción encajaConocimiento: reglas, esquemas, procedimientosGenerar SQL válido de Illuminate
ComandoLa persona, escribiendo /nombreUn procedimiento con orden y puntos de controlPasar el quality gate completo
HookClaude Code, en un eventoComprobaciones que no pueden depender de nadieLint tras cada edición

En el plugin de mi equipo los comandos se apoyan siempre en una skill: el comando fija el orden y los momentos en los que hay que parar, y la skill tiene el detalle de cada paso. Así el procedimiento no se escribe dos veces.

Los comandos: orden y puntos de control

Un comando es un fichero Markdown con frontmatter que se convierte en un prompt cuando alguien lo invoca. Uno de los patrones que usamos es un quality gate en bucle. Una versión genérica, como ejemplo:

---
description: Ejecuta el quality gate completo y corrige lo que falle
argument-hint: "[ruta-del-módulo]"
allowed-tools: Bash, Read, Edit, Grep
---

Ejecuta el gate completo sobre `$1` (o el directorio actual): lint, tests
(incluidas las reglas de arquitectura) y verificación de cobertura.

Si algo falla, corrígelo y vuelve a ejecutar el gate. No termines hasta que
todo pase.

No modifiques umbrales ni reglas para pasar el gate. Si una regla te parece
incorrecta, dilo y para.

Al final, resume qué fallaba, qué has corregido y el estado de cada gate.

La línea que más importa es la de los umbrales. Un agente al que se le pide dejar un gate en verde tiene dos caminos, y uno de ellos es bajar el umbral de cobertura o desactivar la regla que falla. Es el atajo más corto hacia el objetivo literal, así que hay que prohibirlo de forma explícita.

El comando y el hook comparten herramienta, pero no momento. El hook se ejecuta tras cada edición y solo puede hacer lo barato, como el estilo, con un timeout corto. El gate completo, con tests y cobertura, se lanza antes de dar un cambio por terminado o abrir un PR. Una forma sencilla de organizarlo es un único script con dos modos, uno rápido para el hook y otro completo para el comando.

allowed-tools declara qué herramientas puede usar el comando. En el ejemplo, las necesarias para ejecutar el gate y corregir ficheros existentes, sin Write.

El otro patrón que usamos son comandos para procedimientos con pasos destructivos, como un despliegue. Ahí el comando hace sobre todo de freno: pregunta los valores que faltan en lugar de suponerlos, muestra cada paso que escribe en un entorno sensible y espera una confirmación explícita para ese paso, y prohíbe borrados sin aprobación. La skill que lo acompaña contiene el detalle técnico; el comando, los puntos en los que una persona tiene que decir que sí. El caso más largo de este tipo, una migración de un proyecto legacy por fases, tiene su propio artículo: modernizar legacy con IA.

El plugin no se copia a mano en cada máquina: lo distribuimos mediante un marketplace del departamento, y es ahí donde vamos añadiendo skills y comandos nuevos a medida que aparecen necesidades. Cómo evitar que ese crecimiento acabe en skills que se pisan entre sí es parte de lo que cuento en el artículo sobre evals de skills.

Qué cuidaría al construir un hook así

El script del principio es deliberadamente mínimo. Si lo llevara a un plugin de equipo, hay tres cosas que resolvería antes.

La primera es distinguir entre «el código tiene errores» y «la herramienta no está configurada». Si el repo tiene gradlew pero no aplica el plugin del linter, la tarea no existe y Gradle falla. Un script que trate ese fallo igual que un error de estilo le pide al agente que corrija algo que no tiene arreglo dentro del código, y el agente probablemente lo intentará. Comprobar antes que la tarea existe, o filtrar ese error concreto, lo evita.

La segunda es el silencio cuando el gate no se ejecuta. Un hook que se retira con salida 0 no le dice nada al agente, y eso es lo correcto en un repo JavaScript. Pero en una ejecución deliberada, como la del comando de quality gate, preferiría que el script dijera en voz alta que no ha comprobado nada, en lugar de dejar que el resumen final parezca un éxito.

La tercera es que no todas las reglas de una skill se pueden convertir en hook, y hay que separarlas. «Los controladores no acceden directamente a los repositorios» se puede comprobar con una regla de arquitectura; «elige nombres que expliquen la intención» no se puede comprobar con nada. Las primeras deberían acabar en un hook o en un gate; las segundas seguirán dependiendo de la skill y de la revisión humana.

Ninguna de las tres cambia la idea de fondo, que sigue pareciéndome la correcta: el conocimiento va en skills, los procedimientos con puntos de control en comandos y lo que no puede depender de la memoria del agente, en hooks. De dónde sale el conocimiento que acaba en las skills lo cuento en cómo destilar el conocimiento de un equipo en skills, y el mapa completo del tema está en la guía de agent skills.

OshyTech

Ingeniería backend y de datos orientada a sistemas escalables, automatización e IA.

Navegación

Copyright 2026 OshyTech. Todos los derechos reservados