Hooks en Claude Code: automatiza y bloquea acciones del agente en el punto exacto
Resumen
Guía práctica de hooks en Claude Code para agentes de IA: configuración por evento y matcher en settings.json, JSON que llega por stdin, códigos de salida que bloquean de verdad (exit 2), decisiones allow, deny y ask con hookSpecificOutput, y un ejemplo real para frenar comandos destructivos sin tocar el turno del modelo.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Un agente de código que solo "pide permiso" deja decisiones importantes al azar del modelo. Los hooks de Claude Code resuelven eso al revés: tú defines comandos que se ejecutan automáticamente en puntos fijos del ciclo de vida (antes de una herramienta, al enviar un prompt, al terminar una sesión) y el agente debe pasar por ahí sí o sí. Es la diferencia entre confiar en que el modelo decida bien y tener un control determinístico en el punto exacto donde se ejecuta la acción.
Esta guía cubre la configuración real en 2026: dónde viven los hooks, qué eventos existen, cómo escriben matchers sin sorpresas, qué significa cada código de salida y cómo bloquear acciones peligrosas con un ejemplo completo.
Qué problema resuelven los hooks
Un prompt o una regla en CLAUDE.md es una sugerencia: el modelo puede ignorarla. Un hook es ejecución: corre como proceso real y su decisión se aplica antes de que la herramienta toque tu disco. Usos típicos:
- Bloquear comandos destructivos (
rm -rf,git push --force,DROP TABLE) enPreToolUse, sin importar qué planea el modelo. - Inyectar contexto automático al iniciar sesión (
SessionStart): rama activa, issues, estado del repo. - Avisar al modelo después de ejecutar algo (
PostToolUse): lint que falló, test roto, advertencia de seguridad. - Auditar: registrar cada llamada a herramientas en un log externo sin tocar el flujo.
La regla mental: el prompt educa, el hook obliga. Para todo lo que sea política dura (seguridad, dinero, datos), hook. Para convenciones de estilo, contexto.
Dónde se configuran
Los hooks viven en archivos de settings JSON, con distinto alcance:
| Ubicación | Alcance | Se comparte |
|---|---|---|
~/.claude/settings.json | Todos tus proyectos | No (local) |
.claude/settings.json | Un proyecto | Sí (commiteable) |
.claude/settings.local.json | Un proyecto | No (gitignored) |
| Managed policy settings | Toda la organización | Sí (admin) |
Plugin hooks/hooks.json | Mientras el plugin esté activo | Sí (viene con el plugin) |
La elección práctica: hooks de seguridad de repo van en .claude/settings.json (el equipo hereda la política al clonar); preferencias personales en ~/.claude/settings.json; experimentos en settings.local.json. En sesiones cloud de Claude Code on the web no se lee tu settings local: los hooks llegan del repo o de la organización, otro motivo para commitearlos.
La estructura tiene tres niveles: eliges el evento, opcionalmente un matcher que filtra cuándo aplica, y uno o más handlers (comandos shell) que se ejecutan cuando hay match.
Los eventos que importan
Claude Code dispara eventos en tres ritmos: una vez por sesión, una vez por turno y en cada llamada a herramientas del loop agéntico.
| Evento | Cuándo dispara | Uso típico |
|---|---|---|
SessionStart | Al iniciar o reanudar sesión | Inyectar contexto del repo |
UserPromptSubmit | Al enviar un prompt, antes de procesarlo | Validar o bloquear prompts |
PreToolUse | Antes de cada llamada a herramienta | Bloquear o permitir tools |
PostToolUse | Después de cada llamada a herramienta | Feedback al modelo, auditoría |
Stop | Cuando Claude intenta terminar el turno | Forzar verificación antes de parar |
SessionEnd | Al cerrar la sesión | Limpieza, reportes |
PreCompact / PostCompact | Antes/después de compactar contexto | Preservar estado importante |

