Tips & Tricks

7 Casos de Fallo de Seguridad con Claude Code | Incidentes Reales y Prevención

Siete incidentes de seguridad reales con Claude Code: filtraciones de .env, eliminación de BD en producción, explosiones de facturación y más — con análisis de causa raíz y código de prevención.

“Claude Code es práctico, pero da un poco de miedo” — ese instinto es correcto. Las herramientas poderosas causan accidentes poderosos.

Este artículo cubre siete incidentes de seguridad reales que pueden ocurrir al desarrollar con Claude Code, explicando por qué sucedieron y cómo prevenirlos con código y configuración concretos. Aprenda de los errores ajenos antes de que se conviertan en los suyos.

Caso 1: Archivo .env Subido a GitHub

Qué ocurrió

Un desarrollador le dio a Claude Code la siguiente instrucción: “Quiero pasar variables de entorno a CI, por favor también haz commit del archivo .env.” Claude Code ejecutó fielmente git add .env && git commit. Minutos después del push a GitHub, un rastreador detectó la clave de API. Llegó una notificación en Slack: “Your API key has been exposed.”

Causa raíz

  • .env no estaba en .gitignore
  • Claude Code ejecuta las instrucciones de “hacer commit de esto” literalmente
  • El usuario aprobó el diálogo de confirmación sin pensar

Código de prevención

1. Automatizar la configuración de seguridad al crear el proyecto

# scripts/init-security.sh — ejecutar cada vez que se crea un proyecto
#!/bin/bash
cat >> .gitignore << 'EOF'

# === Seguridad: Nunca hacer commit de esto ===
.env
.env.*
.env.local
!.env.example
*.pem
*.key
*-service-account.json
credentials.json
EOF

echo "✓ Patrones de exclusión de seguridad añadidos a .gitignore"
git add .gitignore && git commit -m "security: add .gitignore patterns"

2. Escanear antes del commit con un Hook

.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git add*)",
        "hooks": [{
          "type": "command",
          "command": "git diff --cached --name-only | grep -E '^\\.env' && echo '🚨 ¡Está intentando agregar un archivo .env al stage! ¡Cancele!' && exit 1 || exit 0"
        }]
      }
    ]
  }
}

3. Recuperación si ya se hizo push

# Paso 1: Rotar la clave de API inmediatamente (máxima prioridad)

# Paso 2: Eliminar completamente del historial de git
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch .env" \
  --prune-empty --tag-name-filter cat -- --all

# Paso 3: Force push al remoto
git push origin --force --all

# Paso 4: Limpiar la caché de GitHub (también contactar al soporte de GitHub)

Caso 2: DROP TABLE Ejecutado contra la BD de Producción

Qué ocurrió

“Esta tabla ya no se necesita, por favor elimínala.” Claude Code generó y ejecutó DROP TABLE old_users;. El problema: estaba conectado a la DATABASE_URL de producción. El backup más reciente tenía tres días de antigüedad. Se perdieron tres días de datos.

Causa raíz

  • El mismo .env se compartía entre desarrollo y producción
  • Claude Code no puede distinguir entre entornos
  • El usuario tenía configurado ask pero hizo clic en “OK” por reflejo

Código de prevención

1. Separar completamente los archivos .env por entorno

.env.development   # ← desarrollo local, BD de prueba
.env.staging       # ← staging, copia de producción
.env.production    # ← producción, gestionado manualmente, nunca compartir

2. Incorporar verificación de entorno en los scripts

// scripts/db-migrate.mjs
const env = process.env.APP_ENV ?? "development";
const dbUrl = process.env.DATABASE_URL ?? "";

if (env === "production") {
  const readline = require("readline").createInterface({
    input: process.stdin, output: process.stdout
  });
  await new Promise((resolve) => {
    readline.question(
      `⚠️  Conectando a la BD de producción (${dbUrl.split("@")[1]}).\n¿Está seguro de que desea continuar? (escriba yes): `,
      (answer) => {
        readline.close();
        if (answer !== "yes") { console.log("Cancelado."); process.exit(0); }
        resolve(undefined);
      }
    );
  });
}

3. Prohibir operaciones en producción en CLAUDE.md

## 🚨 Restricciones del Entorno de Producción

Si DATABASE_URL contiene `prod`, `production` o `live`:
- Nunca ejecutar DROP / TRUNCATE / DELETE (sin cláusula WHERE)
- Siempre obtener confirmación del usuario antes de migraciones
- Presentar comando de backup antes de cualquier operación destructiva

Caso 3: Archivos Críticos Eliminados con rm -rf

