Biblioteca · Hooks y agentes auxiliares

Hooks EN VIVO: construimos hooks desde cero

Creador75 minActualizado: octubre de 2026
36 de 105 en la biblioteca

Módulo: 7. Hooks, autonomía a nivel de sistema | Tiempo: ~15 min de teoría + 60 min de práctica


Lo esencial

En la lección anterior (Hooks: reglas automáticas) vimos la teoría: unos 30 eventos, 5 tipos de manejadores, el protocolo JSON. Aquí van tres hooks reales que se usan todos los días. El primero protege contra la filtración accidental de claves de API. El segundo evita que te pases del presupuesto. El tercero escribe un registro completo de qué tocó Claude y cuándo. De regalo, un webhook HTTP para avisos externos. Los construimos desde cero y revisamos cada línea.


Conceptos clave

  • pre-tool-use-no-secrets.sh: revisa los archivos antes de escribirlos en busca de patrones de secretos
  • pre-tool-use-budget-check.sh: revisa un contador de operaciones y detiene todo si se pasó el límite
  • post-tool-use-audit-log.sh: anota en un registro cada cambio de archivo con su timestamp
  • exit code 0 / 2: cómo el hook le dice a Claude Code "permitir" (0) o "bloquear" (2)
  • matcher: un filtro: a qué herramientas reacciona ("Write|Edit", "Bash", "*")
  • tool_input: un objeto JSON con los datos de entrada de la herramienta (ruta, contenido, comando)
  • grep: búsqueda de patrones (claves de API, tokens) en el contenido de los archivos
  • jq: lee el JSON que Claude Code le pasa al hook por stdin
  • Probar el hook: cómo comprobar que el hook se dispara bien

Teoría

Cómo funciona un hook por dentro

🎨 Imagínalo así: un hook es la aduana en la frontera. Cada cargamento (una herramienta) pasa por el escáner (el JSON por stdin). La aduana revisa y o lo deja pasar (exit 0) o lo detiene (exit 2). El cargamento no sabe que existe la aduana: solo avanza por la banda.

Claude Code llama al hook como un script de bash normal. Le pasa los datos por stdin en formato JSON. El hook analiza los datos, aplica su lógica y devuelve el resultado mediante el exit code.

Código
Claude Code quiere escribir un archivo
        ↓
Llama al hook PreToolUse (matcher: "Write|Edit")
        ↓
Le pasa por stdin el JSON:
{
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/project/config.py",
    "content": "API_KEY = 'sk-proj-abc123...'"
  },
  "session_id": "abc123",
  "cwd": "/Users/me/project"
}
        ↓
El hook analiza tool_input.content
        ↓
exit 0 → Claude Code escribe el archivo
exit 2 → Claude Code se detiene y el stderr del hook se le pasa a Claude

Importante: lo que bloquea es el exit code 2, no el 1. El exit code 1 es un error común del script: Claude sigue trabajando.

Al bloquear, el hook escribe un mensaje en stderr (>&2): eso es lo que verá Claude y lo que le dirá al usuario.


🎨 Imagínalo así: el hook no-secrets es como el detector de metales a la entrada de un banco. Traes las llaves en el bolsillo y el arco suena. No porque seas malo: la regla es que las llaves no pasan. Las dejas en la charola y pasas.

Hook 1: pre-tool-use-no-secrets.sh

Tarea: evitar que se escriban por accidente claves de API, tokens y contraseñas dentro del código.

El problema que resuelve: muchas veces se pega una clave directo en el código "por mientras", se olvida quitarla y se sube a git. El hook lo detiene antes de que se escriba el archivo.

Crear el archivo

bash
mkdir -p ~/.claude/hooks
touch ~/.claude/hooks/pre-tool-use-no-secrets.sh
chmod +x ~/.claude/hooks/pre-tool-use-no-secrets.sh

El contenido del script

bash
#!/bin/bash
# pre-tool-use-no-secrets.sh
# Bloquea la escritura de archivos con secretos escritos directo en el código

# Leemos los datos de Claude Code por stdin
INPUT=$(cat)

# Extraemos los datos del formato JSON oficial
# tool_name está en el nivel superior
# file_path y content están dentro de tool_input
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')

# Para la herramienta Edit, el contenido está en el campo new_string
if [[ "$TOOL_NAME" == "Edit" ]]; then
  CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty')
fi

# Revisamos solo las herramientas que escriben archivos
# (el matcher "Write|Edit" en settings.json ya filtra,
#  pero una doble revisión no estorba)
if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
  exit 0  # No es una escritura: la dejamos pasar
fi

