\
CONTEXT/academy
Lección 04 · ≈ 14 min

Detección de secrets

Un PreToolUse que bloquea escribir lo que parece una API key.

Detección de secrets

Ahora el primer muro de verdad: un hook que impide que Claude escriba una API key o un token en un fichero. A diferencia del formateo, este corre antes de la acción y la cancela si detecta algo. Es el patrón PreToolUse + exit 2.

El script

Los hooks con lógica van mejor en un fichero. Guarda esto en .claude/hooks/block-secrets.sh:

#!/bin/bash
INPUT=$(cat)
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // empty')

# Patrones de lo que parece un secreto.
if echo "$CONTENT" | grep -Eq \
  'sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z]+ PRIVATE KEY-----|ghp_[A-Za-z0-9]{36}'; then
  echo "Bloqueado: el contenido parece contener un secreto (API key / token / clave privada). Usa variables de entorno." >&2
  exit 2
fi
exit 0

Hazlo ejecutable:

chmod +x .claude/hooks/block-secrets.sh

El registro en settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-secrets.sh" }
        ]
      }
    ]
  }
}

Cómo funciona, paso a paso

  1. Claude intenta un Write o Edit.
  2. Como es PreToolUse, el hook corre antes de escribir nada.
  3. El script lee el JSON de stdin y saca el contenido: tool_input.content (en Write) o tool_input.new_string (en Edit).
  4. Un grep -E busca patrones típicos: claves OpenAI (sk-…), AWS (AKIA…), tokens de GitHub (ghp_…), bloques de clave privada PEM.
  5. Si casa, escribe el motivo en stderr y hace exit 2: la escritura se cancela y Claude recibe el mensaje, así que sabe por qué y puede corregir (p.ej. usar una env var).

El // .tool_input.new_string // empty de jq es un detalle que importa: cubre tanto Write (campo content) como Edit (campo new_string), y si no hay nada devuelve vacío en vez de romper.

Ajusta los patrones a tu realidad

Esos regex son un punto de partida. Añade los formatos de los proveedores que uses (Stripe sk_live_…, Slack xox…, etc.). Y recuerda la lección 1: esto es un invariante determinista (¿el texto casa este patrón?), justo lo que un hook hace bien — no le pidas que juzgue si "tiene pinta de secreto" con criterio, dale patrones.

Falsos positivos: un sk-… de ejemplo en un test o fixture también se bloquearía. Crea una excepción leyendo la ruta del fichero (jq -r '.tool_input.file_path') y dejando pasar las que casen *.test.* o fixtures/, o afina el patrón para no atrapar valores de muestra.

Defensa en profundidad

Esto se combina con lo del curso de permisos: un deny de Read(./.env) impide que lea secretos; este hook impide que los escriba en el código. Capas distintas, mismo objetivo.

Pruébalo

Con el hook registrado, pídele a Claude que escriba una clave de ejemplo en un fichero:

"Crea config.js con la línea const KEY = "sk-abc123def456ghi789jkl012";"

El grep casa el patrón sk-…, así que el script hace exit 2: la escritura se cancela y Claude recibe el mensaje del stderr ("Bloqueado: el contenido parece contener un secreto…"). Comprueba que el fichero no se creó y que Claude reacciona al motivo (típicamente, proponiendo una variable de entorno). Ese bloqueo observable es la verificación de que el muro funciona.

Qué llevas hasta aquí

  • Un hook PreToolUse con matcher Edit|Write intercepta antes de escribir.
  • El script saca el contenido con jq (content o new_string), busca patrones de secreto y hace exit 2 para bloquear.
  • Es un invariante por patrón; ajústalo a los proveedores que uses.

En la siguiente lección, el guard clásico: bloquear git push --force, rm -rf y compañía.

Fuente oficial: Hooks reference

¿Quieres llevar la cuenta de las lecciones que has terminado?

Entrar para guardar progreso