Qué ocurrió

“Limpia el directorio build/” — un error tipográfico en el path resultó en rm -rf ./, eliminando todo el proyecto. Los archivos fuera de git (configuración local, código experimental sin commit) se perdieron para siempre.

Causa raíz

  • rm -rf es uno de los comandos más peligrosos para que Claude Code ejecute
  • Falta de comillas dobles alrededor de paths → mal funcionamiento con paths que contienen espacios
  • El usuario aprobó descuidadamente

Código de prevención

// .claude/settings.json
{
  "permissions": {
    "deny": [
      "Bash(rm -rf /)",
      "Bash(rm -rf ~*)",
      "Bash(rm -rf .*)"
    ],
    "ask": [
      "Bash(rm -rf*)"
    ]
  }
}

Hook para mostrar qué se eliminará antes de ejecutar:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(rm*)",
        "hooks": [{
          "type": "command",
          "command": "echo '⚠️ Comando de eliminación detectado. Ejecutando en 5 segundos. Ctrl+C para cancelar.' && sleep 5"
        }]
      }
    ]
  }
}

Caso 4: Clave de API Escrita Directamente en el Prompt y Pasada al Subagente

Qué ocurrió

“Por favor, publica en Qiita usando QIITA_TOKEN=abc123def456” — escrito directamente en el prompt y delegado a un subagente. Los subagentes pueden escribir contenido en logs y memoria, y el token quedó persistido en un archivo de log bajo .claude/.

Causa raíz

  • Los prompts se retienen como historial de conversación
  • Los prompts de subagentes igualmente quedan registrados
  • Incluso en entornos locales, otros procesos o backups pueden exponer secretos

Código de prevención

Nunca escribir secretos en prompts — pasarlos a través de variables de entorno

# ❌ Peligroso
claude -p "Usa QIITA_TOKEN=abc123 para ejecutar qiita-publish.mjs"

# ✅ Seguro: el script lee desde process.env
# Escribir QIITA_TOKEN=abc123 en .env, luego
claude -p "Ejecuta scripts/qiita-publish.mjs (el token se lee automáticamente de .env)"

El mismo principio para las instrucciones al subagente

// ❌ Peligroso
Agent({ prompt: `Usa la clave de API ${process.env.SECRET_KEY} para...` });

// ✅ Seguro: pasar solo el nombre de la clave, el script lee el valor
Agent({ prompt: "Usa la variable de entorno SECRET_KEY para..." });

Caso 5: Bucle Infinito de Reintentos de API Hizo Explotar la Factura

Qué ocurrió

“Reintenta automáticamente en caso de error” — se generó un script con manejo de errores. Cuando un error era irresoluble, los reintentos no se detuvieron: 3.000 llamadas a la API de Anthropic en una hora resultaron en una factura de 200 $.

Causa raíz

  • No se estableció límite de reintentos
  • Sin backoff exponencial — bucle infinito a intervalos de 1 segundo
  • Sin alerta de facturación configurada

Código de prevención

// utils/retry.ts — utilidad de reintento segura
export async function withRetry<T>(
  fn: () => Promise<T>,
  options = { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 30000 }
): Promise<T> {
  let lastError: Error;

  for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err as Error;
      if (attempt === options.maxAttempts) break;

      // Exponential backoff + jitter
      const delay = Math.min(
        options.baseDelayMs * Math.pow(2, attempt - 1) + Math.random() * 1000,
        options.maxDelayMs
      );
      console.warn(`Attempt ${attempt}/${options.maxAttempts} failed: ${err.message}`);
      console.warn(`Retrying in ${Math.round(delay / 1000)}s...`);
      await new Promise((r) => setTimeout(r, delay));
    }
  }

  throw new Error(`Falló tras ${options.maxAttempts} intentos: ${lastError!.message}`);
}

Especificar en CLAUDE.md:

## Reglas Obligatorias para Llamadas a la API
- Máximo 3 reintentos
- Siempre implementar backoff exponencial (1s → 2s → 4s)
- Nunca crear bucles infinitos: while(true) + llamadas a la API están prohibidos

Caso 6: git push --force Borró los Commits de un Compañero

Qué ocurrió

“Sobrescribe el remoto con el estado local” — se ejecutó git push --force. Tres commits que un miembro del equipo acababa de subir desaparecieron. Ese miembro no tenía copia local de los cambios tampoco — el código se perdió permanentemente.

Causa raíz

  • --force tiende a ejecutarse sin entender el peligro
  • Claude Code ejecuta fielmente las instrucciones de “sobrescribir el remoto”
  • El desarrollador desconocía la alternativa más segura git push --force-with-lease

