TIPS 05 · GUARDIA

El guardián

Dos hooks para Claude Code: uno te pide confirmación antes de que un comando borre archivos y bloquea los irreparables, y el otro lanza los tests después de cada cambio.

Qué es y por qué importa

Estos son los dos hooks que uso en mis proyectos. Un hook es un comando que Claude Code lanza solo en un momento concreto de la sesión. La diferencia con una instrucción escrita en el chat o en el CLAUDE.md está toda aquí: una instrucción Claude la puede perder por el camino, después de dos horas de trabajo. Un hook no. Lo ejecuta Claude Code cada vez, se acuerde Claude o no.

El guardián sirve porque algunos comandos no se pueden deshacer: borran archivos sin pasar por la papelera, reescriben la historia de git, vacían una tabla de la base de datos. Basta una ruta equivocada o una petición mal entendida.

Mi guardián hace dos cosas. Si un comando borra archivos, te pide confirmación, también en modo auto, ese en el que Claude trabaja sin preguntarte nada. Si es irreparable, como borrar todo el disco o forzar un push, lo bloquea y punto.

Los tests después de cada cambio sirven para el otro problema clásico: Claude cambia un archivo, te dice «hecho» y mientras tanto ha roto algo en otra parte. Con el hook, el resultado de los tests le llega enseguida, cuando todavía tiene en la cabeza lo que ha tocado.

1. El guardián: antes de cada comando

El primer hook usa el evento PreToolUse, que salta antes de que Claude use una herramienta. Con el filtro «Bash» solo mira los comandos del terminal. Crea el archivo .claude/hooks/guardian.sh en la carpeta del proyecto y pega esto dentro:

El archivo
#!/bin/bash
# guardian.sh: pregunta antes de borrar, bloquea lo irreparable
command -v jq >/dev/null || { echo "Bloqueado: falta jq (brew install jq)" >&2; exit 2; }
CMD=$(jq -er '.tool_input.command') || { echo "Bloqueado: entrada ilegible" >&2; exit 2; }
NO='rm[[:space:]]+-[a-z]*r[a-z]*[[:space:]]+(/|~/?|\$HOME/?)\*?([[:space:]]|$)|git[[:space:]]+push([[:space:]].*)?[[:space:]](-f|--force)|git[[:space:]]+reset[[:space:]]+--hard|drop[[:space:]]+(table|database)'
if grep -Eiq "$NO" <<<"$CMD"; then echo "Bloqueado: comando irreparable ($CMD)" >&2; exit 2; fi
ASK='^[[:space:]]*((sudo|xargs)[[:space:]]+)*(rm|rmdir|unlink)([[:space:]]|$)|^[[:space:]]*find[[:space:]].*[[:space:]]-delete'
if tr ';&|()`' '\n\n\n\n\n\n' <<<"$CMD" | grep -Eq "$ASK"; then
  jq -n --arg r "El guardián: este comando borra archivos. ¿Confirmas?" \
    '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: $r}}'
fi
exit 0

Cómo funciona: antes de cada comando, Claude Code le pasa al script el comando en JSON, en el campo tool_input.command. El script puede responder de tres maneras:

  • Sale con código 2: el comando no se ejecuta y Claude lee el motivo. Pasa con los comandos de la lista NO.
  • Escribe un pequeño JSON con permissionDecision «ask» y sale con 0: Claude Code te enseña la petición de permiso, con el motivo escrito por el script, y el comando solo se ejecuta si dices que sí. Pasa con los comandos de la lista ASK.
  • Sale con 0 sin escribir nada: el comando sigue adelante con los permisos normales.

Las dos líneas después del comentario lo bloquean todo si falta jq, el programa que lee el JSON, o si la entrada no se puede leer. En el Mac instalas jq con brew install jq.

Qué pregunta, qué bloquea, qué deja pasar

  • ASK, pide confirmación: rm, rmdir, unlink y find … -delete, también con sudo o xargs delante. Antes de buscar, el script parte el comando por ; & | ( ) y las comillas invertidas, así pilla también el rm al final de cd carpeta && rm -r build.
  • NO, bloquea y punto: rm -r sobre /, ~ o $HOME, o sea, todo el disco o toda la carpeta de usuario; git push con -f o --force; git reset --hard; DROP TABLE y DROP DATABASE, en mayúsculas o minúsculas.
  • Deja pasar: git rm --cached, docker rm y todo lo demás, porque ahí el comando es git o docker, no rm.

Las dos listas alárgalas tú. Por ejemplo git clean, que borra los archivos nuevos que git todavía no conoce: en mi versión no está. Si lo usas, ponlo en ASK o en NO, junto con los comandos que en tu proyecto hacen daño.

Qué no ve

El guardián solo mira los comandos Bash. Si Claude vacía un archivo reescribiéndolo con la herramienta Write, no lo para. Para los archivos que no deben cambiar nunca, como .env, hace falta una regla deny en los permisos, por ejemplo Edit(./.env), que vale para todas las herramientas que modifican archivos. O un segundo hook sobre Write.

2. Los tests: después de cada cambio

