Guard de force-push y destructivos
El mismo patrón de la lección anterior, pero esta vez vigilando comandos de shell en vez de contenido de ficheros. Vamos a bloquear de raíz git push --force, rm -rf y otros que no quieres que el agente ejecute jamás por su cuenta.
El script
Guarda esto en .claude/hooks/block-dangerous.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Lista negra de patrones peligrosos.
if echo "$COMMAND" | grep -Eq \
'git push +.*--force|git push +.*-f( |$)|rm +-rf|npm publish|git reset +--hard'; then
echo "Bloqueado: '$COMMAND' es destructivo. Si de verdad lo necesitas, ejecútalo tú a mano." >&2
exit 2
fi
exit 0
chmod +x .claude/hooks/block-dangerous.sh
El registro
Ahora el matcher es Bash, porque vigilamos comandos de shell:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh" }
]
}
]
}
}
Funciona igual que el de secrets: corre antes del Bash, saca tool_input.command, y si casa un patrón peligroso hace exit 2 con el motivo. El agente recibe el mensaje y, en vez de insistir, te deja la decisión a ti.
Que no corra en cada
Bash. Tal cual, el hook se lanza con todos los comandos y filtra dentro del script. Si solo te interesan ciertos comandos, usa el campoifque viste en la anatomía para acotarlo desde el propiosettings.json—p.ej."if": "Bash(git push *)"—: el script solo se ejecuta cuando el comando casa, y te ahorras una invocación por cadals.
Pruébalo
Con el hook registrado, pídele al agente que ejecute algo de la lista negra y mira qué pasa:
"Haz un
git push --forcea la rama actual." "Borra la carpeta de build conrm -rf build/."
No debería ejecutarse. El hook corta con exit 2 y devuelve tu mensaje (Bloqueado: '…' es destructivo…); el agente lo lee y te deja la decisión a ti. Si quieres verlo sin agente de por medio, simúlalo a mano:
echo '{"tool_input":{"command":"rm -rf build/"}}' | .claude/hooks/block-dangerous.sh
echo "código de salida: $?"
Verás el mensaje en stderr y código de salida: 2. Ese bloqueo determinista —pase lo que pase, siempre corta— es el artefacto del curso.
El regex es un punto de partida, no un muro
No sobreconfíes en este patrón. Una lista negra por regex reduce accidentes, pero no es seguridad fuerte: variantes equivalentes del mismo comando se le escapan.
El ejemplo más claro está en el propio rm -rf. El patrón rm +-rf exige el orden literal -rf, así que no casa:
rm -fr foo→ mismas opciones, orden invertido. Se escapa.rm -r -f foo→ flags separadas. Se escapa.
Y eso es solo el principio: comandos encadenados con && o ;, envueltos en bash -c "…", o con la ruta variabilizada pueden esquivar patrones ingenuos. La conclusión no es "haz un regex perfecto" (no existe), sino defensa en profundidad: combina este guard con deny en permisos, sandbox y revisión humana, como ya vimos. Cada capa atrapa lo que se le cuela a la anterior.
Si quieres cubrir al menos las variantes de rm, usa un patrón más laxo que no dependa del orden de las letras:
rm +-[a-z]*r|rm +-[a-z]*f
Esto casa rm -rf, rm -fr y el -r de rm -r -f. Aun así no es exhaustivo (no cubre rutas raras ni encadenados): es un guard mejor, no una garantía.
Hook vs permiso deny: ¿no era esto lo mismo?
Buena pregunta, y la respuesta es importante. En el curso de permisos bloqueabas force-push con un deny: ["Bash(git push --force*)"]. ¿Por qué un hook entonces?
deny en permisos | Hook PreToolUse | |
|---|---|---|
| Define la regla | con un patrón de comando | con código (puede mirar lo que quieras) |
| Bueno para | casos simples y declarativos | lógica compleja: combinaciones, contexto, mensajes a medida |
Para git push --force a secas, el deny sobra. El hook brilla cuando la regla necesita lógica: "bloquea rm -rf salvo dentro de /tmp", "permite force-push en ramas feature/* pero no en main". Eso un patrón no lo expresa; un script sí.
Y son complementarios: el
denyy el hook pueden convivir. Defensa en profundidad otra vez.
Amplía tu lista negra
Añade lo que tu equipo no quiere automatizado: terraform apply, kubectl delete, DROP TABLE en scripts de DB, despliegues a prod. La regla de oro: bloquea lo irreversible que no debería pasar sin un humano delante.
Qué llevas hasta aquí
- Un
PreToolUsecon matcherBashvigila comandos; sacatool_input.commandy bloquea conexit 2. - Un hook supera al
denycuando la regla necesita lógica (contexto, excepciones, mensajes); para patrones simples, eldenybasta. - Ambos se combinan: defensa en profundidad.
En la última lección, un hook que valida la calidad de algo, no solo lo bloquea: Conventional Commits.
Fuente oficial: Hooks reference
¿Quieres llevar la cuenta de las lecciones que has terminado?
Entrar para guardar progreso