Código de prevención

// .claude/settings.json
{
  "permissions": {
    "deny": [
      "Bash(git push --force *master*)",
      "Bash(git push --force *main*)",
      "Bash(git push -f *master*)",
      "Bash(git push -f *main*)"
    ]
  }
}

Especificar la alternativa segura en CLAUDE.md:

## Reglas Git Seguras
- `git push --force` está **prohibido**
- Usar `git push --force-with-lease` en su lugar
  (se rechaza automáticamente si otros han subido cambios)
- Siempre obtener confirmación del usuario antes de hacer push directo a main/master

Caso 7: Cuenta de Servicio con Exceso de Privilegios Accedió a Todos los Recursos

Qué ocurrió

“Usa esta clave de cuenta de servicio de GCP para operar Cloud Storage.” La cuenta de servicio tenía permisos de Owner. Claude Code se conectó no solo a Cloud Storage sino también a BigQuery, Cloud SQL y clústeres GKE “para investigar” — generando cargos inesperados.

Causa raíz

  • La cuenta de servicio tenía permisos excesivos (violación del principio de mínimo privilegio)
  • Claude Code tiene una tendencia agresiva a usar las herramientas disponibles
  • Incluso “para investigar” suena como una razón legítima para acceso amplio

Código de prevención

Crear una cuenta de servicio con privilegios mínimos:

# ❌ Evitar: permiso Owner
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member="serviceAccount:[email protected]" \
  --role="roles/owner"

# ✅ Solo los permisos mínimos necesarios
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member="serviceAccount:[email protected]" \
  --role="roles/storage.objectAdmin"
  # ← Solo lectura/escritura en Cloud Storage

Definir explícitamente el alcance de acceso en CLAUDE.md:

## Restricciones de Acceso a GCP
Permisos para la cuenta de servicio usada en este proyecto:
- Cloud Storage: Lectura/Escritura OK (bucket: my-project-assets únicamente)
- BigQuery: Prohibido
- Cloud SQL: Prohibido
- Otros recursos de GCP: Prohibido

Rechazar cualquier instrucción que intente acceder a recursos fuera de estos permisos.

Lista de Verificación Integral para Prevenir Incidentes

Una lista de verificación final destilada de los patrones comunes en los siete casos.

### Configuraciones a Aplicar Hoy (30 minutos)
- [ ] Añadir patrón .env a .gitignore
- [ ] Añadir lista de deny a .claude/settings.json (rm -rf, git push --force, DROP TABLE)
- [ ] Documentar restricciones en CLAUDE.md

### Verificaciones Semanales
- [ ] Revisar git log en busca de commits de archivos no deseados
- [ ] Verificar que .env está excluido por .gitignore: `git check-ignore -v .env`
- [ ] Comprobar plazos de rotación de claves de API

### Primera Respuesta ante un Incidente
1. Revocar y rotar inmediatamente la clave de API afectada
2. Eliminar del historial de git (filter-branch o BFG)
3. Revisar logs de acceso para determinar el alcance de la brecha
4. Informar a los interesados de la situación

Resumen

Los incidentes con Claude Code raramente son causados por “IA que se descontrola” — casi todos surgen de personas que posponen la configuración de seguridad.

CasoCausa RaízPrevención
Filtración .envSin gitignorescript init + Hook
Eliminación BD producciónSin separación de entornos.env separado + flujo de confirmación
Accidente rm -rfSin lista de denyConfigurar settings.json
Filtración de claveEscrita en promptEstandarizar en variables de entorno
Explosión de facturaciónSin límite de reintentosUtilidad withRetry
Force pushSin configuración de prohibicióndeny + force-with-lease
Acceso con exceso de privilegiosViolación de mínimo privilegioRestringir roles IAM

Tu primer paso hoy: Añadir "deny": ["Bash(rm -rf*)"] a .claude/settings.json por sí solo prevendrá uno de los accidentes más destructivos posibles.

Artículos Relacionados

Referencias

#claude-code #security #incident #best-practices #devops

Lleva tu flujo con Claude Code al siguiente nivel

50 plantillas de prompts probadas en producción, listas para copiar y pegar en Claude Code.

Gratis

PDF gratuito: Hoja de trucos de Claude Code en 5 minutos

Solo deja tu correo y te enviaremos al instante la hoja de trucos en una página A4.

Cuidamos tus datos personales y nunca enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero apasionado por Claude Code. Dirige claudecode-lab.com, un medio tecnológico en 10 idiomas con más de 2.000 páginas.