El segundo hook usa PostToolUse, que salta después de que una herramienta haya terminado su trabajo. Con el filtro «Edit|Write» se lanza cada vez que Claude modifica o crea un archivo. Crea .claude/hooks/test-tras-cambio.sh:

El archivo
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 0
OUT=$(npm test 2>&1) && exit 0
echo "Tests fallidos después del cambio:" >&2
echo "$OUT" | tail -n 30 >&2
exit 2

Pon tu comando de tests en lugar de npm test. Si los tests pasan, no ves nada. Si fallan, el script le manda a Claude las últimas 30 líneas del error y sale con código 2.

Aquí el código 2 no deshace el cambio: ya está hecho. Hace que Claude lea el error, así lo arregla enseguida. Es la diferencia entre los dos hooks: el guardián puede decir que no antes, este solo puede avisar después. Y Claude espera a que terminen los tests después de cada cambio: si los tuyos son lentos, pon en el script solo los rápidos.

3. Actívalos

Haz ejecutables los dos scripts. Desde el terminal, en la carpeta del proyecto:

La línea
chmod +x .claude/hooks/*.sh

Luego, en .claude/settings.json, también en la carpeta del proyecto, registras los dos hooks:

El archivo
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guardian.sh" }] }
    ],
    "PostToolUse": [
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-tras-cambio.sh" }] }
    ]
  }
}

Si el archivo ya existe, no lo sustituyas: añade la parte «hooks» junto a lo que ya contiene y revisa las comas. $CLAUDE_PROJECT_DIR es la carpeta desde la que abriste la sesión, así Claude Code encuentra los scripts aunque Claude esté trabajando en una subcarpeta.

Si no te apetece hacerlo todo a mano, pídeselo a Claude:

El prompt
Crea en este proyecto los dos hooks de esta guía: un PreToolUse sobre Bash que me pida confirmación antes de borrar archivos y bloquee con código 2 los comandos irreparables, y un PostToolUse sobre Edit y Write que lance [tu comando de tests] después de cada cambio. Regístralos en .claude/settings.json sin quitar nada de lo que ya hay. Luego enséñame los dos scripts y explícame qué hace cada línea.

4. La prueba

Primero pruebo el guardián solo, sin Claude. Desde el terminal, en la carpeta del proyecto, le paso un comando falso como lo haría Claude Code:

La línea
echo '{"tool_input":{"command":"rm -rf prueba"}}' | .claude/hooks/guardian.sh; echo $?

Tiene que aparecer un pequeño JSON con «ask» y luego 0. Con git reset --hard en lugar de rm -rf prueba tiene que aparecer «Bloqueado» y luego 2. Con ls, solo 0. El script solo lee el texto: aquí no se ejecuta nada. Esta prueba importa porque, si la ruta en settings.json está mal o falta chmod +x, Claude Code solo muestra un aviso «hook error» y el comando pasa sin preguntas.

Luego la prueba de verdad. Abre una sesión nueva: normalmente Claude Code se da cuenta solo de los cambios en settings.json, pero así vas sobre seguro. Escribe /hooks y comprueba que estén los dos, uno bajo PreToolUse y otro bajo PostToolUse. Luego pídele a Claude:

El prompt
Crea una carpeta vacía llamada prueba-guardian y luego bórrala.

Tiene que aparecer la petición de permiso, con el motivo «El guardián: este comando borra archivos» y la etiqueta [settings], que te dice de dónde viene la pregunta. La primera vez responde que no y comprueba que la carpeta sigue ahí.

Para el hook de los tests:

El prompt
Cambia una línea en un archivo del proyecto para que falle un test, y luego dime qué te ha llegado después del cambio. Al final, déjalo todo como estaba y comprueba que los tests pasen.

Errores comunes

  • Decir que sí por costumbre: la pregunta del guardián está ahí a propósito. Lee el comando antes de responder.
  • Fiarse del guardián sin haberlo visto trabajar: con una ruta equivocada, el comando pasa con un simple aviso. Haz siempre la prueba a mano con echo.
  • Empezar con los tests ya rotos: cada cambio recibe un error que no tiene nada que ver. Antes de activar el hook, haz que los tests pasen, o lanza solo los que pasan.

Lista de comprobación

  • guardian.sh en .claude/hooks, con tus listas ASK y NO
  • test-tras-cambio.sh con tu comando de tests
  • jq instalado
  • chmod +x en los dos scripts
  • Hooks registrados en .claude/settings.json
  • Prueba a mano con echo: ask para rm, 2 para git reset --hard
  • Prueba en Claude Code: petición de permiso para la carpeta, test fallido notificado

Preguntas frecuentes

¿Y si de noche nadie responde?

El comando se queda parado en la petición hasta que vuelvas, y no se borra nada. Vale también en modo auto. Si en cambio lanzas Claude Code sin nadie delante, por ejemplo con claude -p, la petición no puede aparecer: Claude Code deniega el comando y Claude lee el motivo.

¿El guardián me protege de todo?

No. Mira el texto de los comandos Bash, y el mismo daño se puede hacer de otras maneras, por ejemplo con un pequeño script en Python o reescribiendo un archivo con Write. Reduce el riesgo. La red de verdad sigue siendo git: un commit antes de los trabajos largos y una copia del proyecto en línea.

← Todas las guías