CasoAvanzado
Tenía las claves de todos mis desarrollos en un bloc de notas
Siempre lo mismo: una nueva plataforma, una nueva contraseña, una API key que termina en un bloc de notas. Lo peor no era perderlas — lo peor era que estaban a la vista. Esto es cómo se resolvió sin guardar secretos en la base de datos.
Siempre me pasaba lo mismo. Empezaba un proyecto, me daba de alta en una plataforma nueva, generaba la contraseña, generaba una API key… y esa key tenía que ir a parar a algún lado. Un bloc de notas. Un archivo suelto. Un papel. A veces las tres cosas, con valores distintos, y ninguna me decía cuál era la buena.
Lo peor no era perderlas. Lo peor era que estaban a la vista. Cualquiera que abriera ese archivo tenía acceso a todo, sin ningún tipo de inconveniente. Y yo no tenía forma de saber si la clave que estaba en el .env de un proyecto seguía siendo la misma que había anotado seis meses antes.
Y cuando quería abrir la plataforma donde guardaba la API, no recordaba con qué mail lo había generado o cuál era el proyecto.
Por eso construí Variables.
La decisión de fondo: la app no guarda secretos
Suena raro para una app de claves, pero es exactamente eso: el valor de un secreto no entra nunca a la base de datos de la app. Ni siquiera hasheado.
Los valores viven en el Keychain de macOS, o el de Windows —el llavero del sistema, cifrado por el sistema operativo— y la app guarda todo lo demás: a qué plataforma pertenece cada clave, de qué cuenta cuelga, en qué proyecto se usa, con qué nombre de variable, cuándo vence, cuánto sale.
De ahí salen dos reglas que no se negocian:
-
El valor no cruza del servidor al navegador.
No se loguea, no se serializa en una respuesta, no llega al HTML. No hay «ojito» ni botón de revelar. Ni cuando lo recupero.
-
«Recuperar el valor» se resuelve en el servidor.
El proceso lee el llavero y escribe directo en el portapapeles. Como la app corre sólo en mi máquina, ese es mi portapapeles.
Cómo funciona, en la práctica
Los cuatro carriles son las tres pestañas del menú más el llavero de macOS. Tocá una estación y se enciende con lo que la rodea; «Recorrer» las camina de a una.
Las tres pestañas y el llavero
- Los .env.local. Uno por repo, y la única fuente: la app los lee y nunca los escribe.
- La bandeja de candidatos. Una carpeta cuenta como proyecto solo si tiene .git o CLAUDE.md, y aparecen solo los que faltan cargar. Nada se da de alta solo: la app propone y se confirma a mano.
- La pantalla del proyecto. Sus variables, una por una, y el botón Escanear .env.
- Escanear .env. Lee el archivo y el llavero y los compara en memoria. El valor se descarta ahí mismo.
- El Keychain de macOS. El único lugar donde vive el valor, cifrado por el sistema. La base de la app no lo guarda, ni siquiera hasheado.
- Un veredicto por variable. ok, falta_en_keychain, valor_distinto o no_catalogada. Es lo único que queda guardado.
- Fuera del llavero. Lo que no es credencial, o lo que la plataforma rota sola, se marca aparte y deja de contar como pendiente.
- Catalogar la variable. Una no_catalogada se cataloga ahí mismo, eligiendo plataforma y cuenta. Es el único camino que crea claves, y el valor va al llavero.
- La ficha de la plataforma. Junta las cuentas, cómo se entra a cada una, sus claves y los vencimientos.
- Al portapapeles, sin verlo. Copiar lo resuelve el servidor: el valor no pasa por el navegador ni aparece en la pantalla.
La app tiene tres pestañas, y son tres pasos de la misma historia.
① Importar — detecta los proyectos. Busca los .env.local bajo mi carpeta de trabajo y arma una bandeja de candidatos. Una carpeta cuenta como proyecto sólo si tiene .git o CLAUDE.md, así que no se llena de basura de node_modules. Y en la bandeja aparecen sólo los que faltan cargar: nada se da de alta solo, la app propone y yo confirmo.
② Proyectos — el .env, variable por variable. Adentro de cada proyecto está el botón Escanear .env, que es el corazón del asunto: lee el archivo, lee el llavero, compara, y devuelve un veredicto por variable.
ok— el archivo y el llavero coinciden.falta_en_keychain— hay que pegar el valor una vez.valor_distinto— las dos copias se separaron. Ese aviso es exactamente lo que nunca tuve.no_catalogada— la variable todavía no tiene clave.
El valor se compara en memoria y se descarta: lo único que queda guardado es el veredicto.
Y hay una salida para lo que no hay que guardar: VAULT_PATH, o un token que la plataforma rota sola. Se marcan como «no va al llavero», dejan de contar como pendientes y aparecen listadas aparte. Sin eso, el contador de cosas por revisar no llegaba nunca a cero — y un contador así se deja de mirar.
③ Plataformas — el catálogo. Cuando una variable sale no_catalogada, la catalogo ahí mismo: elijo la plataforma y de qué cuenta cuelga, sin salir de la pantalla. Eso crea (o reusa) la plataforma, la cuenta y la clave — es el único camino que crea claves. La clave queda con un nombre de llavero que genera la app (supabase.santhru-crm + service-role-key), en kebab-case e inmutable: nunca tipeo ese nombre. Después, la ficha de la plataforma junta las cuentas, cómo se entra a cada una, sus claves y los vencimientos.
Armar y actualizar son el mismo motor. Guardar un valor o borrar una clave vuelven a disparar el escaneo solos, así que la fila se actualiza sin que yo apriete nada. Y cuando quiero la clave, el botón copiar la manda al portapapeles desde el servidor: el valor no pasa por el navegador ni aparece en la pantalla.
La app lee los .env.local. Nunca los escribe. Escribir cerraría el círculo de la rotación, pero el costo de un bug sería pisar el archivo de un proyecto que funciona. Detectar y avisar da el 80% del valor con 0% de ese riesgo.
Cómo lo pensamos (antes de escribir una línea de código)
Lo primero que se escribió no fue código: fue un spec de diseño con 8 decisiones, cada una con su motivo al lado. El motivo es lo que permite revisar la decisión después, cuando ya nadie se acuerda por qué era así.
Algunas decisiones fueron sobre lo que no íbamos a hacer:
- No mostrar nunca el valor en pantalla. Copiar al portapapeles cubre todos los casos reales; mostrarlo sólo agrega superficie de exposición: capturas, pantalla compartida, alguien atrás.
- No escribir el Keychain con el comando
security. Con el CLI, el secreto viaja como argumento y queda visible para otros procesos. Se usa un binding nativo: nunca sale de la memoria del proceso. - Nada de IA en la v1. Quedó para la v2, y con eso la app no depende de la red para funcionar.
- Nada de nube, ni deploy, ni multiusuario. Las dos fuentes de datos —mi filesystem y mi llavero— no existen fuera de esta máquina.
Cómo lo construimos
El spec se convirtió en un plan de 16 tareas autocontenidas: cada una escrita para que un agente la pueda tomar sin haber estado en la conversación original.
Y de ahí, el ciclo que se repitió 16 veces:
- Un agente por tarea, arrancando siempre por el test que todavía falla.
- Otro agente revisa y busca el error. Si encuentra algo, se arregla antes de seguir — en el historial hay commits que dicen literalmente «Arreglar hallazgos de revisión de Task 12».
- Los docs se actualizan en el mismo paso que el código, no después.
gitleakscorre en cada commit, porque el único leak real que tuve en mi vida no entró por un.env: entró por un token pegado en un.mdde documentación.
Resultado: 115 commits, 160 tests verdes, TypeScript y build limpios.
Cómo está funcionando ahora
No la levanto a mano. Corre como servicio desde que prendo la Mac y la abro desde el Dock como una ventana propia, con su ícono, sin barra de direcciones. Parece una app nativa.
Y lo que más uso, que no estaba en el plan original: buscar con ⌘K y que me diga en qué proyecto se usa una clave y cómo se entra a esa plataforma. Resulta que ese era el problema real. Las claves ya las tenía; lo que no tenía era el mapa.
Lo que me llevo
-
Escribir el porqué antes que el código.
Un spec con los motivos al lado convierte cada discusión posterior en una consulta de dos minutos.
-
La regla que no se negocia tiene que ser un test.
Si es sólo un párrafo en un documento, se rompe sola en tres semanas.
-
Decidir qué NO va es tan importante como decidir qué va.
La mitad de las buenas decisiones de esta app son cosas que se descartaron con el motivo escrito.
-
Una tarea por vez, revisada por alguien que no la escribió.
Aunque ese alguien sea otro agente.
- producto-propio
- seguridad
- arquitectura
- datos