logo
tonny.wtf
Published on

Claude Code Hooks: cómo montar una red de seguridad base

Authors

¿Alguna vez le has pedido a Claude Code que haga algo y ha pasado de ti?

Le dices que formatee el código y ni caso. Le adviertes que no toque cierto archivo y va directo a modificarlo. Le pides que pase los tests antes de dar por terminada la tarea y se le olvida.

El problema es que CLAUDE.md es, en el fondo, una sugerencia. Claude lo lee y muchas veces lo sigue, pero en un entorno real un 80% de acierto no sirve. Cuando quieres control de verdad, necesitas usar hooks.

Los hooks no son sugerencias. Son reglas automáticas. Se disparan cada vez que Claude edita un archivo, ejecuta un comando o termina una tarea.

Hoy os comparto cómo pienso el setup base de hooks para cualquier proyecto: las 8 configuraciones que suelo meter en el settings.json nada más empezar y de las que me olvido después.

Cómo funcionan los hooks, en 30 segundos

Son scripts que se ejecutan en segundo plano cuando Claude Code interactúa con tu sistema. Los configuras una vez y trabajan solos.

Los dos tipos principales que vas a usar son estos:

  • PreToolUse: se ejecuta antes de que Claude haga algo. Puedes inspeccionar la acción y bloquearla si devuelves exit code 2. Piensa en ello como un middleware de validación.
  • PostToolUse: se ejecuta después de la acción. Ideal para lanzar formateadores, tests o guardar logs de lo que ha hecho.

Y los puedes configurar en tres niveles:

  • .claude/settings.json → a nivel de proyecto. El sitio ideal porque lo subes a Git y todo el equipo comparte las mismas reglas.
  • ~/.claude/settings.json → a nivel de usuario, para todos tus proyectos locales.
  • .claude/settings.local.json → sólo local, no se commitea.

La idea importante

Trabajar con agentes a nivel profesional no va tanto de escribir un prompt mágico como de montar el arnés adecuado para que no descarrilen.

Si quieres que el agente sea útil de verdad, tienes que asumir una cosa: el comportamiento correcto no se pide; se automatiza.

Las 8 configuraciones base

1. Autoformateo en cada archivo que toca

El problema: Claude te escribe código que funciona, pero se salta tus reglas de formato. Poner “usa siempre Prettier” en el prompt funciona a veces. No siempre.

El hook: lanzar Prettier automáticamente tras cada escritura.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "file_path=$(jq -r '.tool_input.file_path // empty'); if [ -n \"$file_path\" ]; then npx prettier --write -- \"$file_path\" 2>/dev/null; fi; exit 0"
          }
        ]
      }
    ]
  }
}

Si usas otro formateador, como black para Python o gofmt para Go, sólo cambias el comando.

Esto debería venir de serie en cualquier proyecto. No por elegancia, sino porque evita commits con formatos raros y reduce ruido en revisión.

2. Bloqueo de comandos destructivos

El problema: Claude tiene permisos para ejecutar un rm -rf, un git reset --hard o un DROP TABLE. Seguramente no lo haga, pero cuando hay datos o infra sensible cerca, “seguramente” no es una política de seguridad.

El hook: bloquear estos comandos antes de que lleguen a ejecutarse.

Crea .claude/hooks/block-dangerous.sh:

#!/usr/bin/env bash
set -euo pipefail
cmd=$(jq -r '.tool_input.command // ""')

dangerous_patterns=(
  "rm -rf"
  "git reset --hard"
  "git push.*--force"
  "DROP TABLE"
  "DROP DATABASE"
  "curl[^|]*\\|[[:space:]]*sh([[:space:]]|$)"
  "wget[^|]*\\|[[:space:]]*bash([[:space:]]|$)"
)

for pattern in "${dangerous_patterns[@]}"; do
  if echo "$cmd" | grep -qiE "$pattern"; then
    echo "Bloqueado: '$cmd' coincide con el patrón peligroso '$pattern'. Propón una alternativa más segura." >&2
    exit 2
  fi
done
exit 0

Y en tu settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

Ese exit 2 es la clave. Es lo que frena la acción y devuelve el error a Claude para que busque otra forma de hacerlo.

3. Proteger archivos sensibles

El problema: Claude puede leer y editar cualquier archivo, incluyendo .env, .git/ o lockfiles que no debería tocar alegremente.

El hook: marcar zonas prohibidas.

Crea .claude/hooks/protect-files.sh:

#!/usr/bin/env bash
set -euo pipefail
file=$(jq -r '.tool_input.file_path // .tool_input.path // ""')

protected=(
  ".env*"
  ".git/*"
  "package-lock.json"
  "yarn.lock"
  "*.pem"
  "*.key"
  "secrets/*"
)

for pattern in "${protected[@]}"; do
  if [[ "$file" == $pattern ]]; then
    echo "Bloqueado: '$file' está protegido. Explica por qué necesitas editar este archivo." >&2
    exit 2
  fi
done
exit 0

Añádelo al JSON:

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

No se trata de desconfiar del modelo. Se trata de que hay archivos que no merece la pena dejar expuestos a errores tontos.