Los dos que vas a usar casi siempre son PreToolUse (el control antes del daño) y PostToolUse (el feedback después de la acción). Stop es el subestimado: un hook que rechaza la parada obliga al agente a seguir trabajando, útil para gates de calidad tipo "no termines si el build falla".
Matchers: filtra por herramienta sin regex innecesaria
El matcher decide para qué tools se dispara el hook en PreToolUse y PostToolUse. La tabla del matcher es más simple de lo que parece:
| Matcher | Comportamiento |
|---|---|
"*" o vacío | Match con todo |
Bash | Solo la tool Bash (exacto) |
Edit|Write | Bash-style OR: Edit o Write |
^Notebook | Regex: cualquier tool que empiece con Notebook |
Solo si el matcher contiene caracteres especiales se interpreta como regex JavaScript no anclada. Ojo con Edit.*: matchea tanto Edit como NotebookEdit; si quieres solo Edit, ancla con ^Edit$. Y en versiones viejas (< v2.1.195) un nombre con guion como code-reviewer caía en el camino regex y matcheaba de más: ancla con ^code-reviewer$ si tu organización aún corre builds viejos.
El JSON que llega por stdin
Tu hook recibe un JSON por stdin con contexto del evento. Para PreToolUse de un comando Bash:
{
"session_id": "abc123",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run test suite"
},
"cwd": "/home/user/my-project",
"permission_mode": "default"
}
Campos que usarás en el 90% de los hooks: tool_name (qué herramienta), tool_input (con qué argumentos, incluido el comando crudo en Bash) y permission_mode (el modo actual: default, plan, acceptEdits, bypassPermissions). Con eso puedes tomar decisiones reales: bloquear rm salvo en un whitelist, exigir doble confirmación en modo bypassPermissions, etc.
Códigos de salida: lo que sí bloquea
Aquí está el detalle que más bugs evita. El exit code de tu hook decide:
| Exit code | Efecto |
|---|---|
0 | Éxito. stdout se procesa como JSON o texto según formato; no bloquea |
2 | Error bloqueante: la acción se cancela y Claude ve tu stderr |
| Otro (1, 3...) | No bloquea: es un error no bloqueante y el flujo sigue |
El caso trampa: un hook que hace exit 1 con un mensaje de error no detiene la herramienta. Claude Code lo trata como fallo no bloqueante y la acción continúa. Si tu hook es una política de seguridad, el bloqueo es exit 2, sin excepciones. Y aunque imprimas JSON con permissionDecision: "allow", un exit 2 gana: el bloqueo por código es lo único que el JSON no puede anular.
Timeouts: un hook command que se pasa del timeout se cancela y no bloquea la tool call. No confíes en un hook colgado como si fuera un gate: si necesitas bloqueo garantizado, mantén el hook rápido y determinístico.
Decisiones estructuradas con hookSpecificOutput
Además del exit code, tu hook puede imprimir JSON en stdout con decisión estructurada. Para PreToolUse, el formato actual usa hookSpecificOutput (los campos top-level decision/reason están deprecados para este evento):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Comando destructivo bloqueado por política del repo"
}
}
Los tres valores de permissionDecision:
allow: salta el prompt de permiso y ejecuta la tool.deny: bloquea la tool; la razón se muestra a Claude (puede proponer una alternativa).ask: fuerza confirmación del usuario aunque el modo auto aprobara.
El ejemplo completo para bloquear rm destructivo, guardado en .claude/hooks/block-rm.sh con chmod +x:
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')
if [[ "$command" == rm* ]]; then
echo "Blocked: rm commands are not allowed by repo policy" >&2
exit 2
fi
exit 0
Y su registro en .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-rm.sh"
}
]
}
]
}
}