# Patrones que buscamos (posibles claves de API y tokens)
PATTERNS=(
  'sk-[a-zA-Z0-9]{20,}'          # OpenAI / Anthropic API keys
  'ghp_[a-zA-Z0-9]{36}'          # GitHub Personal Access Token
  'xoxb-[0-9]+-[a-zA-Z0-9]+'     # Slack Bot Token
  'AKIA[0-9A-Z]{16}'              # AWS Access Key
  'AIza[0-9A-Za-z_-]{35}'         # Google API Key
  'password\s*=\s*["\'][^"\']+["\']'  # Contraseña explícita en el código
  'secret\s*=\s*["\'][^"\']+["\']'    # Secreto explícito en el código
)

# Revisamos el contenido contra cada patrón
for PATTERN in "${PATTERNS[@]}"; do
  if echo "$CONTENT" | grep -qE "$PATTERN"; then
    # Mensaje a stderr: Claude lo verá y se lo pasará al usuario
    echo "BLOQUEADO: se detectó un posible secreto o clave de API en el archivo $FILE_PATH" >&2
    echo "Patrón: $PATTERN" >&2
    echo "Usa variables de entorno (.env) o un gestor de secretos en lugar de escribirlo en el código." >&2
    exit 2  # Exit code 2 = bloquear la acción
  fi
done

exit 0  # No se encontraron secretos: lo permitimos

Agregarlo a settings.json

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/pre-tool-use-no-secrets.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Fíjate en la estructura:

  • "PreToolUse" en CamelCase (no pre_tool_use)
  • "matcher": "Write|Edit": el hook se dispara solo al escribir o editar archivos (no al leer ni con comandos de bash)
  • "type": "command" (no "type": "bash")
  • "timeout": 30: si el script no responde en 30 segundos, Claude sigue

Probar el hook

Dale a Claude Code esta instrucción:

Escribe esto en el chat
Crea el archivo config.py con el contenido: API_KEY = 'sk-proj-test123456789012345678901234'

Resultado esperado:

Escribe esto en el chat
BLOQUEADO: se detectó un posible secreto o clave de API en el archivo config.py
Usa variables de entorno (.env) o un gestor de secretos en lugar de escribirlo en el código.

Claude Code no escribirá el archivo. Te propondrá usar .env.


🎨 Imagínalo así: el hook budget-check es un medidor de agua. Llegaste a 500 litros y se corta el suministro. No porque no haya agua: hay un límite puesto. ¿Quieres más? Abre la llave a mano mañana.

Hook 2: pre-tool-use-budget-check.sh

Tarea: controlar el gasto: detener a Claude Code si hay demasiadas operaciones en un día.

El problema que resuelve: las tareas autónomas largas pueden hacer miles de operaciones. El hook pone un límite estricto.

bash
#!/bin/bash
# pre-tool-use-budget-check.sh
# Control de presupuesto por número de operaciones al día

COUNTER_FILE="/tmp/claude_ops_$(date +%Y%m%d).count"
DAILY_LIMIT=500  # Máximo de operaciones al día

# Leemos el contador actual
if [ -f "$COUNTER_FILE" ]; then
  CURRENT=$(cat "$COUNTER_FILE")
else
  CURRENT=0
fi

# Revisamos el límite
if [ "$CURRENT" -ge "$DAILY_LIMIT" ]; then
  echo "ALTO: se alcanzó el límite diario de operaciones ($CURRENT/$DAILY_LIMIT)" >&2
  echo "Se reinicia a medianoche. Para reiniciarlo a mano: rm $COUNTER_FILE" >&2
  exit 2  # Exit code 2 = bloquear
fi

# Aumentamos el contador
echo $((CURRENT + 1)) > "$COUNTER_FILE"

# Aviso al llegar al 80% (por stdout: no bloquea)
THRESHOLD=$((DAILY_LIMIT * 80 / 100))
if [ "$CURRENT" -ge "$THRESHOLD" ]; then
  echo "AVISO: llevas $CURRENT/$DAILY_LIMIT operaciones (80% del límite)"
fi

exit 0

Agregarlo a settings.json (junto al primer hook)

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/pre-tool-use-no-secrets.sh",
            "timeout": 30
          }
        ]
      },
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/pre-tool-use-budget-check.sh"
          }
        ]
      }
    ]
  }
}

Fíjate: el primer hook tiene "matcher": "Write|Edit": solo revisa la escritura de archivos. El segundo no tiene matcher, así que se dispara con todas las herramientas.

Varios hooks PreToolUse se ejecutan en paralelo. Si cualquiera devuelve exit 2, la acción se bloquea.


🎨 Imagínalo así: el hook audit-log es la grabadora de vuelo (la caja negra) de un avión. Registra cada movimiento sin parar. Después de un "accidente" (algo se rompió), abres la caja y ves exactamente: a las 14:23 Claude cambió config.py, a las 14:25 corrió un comando de bash. Sin la caja, solo puedes adivinar.