4. Tests automáticos tras editar

El problema: el agente hace un cambio, te dice que ya está, y cuando vas a hacer commit descubres que ha roto media suite.

El hook: correr tests tras cada cambio relevante.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run test --silent 2>&1 | tail -5; exit 0"
          }
        ]
      }
    ]
  }
}

El tail -5 importa. Claude no necesita tragarse 400 líneas de log. Sólo necesita una señal útil para corregir rápido.

Este tipo de feedback loop mejora mucho el resultado final porque el agente ve el fallo en el momento, no media hora después.

5. Nada de PRs con tests rotos

El problema: Claude termina una feature, abre una PR y la integración continua se pone en rojo al instante.

El hook: bloqueo estricto. Si los tests no pasan, no hay PR.

Crea .claude/hooks/require-tests-for-pr.sh:

#!/usr/bin/env bash
set -euo pipefail

if npm run test --silent; then
  exit 0
else
  echo "Los tests están fallando. Arregla los errores antes de crear una PR." >&2
  exit 2
fi

Y configúralo así:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__github__create_pull_request",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/require-tests-for-pr.sh"
          }
        ]
      }
    ]
  }
}

Aquí la lógica es simple: si el agente va a operar sobre tu flujo de entrega, entonces las garantías mínimas tienen que vivir en el sistema, no en el prompt.

6. Auto-linting y reporte

El problema: el código hace lo que toca, pero incumple reglas de ESLint o checks de tipado.

El hook: pasar el linter tras cada edición para que Claude vea el error y lo arregle al momento.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "file_path=$(jq -r '.tool_input.file_path // empty'); if [ -n \"$file_path\" ]; then npx eslint --fix -- \"$file_path\" 2>&1 | tail -10; fi; exit 0"
          }
        ]
      }
    ]
  }
}

Si juntas este hook con el de Prettier, el código ya llega bastante limpio antes siquiera de que tú lo mires.

7. Log de auditoría de comandos

El problema: Claude puede lanzar bastantes comandos para entender el entorno. Si algo se rompe, necesitas trazabilidad.

El hook: guardar cada comando con timestamp.

Crea .claude/hooks/log-commands.sh:

#!/usr/bin/env bash
set -euo pipefail
cmd=$(jq -r '.tool_input.command // ""')
printf '%s %s\n' "$(date -Is)" "$cmd" >> .claude/command-log.txt
exit 0

Y añádelo a la configuración:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/log-commands.sh"
          }
        ]
      }
    ]
  }
}

Eso sí: mete .claude/command-log.txt en el .gitignore para no ensuciar el repo.

8. Auto-commit al terminar

El problema: el agente acaba una tarea, te distraes, y el siguiente cambio se mezcla con el anterior en un diff enorme.

El hook: hacer commit automáticamente al final.

Crea .claude/hooks/auto-commit.sh:

#!/usr/bin/env bash
set -euo pipefail
# Solo stagea cambios en ficheros ya trackeados; evita incluir ficheros nuevos no deseados.
git add -u
if ! git diff --cached --quiet; then
  git commit -m "chore(ai): aplicar cambios de Claude"
fi
exit 0

Y engánchalo al evento Stop:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/auto-commit.sh"
          }
        ]
      }
    ]
  }
}

No es imprescindible en todos los proyectos, pero cuando trabajas mucho con agentes ayuda a mantener cambios pequeños y separados.

El settings.json unificado

Para no ir copiando trozos, este sería el archivo completo.

Sólo tienes que crear .claude/hooks/, meter los scripts .sh, darles permisos con chmod +x .claude/hooks/*.sh, y pegar esto en .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/block-dangerous.sh" },
          { "type": "command", "command": ".claude/hooks/log-commands.sh" }
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/protect-files.sh" }
        ]
      },
      {
        "matcher": "mcp__github__create_pull_request",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/require-tests-for-pr.sh" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          { "type": "command", "command": "file_path=$(jq -r '.tool_input.file_path // empty'); if [ -n \"$file_path\" ]; then npx prettier --write -- \"$file_path\" 2>/dev/null; fi; exit 0" },
          { "type": "command", "command": "file_path=$(jq -r '.tool_input.file_path // empty'); if [ -n \"$file_path\" ]; then npx eslint --fix -- \"$file_path\" 2>&1 | tail -10; fi; exit 0" }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/auto-commit.sh" }
        ]
      }
    ]
  }
}

Mi recomendación real

Si vais a montar esto hoy, yo empezaría por dos cosas:

  1. autoformateo
  2. bloqueo de comandos destructivos

Sólo con eso ya te quitas una buena parte de los fallos más comunes.

El resto lo iría añadiendo cuando el proyecto lo pida: tests si el repo ya tiene suite fiable, linting si de verdad aporta señal, auditoría si estás en un entorno sensible, y auto-commit sólo si te encaja en el flujo.

Porque esa es la idea de fondo: no necesitas meter veinte barreras desde el día uno, pero sí poner las que convierten a un agente en algo razonablemente seguro de operar.