¡Hola developer 👋🏻!
Cuando trabajas con GitHub Copilot en modo agente, el agente puede leer y escribir archivos, ejecutar comandos y tomar decisiones de forma autónoma.
Y eso está genial… hasta que te haces la pregunta incómoda:
👉 ¿Qué pasa con la calidad del código que genera?
👉 ¿Y con el formato?
Aquí es donde entran en juego los Agent hooks.
Estos no son más que «pequeños scripts» que se ejecutan automáticamente en momentos clave del ciclo de vida del agente, sin que tengas que pedírselo.
En este artículo te comparto dos que estoy usando muchísimo en proyectos JavaScript/TypeScript… y que ya se han vuelto imprescindibles en mi flujo de trabajo.
Si quieres verlos en acción, aquí tienes el vídeo completo 👇🏻
¿Qué son los agent hooks?
Los hooks son scripts que se ejecutan en distintos momentos del ciclo de vida de una sesión de agente.
Aunque hay varios eventos disponibles, los que más estoy utilizando son:
- preToolUse → justo antes de que el agente use una herramienta (por ejemplo, antes de modificar un archivo)
- postToolUse → justo después de que la use
Se configuran en un archivo .github/hooks/hooks.json dentro del repositorio:
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"bash": ".github/hooks/scripts/lint.sh"
}
],
"postToolUse": [
{
"type": "command",
"bash": ".github/hooks/scripts/format.sh"
}
]
}
}El nombre del JSON puede ser diferente a hooks.json y dentro de este cada evento es un array por lo que podrías tener más de un script asociado a ese momento.
Hook de lint (preToolUse)
El hook lint.sh se ejecuta antes de que el agente escriba o modifique un archivo. En este ejemplo, su función es pasar ESLint sobre el archivo en cuestión y, si hay errores, bloquear la operación devolviendo una decisión de deny.
Los hooks reciben un JSON por stdin con información sobre la herramienta que se va a usar y sus parámetros. El script extrae el nombre de la herramienta y la ruta del archivo, y solo actúa en herramientas de escritura (replace_string_in_file, create_file, etc.):
#!/bin/bash
# ─────────────────────────────────────────────────────────────
# 🔍 LINT — Verifica la calidad del código antes de escribir
# 📌 Se ejecuta en: preToolUse (antes de que el agente
# escriba o modifique un archivo)
# 🛠️ Herramienta: ESLint
# ─────────────────────────────────────────────────────────────
# ── Helpers para responder al hook ──────────────────────────
allow() {
jq -n --arg reason "$1" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow",
permissionDecisionReason: $reason
}
}'
exit 0
}
deny() {
jq -n --arg reason "$1" --arg context "$2" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: $reason,
additionalContext: $context
}
}'
exit 1
}
# ── Lógica principal ────────────────────────────────────────
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE=$(echo "$INPUT" | jq -r '.tool_input.filePath // empty')
# Solo lint en herramientas que escriben archivos
[[ "$TOOL_NAME" =~ ^(replace_string_in_file|multi_replace_string_in_file|create_file|run_in_terminal|run_task)$ ]] \
|| allow "⏭️ Read-only tool, no lint needed"
# Necesitamos una ruta de archivo
[ -z "$FILE" ] && allow "⏭️ No filePath in input"
# Solo lint en archivos JS/TS/Astro
EXT="${FILE##*.}"
[[ "$EXT" =~ ^(js|jsx|ts|tsx|astro)$ ]] || allow "⏭️ No es un archivo JS/TS/Astro"
# El archivo debe existir
[ ! -f "$FILE" ] && deny "⚠️ File not found: $FILE" ""
# Verificar que eslint está disponible
command -v npx &> /dev/null || allow "⚠️ npx no disponible — saltando lint"
# Ejecutar ESLint
if LINT_OUTPUT=$(npx eslint "$FILE" 2>&1); then
allow "✅ Linting passed for $FILE"
else
deny "❌ Linting failed for $FILE" "$LINT_OUTPUT"
fi
Cuando el hook devuelve deny, el agente recibe el motivo y el contexto adicional (la salida de ESLint), lo que le permite corregir el código antes de volver a intentar la escritura.
Hook de formato (postToolUse)
El hook format.sh se ejecuta después de cada escritura. Llama a Prettier sobre el archivo modificado para garantizar que el formato sea siempre consistente, independientemente de lo que haya generado el agente:
#!/bin/bash
# ─────────────────────────────────────────────────────────────
# ✨ FORMAT — Formatea el código automáticamente después de
# cada cambio
# 📌 Se ejecuta en: postToolUse (después de que el agente
# escribe o modifica un archivo)
# 🛠️ Herramienta: Prettier
# ─────────────────────────────────────────────────────────────
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
# Solo formatear después de herramientas que escriben archivos
if ! [[ "$TOOL_NAME" =~ ^(replace_string_in_file|multi_replace_string_in_file|create_file|run_in_terminal|run_task)$ ]]; then
echo "[format.sh] ⏭️ Skipped (read-only tool): $TOOL_NAME"
exit 0
fi
# Extraer rutas de archivo según el tipo de herramienta
if [[ "$TOOL_NAME" == "multi_replace_string_in_file" ]]; then
FILES=$(echo "$INPUT" | jq -r '.tool_input.replacements[].filePath // empty' | sort -u)
else
FILES=$(echo "$INPUT" | jq -r '.tool_input.filePath // empty')
fi
if [ -z "$FILES" ]; then
echo "[format.sh] ⚠️ No filePath found in input"
exit 0
fi
# Formatear cada archivo
while IFS= read -r FILE; do
[ -z "$FILE" ] && continue
# Solo formatear archivos del proyecto web
EXT="${FILE##*.}"
if ! [[ "$EXT" =~ ^(js|jsx|ts|tsx|astro|css|html|json|md)$ ]]; then
echo "[format.sh] ⏭️ No es un archivo formateable: $FILE"
continue
fi
# Verificar que prettier está disponible
if ! command -v npx &>/dev/null; then
echo "[format.sh] ⚠️ npx no disponible — saltando format"
exit 0
fi
# El archivo debe existir
if [ ! -f "$FILE" ]; then
echo "[format.sh] ⚠️ File not found: $FILE"
continue
fi
# Ejecutar Prettier
echo "[format.sh] 📝 Formatting: $FILE"
BEFORE=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE" 2>/dev/null)
FORMAT_OUTPUT=$(npx prettier --write "$FILE" 2>&1)
EXIT_CODE=$?
AFTER=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE" 2>/dev/null)
if [ $EXIT_CODE -ne 0 ]; then
echo "[format.sh] ❌ Formatter error: $FORMAT_OUTPUT"
elif [ "$BEFORE" = "$AFTER" ]; then
echo "[format.sh] ✅ No changes needed"
else
echo "[format.sh] ✨ File formatted successfully"
fi
done <<< "$FILES"Como puedes ver en este caso lo primero que revisa es cuál es la tool que se ha ejecutado con el objetivo de solo lanzar prettier cuando tiene sentido, es decir cuando hay nuevo código que debe ser formateado.
Resultado en la práctica
Con estos dos hooks activos, el flujo de trabajo con el agente queda así:
- Le pido al agente hacer una implementación
- El agente decide modificar un archivo
- preToolUse → ESLint analiza los cambios; si hay errores, el agente los corrige antes de escribir
- El agente escribe el archivo
- postToolUse → Prettier formatea el resultado automáticamente
Todo esto ocurre de forma transparente, sin interrumpir el flujo del agente ni requerir intervención manual. Aunque en el vídeo te muestro otros ejemplos donde si puedes interactuar con el hook a través del chat.
Una limitación importante
Los hooks solo se disparan cuando el agente usa una herramienta. Si tú editas un archivo directamente desde VS Code o la terminal, los hooks no se ejecutan. Para ese caso, lo mejor es configurar Prettier como formateador por defecto en VS Code con editor.formatOnSave: true.
Conclusión
Los agent hooks son una herramienta sencilla pero muy potente para mantener la calidad del código cuando trabajas con GitHub Copilot en modo agente. Con unas pocas líneas de bash puedes garantizar que todo lo que el agente toque cumpla con tus estándares de calidad, de forma automática y sin fricción. Pero hay muchos otros casos de uso que puedes ver de forma rápida en esta tabla de aquí.
¡Nos vemos 👋🏻!
