Skip to content
◀ Exit Level 08 ★★★☆☆ Time 7 min

Stage 08 — CLAUDE CODE

Hooks de Claude Code: el linter que se ejecuta solo

Published: at 09:10

Table of contents

Open Table of contents

El problema que los hooks resuelven

En el artículo anterior terminamos con una frase incómoda: el CLAUDE.md es contexto, no un candado. Puedes escribir “pasa siempre Prettier después de editar” y funcionará el 90 % de las veces. El 10 % restante es un diff sucio en tu PR.

Los hooks son la otra mitad. Son comandos que Claude Code ejecuta en momentos fijos del ciclo de vida, independientemente de lo que el modelo decida. No se negocian, no se olvidan al compactar el contexto y no dependen de cómo redactaste la instrucción.

La división de trabajo queda así:

Necesitas…Herramienta
Que Claude entienda tus convenciones y las aplique con criterioCLAUDE.md
Que algo pase siempre, sin excepciónHook
Un procedimiento largo que solo hace falta a vecesSkill

Anatomía de un hook

Los hooks viven en los archivos de settings, con tres niveles de anidamiento: evento → matcher → handlers.

ArchivoAlcance¿Se comparte?
~/.claude/settings.jsonTodos tus proyectosNo
.claude/settings.jsonEl proyectoSí, va a git
.claude/settings.local.jsonEl proyecto, solo túNo, va al .gitignore

La estructura mínima:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh",
            "statusMessage": "Formateando..."
          }
        ]
      }
    ]
  }
}

Los eventos que más rinden en frontend son cuatro:

El matcher filtra por nombre de herramienta. Si solo contiene letras, números, guiones y |, se interpreta como lista exacta (Edit|Write); cualquier otro carácter lo convierte en expresión regular (mcp__.*).

Hook 1: formatear lo que se acaba de tocar

El clásico, y el que más ruido quita de los diffs. La gracia está en formatear solo el archivo editado, no el proyecto entero.

El hook recibe un JSON por stdin con, entre otras cosas, tool_name, tool_input y cwd. Para Edit y Write, la ruta está en tool_input.file_path:

#!/usr/bin/env bash
# .claude/hooks/format.sh
set -euo pipefail

file=$(jq -r '.tool_input.file_path // empty')
[[ -z "$file" ]] && exit 0

case "$file" in
  *.ts|*.tsx|*.js|*.jsx|*.astro|*.css|*.md)
    npx prettier --write "$file" >/dev/null 2>&1 || true
    ;;
esac

exit 0

Detalle que se pasa por alto: PostToolUse no bloquea, así que si el formateador falla no rompes la sesión. Por eso el || true. Un hook de formato que aborta el turno porque Prettier no encontró su config es peor que no tener hook.

Hook 2: devolver los errores de tipos a Claude

Este es el que cambia de verdad la experiencia. Cuando Claude edita un .tsx, quieres que él se entere de que rompió los tipos, no enterarte tú diez minutos después.

Un PostToolUse que sale con código distinto de 0 muestra stderr en la transcripción, y Claude lo lee. Es un bucle de realimentación gratis:

#!/usr/bin/env bash
# .claude/hooks/typecheck.sh
set -uo pipefail

file=$(jq -r '.tool_input.file_path // empty')
[[ "$file" != *.ts && "$file" != *.tsx && "$file" != *.astro ]] && exit 0

if ! output=$(npx tsc --noEmit 2>&1); then
  echo "Errores de tipos tras editar $file:" >&2
  echo "$output" | head -20 >&2
  exit 1
fi

Nota el exit 1, no exit 2. En PostToolUse el código 2 tampoco bloquea nada (la herramienta ya se ejecutó), pero cualquier salida distinta de cero muestra stderr. Con proyectos grandes, cambia tsc --noEmit por algo incremental o el hook se convertirá en una espera de quince segundos por cada edición.

Hook 3: bloquear de verdad, con PreToolUse

Aquí es donde los hooks dejan de ser conveniencia y pasan a ser una barrera. PreToolUse es de los eventos que bloquean: sal con código 2 y la llamada a la herramienta no se ejecuta; stderr se le entrega a Claude como motivo.

#!/usr/bin/env bash
# .claude/hooks/protect-paths.sh
set -euo pipefail

