Founders$49 única vez → 2 años de Pro ($98 de valor)Ser Founder →
ClauLock

Blog

Pegué mi API key de OpenAI en una conversación con Claude. Esto fue lo que encontré.

Recorrido en primera persona de un solo paste: dónde quedó el plaintext, por qué borrar el chat no lo deshizo, y el momento exacto en que dejé de confiar en la disciplina humana para mantener keys fuera de las ventanas de contexto LLM.

Por Jesús E. Viera · · 8 min de lectura

La historia que vas a leer es real y vergonzosa. La comparto porque el mismo molde de error le pasa a casi cualquier dev que empareja un agente IA con credenciales que le importan, y la mayoría no se entera.

Un martes en la tarde de marzo, estaba prototipando una herramienta interna pequeña para hacer fine-tuning de un modelo privado de OpenAI sobre un dataset de respuestas de soporte. Tenía Claude Code abierto, un repo nuevo, y un solo objetivo: arrancar el fine-tune antes del almuerzo.

Claude necesitaba una API key de OpenAI. Hice lo que cualquier dev cuidadoso ha hecho al menos una vez: tipeé "Aquí está la key:" en el chat, pegué mi sk-… real, y le pedí a Claude que la usara. El fine-tune arrancó. Llegó el almuerzo. Al final del día el modelo estaba 60% entrenado y yo había pasado al siguiente problema.

Dos días después, revisando mi dashboard de OpenAI por una razón no relacionada, noté que esa key tenía uso desde una IP que no reconocía. No era de Claude. No era de mi laptop. Era de algún lugar en el que nunca había estado.

Lo que sigue es lo que reconstruí en las siguientes tres horas. Nada fue malicioso — no hubo breach, ni leak desde Anthropic, ni compromiso de un tercero. Solo estaba mi propio paste, replicado en lugares para los que yo no había planeado que fuera.

Capa 1: el transcript del chat

La obvia. El plaintext quedó en mi historial de Claude Code, carácter por carácter, rodeado del prompt que escribí y de las respuestas de Claude. Había usado el botón "borrar conversación" dos días antes al cerrar. Ese botón la sacó de la lista en el UI. No la sacó del local storage, ni del cache de recovery del IDE, ni del archivo JSONL del transcript que Claude Code guarda en ~/.config/claude-code/projects/ para soportar resume.

Lección uno, la que más me sorprendió: "borrar el chat" no es lo mismo que "borrar los bytes que contienen el chat". El cache es local pero yo tengo laptops que sincronizan, backups de Time Machine, y a veces alguien me pide la pantalla prestada. La superficie de "lugares donde ese string todavía existe" ya era mucho más grande de lo que la ventana del chat sugería.

Capa 2: la ventana de contexto del modelo

Claude es stateless entre conversaciones nuevas, pero dentro de la conversación donde pegué la key, cada prompt subsiguiente que mandé incluyó esa key como parte del contexto vivo. Hubo catorce mensajes en ese hilo. La key estaba en todos después del paste.

Por cada mensaje, la API de Anthropic recibió el historial entero hasta ese punto. Así está diseñado — el modelo no tiene memoria; el cliente manda la historia. Entonces la key viajó a la API catorce veces más después de que la pegué, aunque yo solo pretendía "mandarla" una.

Las políticas de Anthropic son claras en que no entrenan con inputs de la API por defecto y que retienen solo por ventanas cortas de debugging. No los estoy acusando de nada. El punto es otro: mi key salió de mi máquina catorce veces extra, y no había en el UI ninguna pista que sugiriera eso.

Capa 3: el corpus de autocomplete de mi IDE

La que jamás hubiera adivinado. Después de pegar la key, cada prefijo sk-... que tipeaba en el mismo proyecto autocompletaba a mi key real. Mi IDE la había cacheado. También mi history del shell, porque también la había echo-eado una vez debugueando. También un DM en Slack donde le explicaba la situación a un colega usando "la key empieza con sk-pr-" — que, sin ser el string completo, era suficiente para correlacionar con el corpus del IDE si alguien rastrillara los dos.

Ninguna de estas superficies por sí sola es un "breach". Cada una es una cosa perfectamente normal que hacen herramientas perfectamente normales. Juntas, hicieron que la key fuera recuperable desde al menos cuatro lugares para los que no había planeado.

¿Y la IP sospechosa?

Para cerrar el ciclo del incidente real: la IP desconocida era un runner de CI de un job personal de GitHub Actions que había dejado de usar dos meses antes. El job tenía mi key de OpenAI cacheada como repository secret. Después de mi paste, le pedí a Claude que pusheara un refactor a ese mismo repo. En algún parte del diff, un workflow viejo resucitó. El runner tomó el secret cacheado, corrió un smoke test nocturno, y silenciosamente quemó $11 de crédito de API antes de que yo notara.

No hubo malicia en ningún eslabón de esa cadena. Solo estábamos yo, una herramienta, y el comportamiento totalmente razonable de cada componente intermedio. La key salió porque la traté como un string, y los strings fluyen.

Qué hice al respecto

Roté la key. Audité cada lugar que pude encontrar que la referenciara. Anoté cada capa de arriba para acordarme la próxima. Después arranqué a construir ClauLock, porque lo que de verdad quería era que el paste fuera imposible — que el instante en que una credencial cruzara al campo de visión del modelo requiriera un gesto distinto, uno que yo sintiera.

El mecanismo es el más simple que se me ocurrió: cuando Claude necesita un secreto, no me puede preguntar en el chat. Tiene que pedir el secreto por nombre via un canal separado. ClauLock intercepta el pedido, abre un popup del OS fuera del contexto del modelo, y me pregunta ahí. Pego una vez. El plaintext queda cifrado en un vault local. Desde entonces, cada referencia es un placeholder como {{OPENAI_KEY}}. El modelo puede usar la key — no la puede ver.

El diseño completo está en cómo dejar de filtrar API keys a tu agente IA. El threat model y la comparación contra herramientas existentes está en Vault vs ClauLock. Si quieres instalarlo ahora y probar, el picker en la home detecta tu OS y te da el one-liner correcto.

No voy a pretender que esta es la única vía. Apretar tu disciplina de paste lo suficientemente fuerte probablemente también funciona. Te puedo decir que yo lo intenté por años y me falló un martes en marzo. La disciplina es un mal límite de seguridad.

Si te llevas una sola cosa de este post

"Borrar chat" es un gesto de UI. No es un gesto de seguridad. Los bytes que pegaste siguen en algún lado. Probablemente en más lugares de los que esperas. La única ruta que cierra esa superficie es nunca poner los bytes en el chat para empezar.

Lo que uses para llegar ahí — ClauLock, otro secrets manager, un hook casero — haz que el paste requiera una acción difícil de hacer por accidente. Ese único cambio vale más que todos los scripts retroactivos de cleanup juntos.