a secure dotenvβfrom the creator of `dotenv`
100K+
A secure dotenvβfrom the creator of dotenvβ .
Β
Install and use it in code just like dotenv.
npm install @dotenvx/dotenvx --save
// index.js
require('@dotenvx/dotenvx').config()
// or import '@dotenvx/dotenvx/config' // for esm
console.log(`Hello ${process.env.HELLO}`)
Β
or install globally - unlocks dotenv for any language, framework, or platform!
curl -L -o dotenvx.tar.gz "https://github.com/dotenvx/dotenvx/releases/latest/download/dotenvx-$(uname -s)-$(uname -m).tar.gz"
tar -xzf dotenvx.tar.gz
./dotenvx encrypt
Β
winget install dotenvx
dotenvx encrypt
Β
Encrypt your secrets in .env files. The values become ciphertext and only your private key can unlock them.
$ dotenvx encrypt
β encrypted (.env)
To encrypt a fresh plaintext file with a public key created elsewhere:
dotenvx encrypt -f .env.production --with <public-key>
This writes the supplied public key into the file and encrypts its values without creating or storing a private key. If every existing DOTENV_PUBLIC_KEY* entry matches the supplied key, it reuses that key, preserves existing ciphertext, and encrypts new plaintext values. A different public key, or an encrypted: value without a matching declared public key, fails with ALREADY_ENCRYPTED and leaves the file unchanged.
Commit your encrypted .env files with your code. It's safe. Now you can securely share secrets through git.
$ git add .env
$ git commit -m "encrypt .env"
Ship your code and secrets together. Dotenvx uses your private key to decrypt and inject your secrets just-in-time to your code.
$ dotenvx run -- node index.js
β injected env (2) from .env
Β
Three widely used and proven primitives make up Dotenvx's design. Git, .env, and secp256k1.
We chose git because it is the best way to deliver digital goods. It is the container ship of the digital world. All code travels on its ships. Why not secrets? Why invent an inferior secrets delivery mechanism when the world class one is right there at your fingertips.
We chose .env because it is the open standard for loading secrets into code. It represents environment variables, the core primitive that all secrets end up injected into at runtime of code.
Lastly, we needed to choose an encryption algorithm. Encryption of the values inside the .env file would allow us to place those values with code, leveraging git's distribution advantages while still following the twelve-factor config β by separating the decryption key from the code (environment).
It was important that we choose a proven, simple, asymmetric standard with small keys. We chose secp256k1, battle-tested by Bitcoin for more than seventeen years. We made cryptography an opinionated choice so developers wouldn't have to. Encryption just worked.
The result is a system built entirely from primitives, developers already understand, and infrastructure they already use. Secrets can travel with code without becoming part of the code. Git distributes them, .env defines how apps consume them, and secp256k1 keeps their values unreadable. The kicker, all this works by setting a single private key on your infrastructure - no more risky async coordination of secrets, no more centralized downtime risk. Your secrets are always just there with your code, ready to be unlocked at runtime.
Β
$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ node index.js
Hello undefined # without dotenvx
$ dotenvx run -- node index.js
Hello World # with dotenvx
> :-D
More examples
Run Claude with your real secrets while redacting them from its output.
Prerequisite: install Claude Codeβ to get the claude command.
$ curl -fsSL https://claude.ai/install.sh | bash
$ claude --version
$ echo "HELLO=World" > .env
$ dotenvx spec
$ dotenvx run -- claude -p 'Run `dotenvx get HELLO` and echo back just Hello VALUE' --dangerously-skip-permissions
Hello [REDACTED]
Run Codex with your real secrets while redacting them from its output.
Prerequisite: install the Codex CLIβ to get the codex command.
$ npm install -g @openai/codex
$ codex --version
$ echo "HELLO=World" > .env
$ dotenvx spec
$ dotenvx run -- codex exec 'Run `dotenvx get HELLO` and echo back just Hello VALUE' --skip-git-repo-check
Hello [REDACTED]
Run Cursor with your real secrets while redacting them from its output.
Prerequisite: install the separate Cursor CLIβ . Installing the Cursor desktop app does not necessarily install the agent command.
$ curl https://cursor.com/install -fsS | bash
$ agent --version
$ echo "HELLO=World" > .env
$ dotenvx spec
$ dotenvx run -- agent -p --force 'Run `dotenvx get HELLO` and echo back just Hello VALUE' --output-format text
Hello [REDACTED]
Run with secrets resolved directly from 1Password.
$ echo "HELLO=op://Personal/hello/password" > .env
$ dotenvx run -- sh -c 'echo Hello $HELLO'
Hello World
Run with secrets resolved directly from Bitwarden Password Manager.
$ echo 'HELLO="bw://My Hello Login/password"' > .env
$ dotenvx run -- sh -c 'echo Hello $HELLO'
Hello World
Install the Bitwarden Password Manager CLIβ before running dotenvx.
// package.json
{
"type": "module",
"dependencies": {
"chalk": "^5.3.0"
}
}
// index.ts
import chalk from 'chalk'
console.log(chalk.blue(`Hello ${process.env.HELLO}`))
$ npm install
$ echo "HELLO=World" > .env
$ dotenvx run -- npx tsx index.ts
Hello World
Preface Astro scripts with dotenvx run -- and read your env values in Astro.
{
"scripts": {
"dev": "dotenvx run -- astro dev",
"build": "dotenvx run -- astro build",
"preview": "dotenvx run -- astro preview"
}
}
export async function GET() {
return new Response(
JSON.stringify({
HELLO: process.env.HELLO,
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
);
}
see astro guideβ
Preface Expo scripts with dotenvx run --.
{
"scripts": {
"start": "dotenvx run -- expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "dotenvx run -- expo start --android",
"ios": "dotenvx run -- expo start --ios",
"web": "dotenvx run -- expo start --web",
"lint": "expo lint"
}
}
see expo guideβ
Install Dotenvx and @dotenvx/next-env.
$ npm install @dotenvx/dotenvx
$ npm install @dotenvx/next-env
Override @next/env in your package.json.
{
"overrides": {
"@next/env": "npm:@dotenvx/next-env"
}
}
Encrypt your .env file.
$ npx dotenvx encrypt
β encrypted (.env)
Your encrypted secrets are automatically injected and readable in Next.js.
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({
HELLO: process.env.HELLO
})
}
Set DOTENV_PRIVATE_KEY in production before deploying.
$ dotenvx encrypt -f .env.txt
// src/index.js
import envSrc from '../.env.txt'
import dotenvx from '@dotenvx/dotenvx'
const config = dotenvx.config({ envs: [{ type: 'env', value: envSrc, privateKeyName: 'DOTENV_PRIVATE_KEY' }] })
const envx = config.parsed
export default {
async fetch(request, env, ctx) {
return new Response(`Hello ${envx.HELLO}`)
}
}
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev --var $(dotenvx keypair -f .env.txt --format=colon)",
"start": "wrangler dev --var $(dotenvx keypair -f .env.txt --format=colon)",
}
$ echo "HELLO=Test" > .env.test
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ bun index.js
Hello undefined
$ dotenvx run -f .env.test -- bun index.js
Hello Test
$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + Deno.env.get('HELLO'))" > index.ts
$ deno run --allow-env index.ts
Hello undefined
$ dotenvx run -- deno run --allow-env index.ts
Hello World
Warning
Some of you are attempting to use the npm module directly with `deno run`. Don't, because deno currently has incomplete support for these encryption ciphers.$ deno run -A npm:@dotenvx/dotenvx encrypt Unknown cipherInstead, use
dotenvxas designed, by installing the cli as a binary - via curl, brew, etc.
$ echo "HELLO=World" > .env
$ echo 'import os;print("Hello " + os.getenv("HELLO", ""))' > index.py
$ dotenvx run -- python3 index.py
Hello World
$ echo "HELLO=World" > .env
$ echo '<?php echo "Hello {$_SERVER["HELLO"]}\n";' > index.php
$ dotenvx run -- php index.php
Hello World
$ echo "HELLO=World" > .env
$ echo 'puts "Hello #{ENV["HELLO"]}"' > index.rb
$ dotenvx run -- ruby index.rb
Hello World
$ echo "HELLO=World" > .env
$ echo 'package main; import ("fmt"; "os"); func main() { fmt.Printf("Hello %s\n", os.Getenv("HELLO")) }' > main.go
$ dotenvx run -- go run main.go
Hello World
$ echo "HELLO=World" > .env
$ echo 'fn main() {let hello = std::env::var("HELLO").unwrap_or("".to_string());println!("Hello {hello}");}' > src/main.rs
$ dotenvx run -- cargo run
Hello World
$ echo "HELLO=World" > .env
$ echo 'public class Index { public static void main(String[] args) { System.out.println("Hello " + System.getenv("HELLO")); } }' > index.java
$ dotenvx run -- java index.java
Hello World
$ echo "HELLO=World" > .env
$ echo '(println "Hello" (System/getenv "HELLO"))' > index.clj
$ dotenvx run -- clojure -M index.clj
Hello World
$ echo "HELLO=World" > .env
$ echo 'fun main() { val hello = System.getenv("HELLO") ?: ""; println("Hello $hello") }' > index.kt
$ kotlinc index.kt -include-runtime -d index.jar
$ dotenvx run -- java -jar index.jar
Hello World
$ dotnet new console -n HelloWorld -o HelloWorld
$ cd HelloWorld
$ echo "HELLO=World" | Out-File -FilePath .env -Encoding utf8
$ echo 'Console.WriteLine($"Hello {Environment.GetEnvironmentVariable("HELLO")}");' > Program.cs
$ dotenvx run -- dotnet run
Hello World
$ echo "HELLO=World" > .env
$ dotenvx run --quiet -- sh -c 'echo Hello $HELLO'
Hello World
$ dotenvx run -- next dev
$ dotenvx run -- npm start
$ dotenvx run -- bin/rails s
$ dotenvx run -- php artisan serve
$ docker run -it --rm -v $(pwd):/app dotenv/dotenvx run -- node index.js
Or in any image:
FROM node:latest
RUN echo "HELLO=World" > .env && echo "console.log('Hello ' + process.env.HELLO)" > index.js
RUN curl -fsS https://dotenvx.sh/install.sh | sh
CMD ["/usr/local/bin/dotenvx", "run", "--", "echo", "Hello $HELLO"]
see docker guideβ
name: build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: curl -fsS https://dotenvx.sh/install.sh | sh
- run: dotenvx run -- node build.js
env:
DOTENV_KEY: ${{ secrets.DOTENV_KEY }}
# heroku
heroku buildpacks:add https://github.com/dotenvx/heroku-buildpack-dotenvx
# docker
RUN curl -fsS https://dotenvx.sh | sh
# vercel
npm install @dotenvx/dotenvx --save
// pm2
"scripts": {
"start": "dotenvx run -- pm2-runtime start ecosystem.config.js --env production"
},
# alternatively use npx
$ npx @dotenvx/dotenvx run -- node index.js
$ npx @dotenvx/dotenvx run -- next dev
$ npx @dotenvx/dotenvx run -- npm start
$ npm install @dotenvx/dotenvx --save
{
"scripts": {
"start": "./node_modules/.bin/dotenvx run -- node index.js"
},
"dependencies": {
"@dotenvx/dotenvx": "^0.5.0"
}
}
$ npm run start
> start
> ./node_modules/.bin/dotenvx run -- node index.js
[[email protected]] injecting env (1) from .env.production
Hello World
Reference and expand variables already on your machine for use in your .env file.
# .env
USERNAME="username"
DATABASE_URL="postgres://${USERNAME}@localhost/my_database"
// index.js
console.log('DATABASE_URL', process.env.DATABASE_URL)
$ dotenvx run --debug -- node index.js
[[email protected]] injecting env (2) from .env
DATABASE_URL postgres://username@localhost/my_database
Add the output of a command to one of your variables in your .env file.
# .env
DATABASE_URL="postgres://$(whoami)@localhost/my_database"
// index.js
console.log('DATABASE_URL', process.env.DATABASE_URL)
$ dotenvx run --debug -- node index.js
[[email protected]] injecting env (1) from .env
DATABASE_URL postgres://yourusername@localhost/my_database
Β
Create a
.env.productionfile and use-fto load it. It's straightforward, yet flexible.
$ echo "HELLO=production" > .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ dotenvx run -f .env.production -- node index.js
[[email protected]] injecting env (1) from .env.production
Hello production
> ^^
More examples
$ echo "HELLO=local" > .env.local
$ echo "HELLO=World" > .env
$ dotenvx run -f .env.local,.env -- node index.js
[[email protected]] injecting env (1) from .env.local,.env
Hello local
Comma-separate multiple files after a single -f. Subsequent files do NOT override pre-existing variables defined in previous files or env. This follows historic principle. For example, above local wins β from the first file.
$ echo "HELLO=local" > .env.local
$ echo "HELLO=World" > .env
$ dotenvx run -f .env.local,.env --overload -- node index.js
[[email protected]] injecting env (1) from .env.local,.env
Hello World
Note that with --overload subsequent files DO override pre-existing variables defined in previous files.
$ echo "HELLO=production" > .env.production
$ dotenvx run -f .env.production --verbose -- node index.js
[dotenvx][verbose] injecting env from /path/to/.env.production
[dotenvx][verbose] HELLO set
[[email protected]] injecting env (1) from .env.production
Hello production
$ echo "HELLO=production" > .env.production
$ dotenvx run -f .env.production --debug -- node index.js
[dotenvx][debug] configuring options
[dotenvx][debug] {"envFile":[".env.production"]}
[dotenvx][verbose] injecting env from /path/to/.env.production
[dotenvx][debug] reading env from /path/to/.env.production
[dotenvx][debug] parsing env from /path/to/.env.production
[dotenvx][debug] {"HELLO":"production"}
[dotenvx][debug] writing env from /path/to/.env.production
[dotenvx][verbose] HELLO set
[dotenvx][debug] HELLO set to production
[[email protected]] injecting env (1) from .env.production
Hello production
Use --quiet to suppress all output (except errors).
$ echo "HELLO=production" > .env.production
$ dotenvx run -f .env.production --quiet -- node index.js
Hello production
You can also set DOTENV_QUIET=true.
$ DOTENV_QUIET=true dotenvx run -f .env.production -- node index.js
Hello production
Set --log-level to whatever you wish. For example, to suppress warnings (risky), set log level to error:
$ echo "HELLO=production" > .env.production
$ dotenvx run -f .env.production --log-level=error -- node index.js
Hello production
Available log levels are error, warn, info, verbose, debug, silly
Load envs using Next.js' conventionβ or dotenv-flow conventionβ . Set --convention to nextjs or flow:
$ echo "HELLO=development local" > .env.development.local
$ echo "HELLO=local" > .env.local
$ echo "HELLO=development" > .env.development
$ echo "HELLO=env" > .env
$ dotenvx run --convention=nextjs -- node index.js
Hello development local
$ dotenvx run --convention=flow -- node index.js
Hello development local
(more conventions available upon request)
Β
Add encryption to your
.envfiles with a single command. Usedotenvx encrypt.
$ dotenvx encrypt
β encrypted (.env)
A
DOTENV_PUBLIC_KEY(encryption key) and aDOTENV_PRIVATE_KEY(decryption key) are generated using the same public-key cryptography as Bitcoinβ .
More examples
$ echo "HELLO=World" > .env
$ dotenvx encrypt
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ dotenvx run -- node index.js
[[email protected]] injecting env (2) from .env
Hello World
$ echo "HELLO=Production" > .env.production
$ dotenvx encrypt -f .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ DOTENV_PRIVATE_KEY_PRODUCTION="<.env.production private key>" dotenvx run -- node index.js
[[email protected]] injecting env (2) from .env.production
Hello Production
Note the DOTENV_PRIVATE_KEY_PRODUCTION ends with _PRODUCTION. This instructs dotenvx run to load the .env.production file.
$ echo "HELLO=Ci" > .env.ci
$ dotenvx encrypt -f .env.ci
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ DOTENV_PRIVATE_KEY_CI="<.env.ci private key>" dotenvx run -- node index.js
[[email protected]] injecting env (2) from .env.ci
Hello Ci
Note the DOTENV_PRIVATE_KEY_CI ends with _CI. This instructs dotenvx run to load the .env.ci file. See the pattern?
$ dotenvx set HELLO World -f .env
$ dotenvx set HELLO Production -f .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js
$ DOTENV_PRIVATE_KEY="<.env private key>" DOTENV_PRIVATE_KEY_PRODUCTION="<.env.production private key>" dotenvx run -- node index.js
[[email protected]] injecting env (3) from .env, .env.production
Hello World
Note the DOTENV_PRIVATE_KEY instructs dotenvx run to load the .env file and the DOTENV_PRIVATE_KEY_PRODUCTION instructs it to load the .env.production file. See the pattern?
Point -f at a directory to load the .env inside it. From a workspace, this makes a shared root .env available without repeating its filename.
my-monorepo/
.env
apps/
web/
index.js
$ dotenvx encrypt
$ cd apps/web
$ dotenvx get HELLO -f ../..
World
$ dotenvx run -f ../.. -- node index.js
[[email protected]] injecting env (1) from ../../.env
Hello World
With the private key in your OS secret store, dotenvx finds it automatically from any workspace on the same machine.
The directory also becomes the base when using a convention:
$ dotenvx run -f ../.. --convention=nextjs -- node index.js
[[email protected]] injecting env (1) from ../../.env.development.local, ../../.env.local, ../../.env.development, ../../.env
Hello development local
For a workspace with its own encrypted .env, run from that workspace:
$ dotenvx run -- node index.js
$ echo "HELLO=World" > .env
$ dotenvx encrypt --stdout
$ dotenvx encrypt --stdout > .env.encrypted
secp256k1is a well-known and battle tested curve, in use with Bitcoin and other cryptocurrencies, but we are open to adding support for m
Content type
Image
Digest
sha256:048159a8bβ¦
Size
102.1 MB
Last updated
about 10 hours ago
docker pull dotenv/dotenvx