En algún repositorio público de GitHub hay una contraseña de base de datos de producción metida en un archivo JSON. Lleva ahí 217 días. Nadie la escribió a propósito. Llegó porque un desarrollador le pidió a un agente de código con IA que revisara por qué fallaba el login de staging, el agente propuso el comando obvio y el desarrollador hizo clic en "Sí, y no volver a preguntar".
Ese clic es toda la historia.
El 6 de noviembre, la firma de seguridad Apiiro publicó una investigación que demuestra que este patrón exacto ha dejado 2.539 secretos reales expuestos en 1.994 repositorios públicos de GitHub. Entre ellos: la clave de administrador en la nube de un proveedor de bases de datos, sin caducidad; una contraseña de Postgres de producción en Azure; un token de producción de Square; y la contraseña de propietario de una base de datos de historiales médicos. Sesenta y tres organizaciones, en su mayoría empresas de software, tienen al menos una credencial viva en una lista de permisos pública. Una empresa confirmó que su clave seguía funcionando cuando los investigadores la llamaron.
Esto no es un problema del modelo. Es un problema del comportamiento del producto, y les está pasando a equipos que creían estar siendo cuidadosos.
El mecanismo son tres palabras
Claude Code pregunta antes de ejecutar un comando de shell. Esa es la barrera, y es razonable. Pero el aviso ofrece una opción: "Sí, y no volver a preguntar". Si la eliges, Claude Code escribe una regla de permiso en .claude/settings.local.json, dentro de la carpeta del proyecto. La regla es el comando, palabra por palabra.
Si el comando era PGPASSWORD=hunter2 psql -h prod-db.internal -U admin, la regla es PGPASSWORD=hunter2 psql -h prod-db.internal -U admin. El agente no decidió guardar tu contraseña. Guardó tu comando, y tu comando contenía tu contraseña.
Apiiro lo reprodujo con los modelos más recientes de Anthropic, Opus 5.5 y Fable 5.1. Es la herramienta que envuelve al modelo, no la capa de razonamiento. De dieciséis agentes de código cuya documentación compararon los investigadores, Claude Code es el único confirmado que guarda por defecto el comando completo aprobado en un archivo del proyecto.
La protección existe, pero no viaja
Aquí es donde se pone interesante, y donde casi todos los análisis se detienen un paso antes.
Claude Code sí intenta mantener ese archivo fuera de git. La primera vez que lo escribe dentro de un repositorio, lo añade a tu lista de exclusiones global de git. En tu máquina, en tu configuración de git, ese directorio queda ignorado en todos los repositorios que toques. Es ingeniería cuidadosa, de verdad.
También es ingeniería que no sobrevive al contacto con la realidad.
Las exclusiones globales viven en la configuración de un usuario, en un portátil. En el momento en que el archivo ya está commiteado, la exclusión no hace nada. No protege un clon en la máquina de un compañero, ni una carpeta que se convierte en repositorio después, ni un fork. Y Claude Code nunca añade la ruta al .gitignore del propio proyecto, así que la protección no forma parte del repositorio. Nunca viaja con el código.
Peor: en 539 de los 1.105 repositorios con un secreto confirmado, el commit que cambió el archivo estaba cofirmado por Claude. El agente, haciendo lo que hacen los agentes, commiteó el trabajo. El trabajo incluía su propia lista de permisos.
Qué dicen realmente los números
Apiiro escaneó 104.185 copias públicas de .claude/settings.local.json en 103.617 repositorios. De ellas, 1.122 archivos (un 1,1 % aproximado) contienen un secreto real: 1.379 credenciales distintas. Merece la pena detenerse en el desglose:
- 379 contraseñas de bases de datos y 122 claves service-role de Supabase, que abren una base de datos directamente
- 103 tokens de cuenta de Supabase, que controlan todos los proyectos de esa cuenta
- 43 accesos SSH a servidores públicos, varios de ellos como root vía
sshpass - 42 tokens de GitHub, GitLab y npm, 23 claves de AWS, 6 claves privadas
Más de un tercio de las credenciales expuestas abren una base de datos. No son tokens que rotas cada trimestre. Son las joyas de la corona, en un archivo con un nombre amable.
El tamaño se correlaciona con el riesgo de forma brutal: uno de cada diez archivos de más de 8 KB contiene un secreto real, frente a menos de uno de cada quinientos por debajo de 500 bytes. Cuanto más tiempo lleva un desarrollador pulsando "no volver a preguntar", más pesado se vuelve el archivo. La mediana de los archivos expuestos se commiteó por primera vez 217 días antes del hallazgo. En 118 repositorios lleva ahí más de un año.
Borrar el archivo tampoco lo arregla. Las versiones anteriores guardan otros 734 secretos en 509 repositorios que no aparecen en ninguna copia actual, normalmente porque alguien sí acabó borrando el archivo. Los forks lo empeoran: 211 forks de 74 repositorios llevan los mismos secretos que el original. Es la propiedad clásica de git que sorprende a todo el mundo la primera vez: un secreto commiteado es un secreto permanente, y un secreto permanente en un repositorio público es un secreto publicado.
Tu escáner de secretos probablemente lo pasó por alto
Esta es la parte que los equipos de seguridad necesitan escuchar. Apiiro pasó gitleaks y TruffleHog por los 104.185 archivos. De los 1.122 que contenían un secreto real, gitleaks marcó 564 y TruffleHog 643. 215 no fueron marcados por ninguno de los dos.
El punto ciego son las contraseñas dentro de comandos. Los escáneres están afinados para formatos de clave: un prefijo de proveedor, una cadena larga y aleatoria, un umbral de entropía. Una contraseña después de PGPASSWORD=, sshpass -p, mysql -p, o en una cabecera bearer dentro de una llamada curl, es solo una palabra corriente en una línea de comandos corriente. Parece shell, porque es shell.
Y el patrón no se limita a un archivo. La misma investigación encontró secretos en:
- el
.claude/settings.jsoncompartido que los equipos commitean a propósito (298 de 76.011 archivos) - transcripciones de chats de agentes exportadas de
.specstory, Aider y Cursor (120 archivos en 87 repositorios) - archivos de memoria de agentes como
MEMORY.md(9 repositorios) - notas de traspaso y despliegue (33 archivos, un tercio escritos por el propio agente)
- ajustes de autoaprobación de VS Code Copilot (9 repositorios)
El hábito que filtra es idéntico en todas partes: una persona escribe un secreto dentro de un comando y un agente registra el comando con toda la fidelidad del mundo.
Qué hacer, en orden
Primero rotar. Después limpiar. Ese orden no es una preferencia: es la única secuencia que funciona, porque en el momento en que una credencial toca un commit público pertenece a quien lo haya leído, y el historial lo recuerda.
- Busca el archivo en tu organización. Consulta GitHub por
.claude/settings.local.jsony.claude/settings.jsonen todos los repositorios, y revisa el historial de git incluso donde el archivo ya no existe. - Rota todo lo que el archivo haya contenido alguna vez — contraseñas, tokens, claves, cadenas de conexión — antes de empezar a borrar.
- Deja de rastrearlo e ignóralo.
git rm --cached .claude/settings.local.jsony añádelo al.gitignoredel propio repositorio, no solo al global. - Abre el archivo en tu propio portátil, esté commiteado o no, y elimina cada regla que contenga un secreto. La mayoría de estos archivos nunca tocaron GitHub y siguen en texto plano en el disco.
- Deja de meter secretos en los comandos. Referencia
$DB_URLdesde tu gestor de secretos para que la regla guardada registre un nombre de variable y no una credencial. - Aprueba patrones, no comandos.
Bash(npm run:*)es una regla.npm run deploy --token abc123es una fuga.
Para los equipos de seguridad, cuatro añadidos: trata .claude/, .cursor/, .gemini/, .specstory/ y los archivos de memoria de agentes como rutas que contienen secretos, igual que .env; inclúyelos en el escaneo pre-commit y de CI, historial incluido; añade .claude/settings.local.json a la plantilla de .gitignore de tu organización; y prioriza las credenciales que van acompañadas de la dirección de un host público, porque son las que dan acceso desde cualquier lugar.
La conclusión que repetimos
La lección no es que "los agentes de IA son peligrosos". El modelo de permisos de Anthropic es sensato, y la exclusión global de Claude Code demuestra que alguien pensó en este fallo concreto. La lección es que los ajustes de conveniencia se convierten en almacenamiento sin que nadie lo note. Cada "no volver a preguntar" es un registro. Cada registro tiene una ruta. Y cada ruta acaba tarde o temprano en un repositorio, un archivo comprimido, una capa de Docker o una carpeta sincronizada.
Si tu equipo lleva un año publicando con agentes de código, casi seguro que tienes un directorio .claude/ anterior a cualquier política que hayas escrito desde entonces. Ve a leerlo. No la versión del repositorio: la del portátil que tienes delante. Luego mira qué dijeron los escáneres y asume que no dijeron nada.
El comando que aprobaste una vez sigue guardando tu contraseña. Lo hará hasta que la rotes.