Hook 3: post-tool-use-audit-log.sh

Tarea: llevar un registro completo de lo que cambió Claude: qué archivos, a qué hora, con qué herramienta.

El problema que resuelve: después de una sesión no queda claro qué cambió Claude exactamente. El registro te deja rastrear cada cambio y revertirlo si hace falta.

bash
#!/bin/bash
# post-tool-use-audit-log.sh
# Registro de auditoría de todos los cambios de archivos

LOG_FILE="$HOME/.claude/audit-log.txt"
mkdir -p "$(dirname "$LOG_FILE")"

# Leemos los datos de Claude Code por stdin
INPUT=$(cat)

# Extraemos la información de la acción
# tool_name está en el nivel superior; lo demás, dentro de tool_input
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
PROJECT=$(basename "$(pwd)")

# Registramos solo las acciones con archivos
if [[ -n "$FILE_PATH" ]]; then
  echo "[$TIMESTAMP] PROJECT=$PROJECT TOOL=$TOOL_NAME FILE=$FILE_PATH" >> "$LOG_FILE"
fi

# También registramos los comandos de bash (el campo command dentro de tool_input)
BASH_CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [[ -n "$BASH_CMD" ]]; then
  # Mostramos los primeros 100 caracteres del comando
  SHORT_CMD="${BASH_CMD:0:100}"
  echo "[$TIMESTAMP] PROJECT=$PROJECT BASH: $SHORT_CMD" >> "$LOG_FILE"
fi

exit 0  # Los hooks PostToolUse no bloquean: siempre exit 0

settings.json completo con los tres hooks + aviso

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/pre-tool-use-no-secrets.sh",
            "timeout": 30
          }
        ]
      },
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/pre-tool-use-budget-check.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit|Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/post-tool-use-audit-log.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude terminó la tarea\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Lista de verificación del formato (revísala en tu archivo):

  • Nombres de eventos en CamelCase: PreToolUse, PostToolUse, Stop (no snake_case)
  • Tipo de manejador: "type": "command" (no "type": "bash")
  • Cada evento → un arreglo → un objeto con matcher + hooks → un arreglo de manejadores
  • matcher filtra las herramientas: "Write|Edit", "Bash", o vacío para todas

Cómo leer el registro de auditoría

Código
[2026-10-04 14:23:01] PROJECT=acme-realty TOOL=Write FILE=/project/index.md
[2026-10-04 14:23:04] PROJECT=acme-realty TOOL=Edit FILE=/project/CLAUDE.md
[2026-10-04 14:23:09] PROJECT=acme-realty BASH: mkdir -p .claude/skills
[2026-10-04 14:25:33] PROJECT=my-platform TOOL=Write FILE=/strategy/plan.md

Ves: la hora, el proyecto, la herramienta, el archivo. Si algo se rompió, sabes exactamente qué tocó Claude y cuándo.

bash
# Ver el registro de hoy
tail -50 ~/.claude/audit-log.txt

# Encontrar todos los cambios de un archivo concreto
grep "CLAUDE.md" ~/.claude/audit-log.txt

# Encontrar todas las acciones en un proyecto concreto
grep "PROJECT=acme-realty" ~/.claude/audit-log.txt

🎨 Imagínalo así: proteger el archivo .env es como el sello rojo en el tablero eléctrico. "No tocar sin permiso del electricista." Claude ve el sello y se detiene. Sin el sello, podría confundir cables por accidente y todo el equipo se apaga.

De la práctica: un caso real con el archivo .env

De la transcripción de una clase: "Perfecto, no queremos que Claude toque el documento .env, porque si lo cambia, se rompen todas las automatizaciones: todas dependen de esas contraseñas."

Una variante del hook para proteger un archivo concreto:

bash
#!/bin/bash
# Protege el archivo .env de cualquier cambio de Claude

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Bloqueamos cualquier cambio a .env
if [[ "$FILE_PATH" == *".env"* ]] && [[ "$TOOL_NAME" == "Write" || "$TOOL_NAME" == "Edit" ]]; then
  echo "BLOQUEADO: el archivo .env está protegido contra cambios" >&2
  echo "El archivo contiene secretos. Edítalo a mano." >&2
  exit 2  # Exit code 2 = bloquear
fi

exit 0

Con este hook, Claude Code te responderá literalmente: "No puedo hacerlo: un hook me bloquea el acceso a este archivo."

Más sencillo todavía: puedes usar el filtro "if" en settings.json en lugar de revisarlo en el script:

json
{
  "matcher": "Write|Edit",
  "hooks": [
    {
      "type": "command",
      "if": "Write(*.env)",
      "command": "echo 'BLOQUEADO: .env está protegido' >&2 && exit 2"
    },
    {
      "type": "command",
      "if": "Edit(*.env)",
      "command": "echo 'BLOQUEADO: .env está protegido' >&2 && exit 2"
    }
  ]
}

Aquí "if" funciona como un filtro extra por argumentos: la forma Herramienta(patrón) revisa una sola herramienta, por eso hay dos manejadores para Write y Edit. El hook se dispara solo para los archivos .env.


Extra: webhook HTTP, un hook sin script de bash

No tienes que hacerlo todo con bash. Si tienes un servidor (o un servicio con API de bots, como Slack o Telegram), puedes mandar los datos por HTTP.

Ejemplo: aviso en un chat cuando cambian archivos

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "http",
            "url": "http://localhost:3000/hooks/file-changed",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Claude Code enviará una solicitud POST con los datos JSON del archivo. Tu servidor recibirá:

json
{
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": { "file_path": "/project/index.md", "content": "..." },
  "tool_response": "File written successfully",
  "cwd": "/Users/me/project"
}

El servidor puede reenviarlo a Slack, Discord o Telegram, guardarlo en una base de datos, lo que quieras.

🎨 Imagínalo así: un hook de tipo command es un guardia en su caseta. Un hook HTTP es un guardia que llama a la oficina central. La oficina decide qué hacer. Sirve cuando la lógica de revisión es compleja o cuando necesitas conectar Claude Code con sistemas externos.

Cuándo usar HTTP en lugar de command:

  • Avisos a un servicio externo (Slack, Discord, Telegram)
  • Auditoría centralizada de varias máquinas
  • Cuando la lógica de revisión vive en un servidor (un microservicio de validación)

Probar los hooks: lista de verificación

Después de crear cada hook, compruébalo:

Para el hook no-secrets:

Escribe esto en el chat
Crea el archivo test.py con el contenido: token = 'sk-proj-realkey123456789012345'

Lo esperado: Claude queda bloqueado y ves el mensaje del hook.

Para el hook audit-log:

Escribe esto en el chat
Crea el archivo test-audit.md con el texto "Prueba de auditoría"

Luego: tail -5 ~/.claude/audit-log.txt; debe aparecer un registro nuevo.

Para el hook de aviso al terminar (Stop):

Escribe esto en el chat
¿Qué es Claude Code? (una pregunta corta)

Lo esperado: después de la respuesta aparece una notificación de macOS.


Práctica

Tarea: poner a funcionar los tres hooks

  1. Crea la carpeta ~/.claude/hooks/
  2. Crea los tres scripts de bash con el contenido de la lección
  3. Dales permiso de ejecución: chmod +x ~/.claude/hooks/*.sh
  4. Crea o actualiza .claude/settings.json: agrega los tres hooks según la plantilla de la lección
  5. Prueba cada hook (la lista de verificación de arriba)
  6. Mira cómo se ve el registro de auditoría después de algunas operaciones

Meta: tres hooks funcionando, entender la lógica del exit code, un primer registro de auditoría con entradas reales


Herramientas y recursos

  • jq: leer JSON en bash (brew install jq en Mac)
  • chmod +x: permisos de ejecución para el script
  • osascript: notificaciones nativas de macOS (viene incluido en macOS)
  • tail -f ~/.claude/audit-log.txt: ver el registro en tiempo real
  • /hooks: el comando para ver los hooks activos desde la terminal de Claude Code

Conclusiones clave

exit 0 = permitir, exit 2 = bloquear. ¡No 1, sino justo 2! Exit 1 es solo un error del script; Claude sigue trabajando.

Al bloquear, el mensaje se escribe en stderr (>&2), no en stdout. El stderr se le pasa a Claude como el motivo del bloqueo.

matcher filtra por herramienta: "Write|Edit", solo operaciones con archivos. Sin matcher, el hook se dispara con todo.

Los hooks PostToolUse siempre hacen exit 0: registran, no bloquean. No hace falta detener a Claude después de la acción.

Los datos de Claude Code llegan en formato JSON por stdin. La ruta del archivo está en tool_input.file_path, no solo en file_path.

Tres hooks cubren las tres tareas principales: seguridad (secretos), economía (presupuesto), auditoría (quién tocó qué). Más un webhook HTTP para avisos externos.

El formato de settings.json: nombres de eventos en CamelCase (PreToolUse), tipo de manejador "command" (no "bash"), tres niveles de anidación.


Qué sigue

→ Subagentes: especialización y contexto

La marca se guarda solo en este navegador y no se envía a ningún sitio. Mi progreso