file=$(jq -r '.tool_input.file_path // empty')

case "$file" in
  */dist/*|*/.astro/*|*/node_modules/*|*package-lock.json)
    echo "Ruta generada: $file. Edita el origen, no la salida del build." >&2
    exit 2
    ;;
esac

exit 0

Con el registro correspondiente:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-paths.sh"
          }
        ]
      }
    ]
  }
}

Si prefieres un control más fino, en lugar de salir con 2 puedes salir con 0 e imprimir JSON en stdout:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Los archivos de dist/ se generan, no se editan."
  }
}

permissionDecision acepta allow, deny, ask y defer. El modo ask es el punto medio interesante: no prohíbes, obligas a que te pregunten.

Hook 4: la puerta de salida

El evento Stop se dispara cuando Claude termina de responder, y también bloquea: código 2 le impide detenerse y lo obliga a seguir trabajando. Es el sitio para la comprobación que de verdad define “terminado” en tu proyecto.

En este blog, “terminado” significa que astro check pasa:

#!/usr/bin/env bash
# .claude/hooks/gate.sh
set -uo pipefail

if ! output=$(npm run build 2>&1); then
  echo "El build falla. Aún no has terminado:" >&2
  echo "$output" | tail -30 >&2
  exit 2
fi

Úsalo con cabeza. Un Stop que lanza el build completo en cada turno convierte una sesión ágil en una espera continua. Dos alternativas mejores según el caso:

Filtrar por comando con if

Para el Bash genérico, el matcher se queda corto: te interesa distinguir git status de git push. Para eso está el campo if, con sintaxis de reglas de permisos:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git push *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/confirm-push.sh"
          }
        ]
      }
    ]
  }
}

Es más robusto de lo que parece: las asignaciones de variables al principio se descartan antes de comparar, y los subcomandos también se revisan, así que npm test && git push también encaja con Bash(git push *).

Errores que vas a cometer

Olvidar que stdin es JSON. El hook no recibe argumentos con la ruta del archivo; recibe un objeto por la entrada estándar. Sin jq (o equivalente) no vas a ninguna parte.

Poner rutas relativas en command. El directorio de trabajo puede no ser el que crees. Usa siempre ${CLAUDE_PROJECT_DIR}, que además se exporta como variable de entorno a los procesos hijos.

Confundir los códigos de salida. Solo el 2 bloquea, y solo en los eventos que aceptan bloqueo. Cualquier otro código distinto de cero es un error no bloqueante: se muestra stderr y la acción continúa.

Hacer hooks lentos. El timeout por defecto es generoso (600 segundos), pero cada segundo de hook es un segundo que estás esperando. Filtra por extensión al principio del script y sal pronto.

Para depurar, /hooks dentro de una sesión lista todos los hooks configurados, sus matchers y de qué archivo salen. Es lo primero que hay que mirar cuando algo “no se ejecuta”.

Dónde encaja esto

Los hooks son la capa determinista: lo que no puede fallar. Encima va la capa de criterio, que son las skills — procedimientos que Claude carga cuando hacen falta y no antes. Es el siguiente artículo.

Y si vienes del mundo de los contenedores, la analogía es directa: los hooks son a Claude Code lo que un entrypoint bien escrito es a una imagen de Docker. Si te interesa esa comparación, el artículo sobre Docker para desarrolladores frontend cubre el otro lado del entorno reproducible.


Parte de la serie Claude Code para frontend engineers. ¿Quieres montar esta capa de automatización en el repositorio de tu equipo sin romper el flujo de nadie? Escríbeme o mira en qué trabajo.

Serie

Claude Code para frontend engineers

Parte 03 de 08

  1. Claude Code para frontend engineers: la serie
  2. CLAUDE.md: tu sistema de diseño como contrato
  3. Hooks de Claude Code: el linter que se ejecuta solo (estás aquí)
  4. Skills de Claude Code: empaqueta tu criterio
  5. Subagentes de Claude Code: revisión en paralelo
  6. MCP en frontend: darle ojos a Claude Code
  7. Claude Code en CI: el PR que se revisa solo
  8. Agent SDK: migraciones masivas de frontend
Continue ▶ Skills de Claude Code: empaqueta tu criterio

Power-ups

Level complete

¿Te sirvió? Compártelo y sigue con el siguiente nivel.