
enject: Oculta secretos .env de ojos entrometidos: los secretos viven en almacenes locales cifrados (por proyecto) y se inyectan directamente en las aplicaciones en tiempo de ejecución, sin tocar nunca el disco como texto plano.
Oculta los secretos de .env de los ojos de prAIng.
(Nota: este proyecto se llamaba anteriormente enveil y ha sido renombrado a enject)
Las herramientas de codificación con IA como Claude Code, Copilot, Cursor y otras pueden leer archivos en tu directorio de proyecto, lo que significa que un archivo .env en texto plano es un vertedero accidental de secretos a punto de ocurrir. Esto no es teórico. Es un problema conocido que me ha sucedido varias veces (incluso después de decirle explícitamente a Claude que no espiara en el archivo settings.json de Claude Code). enject resuelve esto asegurando que los secretos en texto plano nunca existan en disco en absoluto. Tu archivo .env contiene solo referencias simbólicas; los valores reales residen en un almacén local cifrado y se inyectan directamente en tu subproceso al iniciarlo.
Este proyecto está inspirado en la solución/publicación del blog de Filip Hric, que utiliza un concepto similar aprovechando 1Password. Quería una solución autónoma que no dependiera de servicios de terceros, dando lugar a esta solución. Y sí, este proyecto fue construido casi en su totalidad con Claude Code con un montón de verificación y pruebas manuales.
Este proyecto está diseñado principalmente para mitigar el problema conocido de que las herramientas de IA/LLM lean accidentalmente los secretos de .env en tu proyecto. Los beneficios adicionales incluyen prevenir la filtración de secretos si un .env se comete accidentalmente a un repositorio, la capacidad de compartir archivos .env que contienen referencias en lugar de secretos en texto plano, y la opción de compartir el propio almacén cifrado.
Este proyecto no es una bala de plata para evitar que un agente de IA obtenga tus secretos. Por ejemplo, un agente aún puede escribir código (por accidente o mediante inyección de aviso) que extraiga secretos a la salida del terminal o a un archivo en tiempo de ejecución. Recomendamos encarecidamente no confiar en esta herramienta, ni en los archivos .env en general, para almacenar secretos de producción.
Tu archivo .env se ve así:
DATABASE_URL=en://database_url
STRIPE_KEY=en://stripe_key
PORT=3000
Técnicamente es seguro cometerlo (aunque quizás no lo hagas), y más importante: seguro para cualquier herramienta de IA que espíe accidentalmente (o quizás no tan accidentalmente) en él.
Cuando ejecutas enject run -- npm start, esto:
en:// contra el mapa descifradoEl archivo de almacén es un blob binario. Sin la contraseña maestra, es indistinguible del ruido aleatorio. El nonce se genera de nuevo en cada escritura, por lo que la reutilización del nonce AES-GCM es imposible. Cualquier modificación al texto cifrado — incluso un solo bit cambiado — provoca que la autenticación falle y se rechace el descifrado.
Esta versión aún está en alfa, por lo que requiere agregar la versión más reciente al instalar al llamar cargo install
cargo install enject --version 0.2.0-alpha
Requiere Rust 1.70+.
git clone https://github.com/greatscott/enject
cd enject
cargo build --release
El binario compilado está en target/release/enject. Instálalo una vez en una ubicación de tu PATH para poder ejecutarlo desde cualquier proyecto:
macOS / Linux (bash o zsh)
# Opción A: ~/.local/bin (no requiere sudo, común en Linux)
mkdir -p ~/.local/bin
cp target/release/enject ~/.local/bin/
# Opción B: /usr/local/bin (requiere sudo, disponible en todo el sistema)
sudo cp target/release/enject /usr/local/bin/
# Opción C: ~/.cargo/bin (ya está en PATH si usaste rustup)
cp target/release/enject ~/.cargo/bin/
Si usaste la opción A y ~/.local/bin no está ya en tu PATH, agrega esto a tu configuración de shell (~/.zshrc, ~/.bashrc, o ~/.bash_profile):
export PATH="$HOME/.local/bin:$PATH"
Luego recárgalo:
source ~/.zshrc # o ~/.bashrc
Verifica que funcionó:
enject --version
El binario se instala globalmente — nunca lo reinstalas. Pero cada proyecto obtiene su propio almacén cifrado:
cd your-project
enject init
Esto crea .enject/ en el directorio actual con la configuración del proyecto y el almacén cifrado. Agrégalo a .gitignore — nunca debe ser comiteado.
Ejecuta esto una vez por proyecto, en la raíz del proyecto:
enject init
Esto genera una sal aleatoria de 32 bytes, escribe .enject/config.toml, crea un almacén cifrado vacío en .enject/store, y te solicita establecer una contraseña maestra. Agrega .enject/ a tu .gitignore — el almacén nunca debe ser comiteado.
enject set some_database_url
# prompts: Value for 'database_url': (hidden)
enject set some_api_key
Los valores siempre se ingresan de forma interactiva. No hay forma de pasar un valor como argumento de línea de comandos — esto evita que los secretos aparezcan en el historial del shell o en la salida de ps.
.envDATABASE_URL=en://some_database_url
MY_API_KEY=en://stripe_key
PORT=3000
Las líneas simples KEY=VALUE pasan sin cambios. Solo se resuelven las referencias en://.
enject run -- npm start
enject run -- python manage.py runserver
enject run -- cargo run
Todo después de -- se pasa textualmente al sistema operativo. El subproceso hereda todo tu entorno de shell (por lo que PATH, HOME, etc. están presentes) con los valores de .env superpuestos.
enject list # print stored key names (never values)
enject delete <key> # remove a secret
enject import <file> # encrypt all values in a plaintext .env, rewrite it as en:// template
enject rotate # re-encrypt the store with a new master password
No hay get ni export. Imprimir un valor secreto en stdout crea un vector de fuga legible por IA — el objetivo de enject es mantener los valores fuera del disco y fuera de cualquier flujo de salida legible.
Cada invariante de seguridad tiene una prueba automatizada correspondiente y una ruta de inspección manual.
cargo test
31 pruebas, todas cubriendo las afirmaciones a continuación.
Automated: store::password::tests::test_encrypt_decrypt_roundtrip
Guarda un secreto, persiste el almacén, lo recarga desde el disco, lo descifra y verifica que el valor viaje correctamente. Solo pasa si los bytes en disco son texto cifrado válido — el texto plano fallaría el descifrado.
cargo test store::password::tests::test_encrypt_decrypt_roundtrip
Inspección manual:
enject init # password: test123
enject set mykey # value: my-super-secret
xxd .enject/store | head -5
strings .enject/store
xxd mostrará datos binarios. strings no devolverá nada — no hay secuencias ASCII para extraer. Los primeros 12 bytes son el nonce aleatorio; todo lo demás es texto cifrado AES-GCM con una etiqueta de autenticación de 16 bytes añadida.
Automated: store::password::tests::test_nonce_changes_on_each_save
Guarda el almacén dos veces seguidas, lee los primeros 12 bytes del archivo cada vez y afirma que son diferentes.
cargo test store::password::tests::test_nonce_changes_on_each_save
Inspección manual:
xxd .enject/store | head -1 # note the first 12 bytes
enject set anotherkey # any write rotates the nonce
xxd .enject/store | head -1 # first 12 bytes are now different
Automated: store::password::tests::test_wrong_password_returns_err
Crea un almacén con una contraseña, luego intenta desbloquearlo con una contraseña diferente y afirma que se devuelve Err.
cargo test store::password::tests::test_wrong_password_returns_err
Manual:
enject list # enter the wrong password
# output: "Wrong master password or corrupted store."
# exit code: 1
AES-GCM produce una etiqueta de autenticación de 16 bytes sobre el texto cifrado. Cualquier modificación — incluso un solo bit cambiado — hace que la verificación falle antes de que el descifrado continúe. El texto plano nunca se expone.
Automated: store::password::tests::test_tampered_ciphertext_returns_err
Cambia un byte en la región de texto cifrado del archivo de almacén (después del nonce de 12 bytes), luego intenta descifrar y afirma Err.
cargo test store::password::tests::test_tampered_ciphertext_returns_err
Manual:
# Flip byte 20 (inside ciphertext, past the nonce)
python3 -c "
data = open('.enject/store', 'rb').read()
bad = data[:20] + bytes([data[20] ^ 0xFF]) + data[21:]
open('.enject/store', 'wb').write(bad)
"
enject list
# output: "Wrong master password or corrupted store."
en:// no resueltaSi una referencia en .env no tiene una clave correspondiente en el almacén, enject run sale inmediatamente con un código distinto de cero. El subproceso nunca se inicia.
Automated: env_template::tests::test_unknown_ev_ref_returns_err
Llama a resolve() con una referencia que no tiene entrada coincidente y afirma Err.
cargo test env_template::tests::test_unknown_ev_ref_returns_err
Manual:
echo "DB=en://nonexistent_key" > .env
enject run -- env
# output: Secret 'nonexistent_key' not found in store. Add it with: enject set nonexistent_key
# exit code: 1 (the `env` subprocess never ran)
Implementar un almacén opcional/adicional en todo el sistema para un mantenimiento más fácil de secretos usados en múltiples proyectos.
Reduce la necesidad de ingresar manualmente la contraseña del almacén cada vez que se realizan actualizaciones.