Checklist para escribir hooks que no rompan la sesión
- Determinístico y rápido: un hook corre en cada tool call; nada de redes lentas ni scripts pesados. Si necesita estado, escribe a un log local.
- Fail-open claro: si tu hook falla por un bug, ¿debería bloquear todo? Para políticas de seguridad el fail-closed es
exit 2; para conveniencia, mejor no bloquear y avisar. - Usa
$CLAUDE_PROJECT_DIRpara rutas: el hook corre desde el cwd de la sesión, y una ruta relativa rompe en sesiones desde subcarpetas. jqsobre parsing manual: el JSON llega por stdin yjq -res la forma estable de extraer campos sin regex frágiles.- Prueba primero con
--debug: los hooks fallidos silenciosos (ruta mal escrita en settings.json) dejan la política sin efecto. El debug log muestra qué hooks matchearon y sus exit codes. PostToolUsepara feedback, no veto: el tool call ya corrió; sirve para avisar al modelo de errores que debe corregir, no para "deshacer".
Errores comunes
"Mi hook no hace nada" — revisa la ruta del comando en settings.json y ejecuta claude --debug para ver el log. El fallo más común es una ruta que no existe o un script sin chmod +x: el hook "corre", falla con 127 y el flujo sigue como si nada.
"Puse exit 1 y no bloqueó" — es el comportamiento documentado: solo exit 2 bloquea por código. Cambia el 1 por 2 o usa JSON con permissionDecision: "deny".
"El hook matchea de más" — probable regex sin anclar: Edit.* incluye NotebookEdit. Usa ^Edit$ o la lista exacta Edit|Write.
"El JSON del hook no se aplica" — en eventos con modelo de decisión estándar, un JSON inválido produce error no bloqueante y la acción sigue. Valida el JSON (por ejemplo, con jq . en desarrollo) antes de depender de él.
FAQ
¿Los hooks funcionan en subagentes?
Sí. Los hooks de settings, managed policy y plugins también corren dentro de subagentes: cuando un subagente llama a una tool, los eventos PreToolUse y PostToolUse disparan los mismos hooks, y el JSON de entrada trae agent_id y agent_type para distinguir la llamada del hilo principal.
¿Se pueden ejecutar hooks HTTP o con MCP tools?
Sí. Además del tipo command, los handlers pueden ser endpoints HTTP (el JSON llega como body del POST) o llamadas a MCP tools. Para endpoints HTTP, las allowlists allowedHttpHookUrls e httpHookAllowedEnvVars de settings controlan qué URLs se permiten y qué variables de entorno se interpolan en headers.
¿Los hooks reemplazan el sistema de permisos?
No, lo complementan. Las reglas de allow/deny de permisos se evalúan igual, decida lo que decida el hook: un allow del hook no anula una deny de permisos, y un deny del hook se suma a las reglas existentes. Piensa en los hooks como una capa de política programable encima del modelo de permisos.
¿Puedo inyectar contexto con hooks?
Sí. SessionStart acepta additionalContext en su hookSpecificOutput (o texto plano en stdout) y PreToolUse también acepta additionalContext junto a la decisión, útil para pasarle al modelo información del entorno justo antes de ejecutar una tool.
Conclusión
Los hooks convierten a Claude Code de "agente que pide permiso" a "agente con puntos de control verificables": el evento correcto, un matcher exacto y un exit code que sí bloquea. Empieza por uno solo: un PreToolUse en Bash que frene los comandos destructivos de tu repo. Cuando ese hook lleve unas semanas salvándote de un rm mal generado, el resto (contexto de sesión, gates de calidad en Stop, auditoría en PostToolUse) se construye solo.
Si el siguiente paso es aislar el entorno completo donde corre el agente, la guía de sandboxing y permisos para agentes de código cubre la otra mitad: qué puede tocar el agente una vez que la acción ya pasó el hook. Para proteger los datos que el agente lee y escribe, revisa cómo evitar fugas de datos en agentes de IA. Y cuando empieces a delegar trabajo a subagentes, cuándo delegar en subagentes (y cuándo no) marca la línea entre paralelizar bien y romper el repo.
Lecturas relacionadas
Sigue explorando Coding Agents y otras piezas para builders.

Kimi Code CLI: guía práctica del agente de IA en la terminal

git pickaxe: encontrar el commit que introdujo una línea con git log -S y -G

Slash commands en Claude Code para agentes: crea tu /comando
