Skip to content

Add skills/ folder with dotenv and dotenvx agent skills for AI coding agent discovery#940

Open
Copilot wants to merge 4 commits intomasterfrom
copilot/explore-skill-md-file
Open

Add skills/ folder with dotenv and dotenvx agent skills for AI coding agent discovery#940
Copilot wants to merge 4 commits intomasterfrom
copilot/explore-skill-md-file

Conversation

Copy link
Contributor

Copilot AI commented Mar 4, 2026

AI coding agents (Claude Code, Copilot, Codex, Gemini CLI) lack a structured activation signal for dotenv — they fall back to parsing the full README or relying on training data. The skills.sh ecosystem solves this with a SKILL.md convention: YAML frontmatter for agent routing + full reference docs.

Changes

  • skills/dotenv/SKILL.md — dotenv skill following the skills.sh format:

    • YAML frontmatter with name + description for agent routing
    • Full content mirroring README.md (badges and logo image stripped; all sections preserved including Usage, Advanced, Docs, FAQ, Agents/AS2)
  • skills/dotenvx/SKILL.md — dotenvx skill mirroring the dotenvx README:

    • YAML frontmatter with name + description for agent routing
    • Quickstart, Run Anywhere, Multiple Environments, Encryption, Variable Expansion, get/set, pre-commit hook, CI/CD (GitHub Actions), Agentic Secret Storage (AS2), and FAQ
  • CHANGELOG.md — Unreleased entry added

Install via skills.sh

npx skills add motdotla/dotenv

Install a specific skill only:

npx skills add motdotla/dotenv --skill dotenvx

Agents get the full, authoritative README content for each package — the same docs humans use — routed precisely via YAML frontmatter. The two skills cross-link each other for easy navigation.

Original prompt

This section details on the original issue you should resolve

<issue_title>Explore SKILL.md file</issue_title>
<issue_description>Explore adding a SKILL.md file to the repo somehow.

  • Would that make sense?
  • Is there a discovery mechanism out there?
  • Could this help LLMs use dotenv?
  • Could this help LLMs use the safer dotenvx?</issue_description>

<agent_instructions>Look up things like Vercel's new https://skills.sh and others doing similar. Make sure dotenv can take advantage of these new distribution channels.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: motdotla <3848+motdotla@users.noreply.github.com>
Copilot AI changed the title [WIP] Explore adding SKILL.md file to repository Add SKILL.md for AI coding agent discovery Mar 4, 2026
@motdotla
Copy link
Owner

motdotla commented Mar 4, 2026

@copilot is this correct? how will skills.sh automatically find it. can you point me to their code or documentation that this will actually work before i publish

Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot is this correct? how will skills.sh automatically find it. can you point me to their code or documentation that this will actually work before i publish

Yes, it's correct. The skills CLI (npx skills add motdotla/dotenv) explicitly includes the repo root as a discovery location.

From the vercel-labs/skills README under Skill Discovery:

The CLI searches for skills in these locations within a repository:

  • Root directory (if it contains SKILL.md)
  • skills/
  • .claude/skills/
  • … (and many other agent-specific directories)

So SKILL.md at the repo root is one of the documented patterns — the CLI will find it and install it to the user's agent directories (e.g. .claude/skills/dotenv/SKILL.md) when they run npx skills add motdotla/dotenv.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • agent-sh.github.io
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)
  • agentskills.io
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)
  • skills.sh
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)
  • vercel.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)
  • www.npmjs.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@motdotla
Copy link
Owner

motdotla commented Mar 4, 2026

hmm, maybe i should just put it under /skills though then, and then have multiple skills inside? like it would be nice to add a skill on how to do encryption using dotenvx, etc. if you were to break this into a folder with different skills what would you do?

@motdotla
Copy link
Owner

motdotla commented Mar 4, 2026

@copilot hmm, maybe i should just put it under /skills though then, and then have multiple skills inside? like it would be nice to add a skill on how to do encryption using dotenvx, etc. if you were to break this into a folder with different skills what would you do?

Co-authored-by: motdotla <3848+motdotla@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot hmm, maybe i should just put it under /skills though then, and then have multiple skills inside? like it would be nice to add a skill on how to do encryption using dotenvx, etc. if you were t...

Done in 7c080d1. The root SKILL.md is gone — skills are now under skills/:

skills/
├── dotenv/SKILL.md      # core: basic usage, ES6, config, parse, populate, debug, preload
└── dotenvx/SKILL.md     # advanced: encrypt, multiple envs, variable expansion, run, CI/CD, AS2

npx skills add motdotla/dotenv discovers both (the CLI scans skills/ subdirectories). Users can also install just one:

npx skills add motdotla/dotenv --skill dotenvx

The two skills cross-link each other so agents can navigate between them. More skills (e.g. a dedicated debugging or migration skill) can be added to skills/ later.

Copilot AI changed the title Add SKILL.md for AI coding agent discovery Add skills/ folder with dotenv and dotenvx agent skills for AI coding agent discovery Mar 4, 2026
@motdotla
Copy link
Owner

motdotla commented Mar 4, 2026

@copilot hmm, shouldn't the skill files just be the same as each of their readmes though? the readmes are so good and complete:

dotenv

# dotenv [![NPM version](https://img.shields.io/npm/v/dotenv.svg?style=flat-square)](https://www.npmjs.com/package/dotenv) [![downloads](https://img.shields.io/npm/dw/dotenv)](https://www.npmjs.com/package/dotenv)

<img src="https://raw.githubusercontent.com/motdotla/dotenv/master/dotenv.svg" alt="dotenv" align="right" width="200" />

Dotenv is a zero-dependency module that loads environment variables from a `.env` file into [`process.env`](https://nodejs.org/docs/latest/api/process.html#process_process_env). Storing configuration in the environment separate from code is based on [The Twelve-Factor App](https://12factor.net/config) methodology.

[Watch the tutorial](https://www.youtube.com/watch?v=YtkZR0NFd1g)

&nbsp;

## Usage

Install it.

```sh
npm install dotenv --save

Create a .env file in the root of your project:

# .env
S3_BUCKET="YOURS3BUCKET"
SECRET_KEY="YOURSECRETKEYGOESHERE"

And as early as possible in your application, import and configure dotenv:

require('dotenv').config() // or import 'dotenv/config' if you're using ES6
...
console.log(process.env) // remove this after you've confirmed it is working

That's it. process.env now has the keys and values you defined in your .env file:

 

Advanced

ES6

Import with ES6:

import 'dotenv/config'

ES6 import if you need to set config options:

import dotenv from 'dotenv'
dotenv.config({ path: '/custom/path/to/.env' })
bun
bun add dotenv
yarn
yarn add dotenv
pnpm
pnpm add dotenv
Monorepos

For monorepos with a structure like apps/backend/app.js, put it the .env file in the root of the folder where your app.js process runs.

# app/backend/.env
S3_BUCKET="YOURS3BUCKET"
SECRET_KEY="YOURSECRETKEYGOESHERE"
Multiline Values

If you need multiline variables, for example private keys, those are now supported (>= v15.0.0) with line breaks:

PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
...
Kh9NV...
...
-----END RSA PRIVATE KEY-----"

Alternatively, you can double quote strings and use the \n character:

PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nKh9NV...\n-----END RSA PRIVATE KEY-----\n"
Comments

Comments may be added to your file on their own line or inline:

# This is a comment
SECRET_KEY=YOURSECRETKEYGOESHERE # comment
SECRET_HASH="something-with-a-#-hash"

Comments begin where a # exists, so if your value contains a # please wrap it in quotes. This is a breaking change from >= v15.0.0 and on.

Parsing

The engine which parses the contents of your file containing environment variables is available to use. It accepts a String or Buffer and will return an Object with the parsed keys and values.

const dotenv = require('dotenv')
const buf = Buffer.from('BASIC=basic')
const config = dotenv.parse(buf) // will return an object
console.log(typeof config, config) // object { BASIC : 'basic' }
Preload

Note: Consider using dotenvx instead of preloading. I am now doing (and recommending) so.

It serves the same purpose (you do not need to require and load dotenv), adds better debugging, and works with ANY language, framework, or platform. – motdotla

You can use the --require (-r) command line option to preload dotenv. By doing this, you do not need to require and load dotenv in your application code.

$ node -r dotenv/config your_script.js

The configuration options below are supported as command line arguments in the format dotenv_config_<option>=value

$ node -r dotenv/config your_script.js dotenv_config_path=/custom/path/to/.env dotenv_config_debug=true

Additionally, you can use environment variables to set configuration options. Command line arguments will precede these.

$ DOTENV_CONFIG_<OPTION>=value node -r dotenv/config your_script.js
$ DOTENV_CONFIG_ENCODING=latin1 DOTENV_CONFIG_DEBUG=true node -r dotenv/config your_script.js dotenv_config_path=/custom/path/to/.env
Variable Expansion

Use dotenvx for variable expansion.

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
[dotenvx@0.14.1] injecting env (2) from .env
DATABASE_URL postgres://username@localhost/my_database
Command Substitution

Use dotenvx for command substitution.

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
[dotenvx@0.14.1] injecting env (1) from .env
DATABASE_URL postgres://yourusername@localhost/my_database
Encryption

Use dotenvx for encryption.

Add encryption to your .env files with a single command.

$ dotenvx set HELLO Production -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
[dotenvx] injecting env (2) from .env.production
Hello Production

learn more

Multiple Environments

Use dotenvx to manage multiple environments.

Run any environment locally. Create a .env.ENVIRONMENT file and use -f to 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
Hello production
> ^^

or with multiple .env files

$ echo "HELLO=local" > .env.local
$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f=.env.local -f=.env -- node index.js
Hello local

more environment examples

Production

Use dotenvx for production deploys.

Create a .env.production file.

$ echo "HELLO=production" > .env.production

Encrypt it.

$ dotenvx encrypt -f .env.production

Set DOTENV_PRIVATE_KEY_PRODUCTION (found in .env.keys) on your server.

$ heroku config:set DOTENV_PRIVATE_KEY_PRODUCTION=value

Commit your .env.production file to code and deploy.

$ git add .env.production
$ git commit -m "encrypted .env.production"
$ git push heroku main

Dotenvx will decrypt and inject the secrets at runtime using dotenvx run -- node index.js.

Syncing

Use dotenvx to sync your .env files.

Encrypt them with dotenvx encrypt -f .env and safely include them in source control. Your secrets are securely synced with your git.

This still subscribes to the twelve-factor app rules by generating a decryption key separate from code.

More Examples

See examples of using dotenv with various frameworks, languages, and configurations.

 

Agents

dotenvx-as2

Software is changing, and dotenv must change with it—that is why I built agentic secret storage (AS2). Agents run code without humans at terminals, so plaintext .env files are the wrong primitive.

AS2 is built for autonomous software: encrypted by default, zero console access, and cryptography‑first delivery that keeps operators out of the loop.

It is backed by Vestauth, the trusted, pioneering auth layer for agents—giving each agent a cryptographic identity so requests are signed with private keys and verified with public keys. No shared secrets to leak.

It's what I'm using now. - motdotla

Quickstart

Install vestauth and initialize your agent.

npm i -g vestauth

vestauth agent init

Your agent sets secrets with a simple curl endpoint:

vestauth agent curl -X POST https://as2.dotenvx.com/set -d '{"KEY":"value"}'

And your agent gets secrets with a simple curl endpoint:

vestauth agent curl "https://as2.dotenvx.com/get?key=KEY"

That's it! This new primitive unlocks secrets access for agents without human-in-the-loop, oauth flows, or API keys. It's the future for agents.

 

FAQ

Should I commit my `.env` file?

No.

Unless you encrypt it with dotenvx. Then we recommend you do.

What about variable expansion?

Use dotenvx.

Should I have multiple `.env` files?

We recommend creating one .env file per environment. Use .env for local/development, .env.production for production and so on. This still follows the twelve factor principles as each is attributed individually to its own environment. Avoid custom set ups that work in inheritance somehow (.env.production inherits values from .env for example). It is better to duplicate values if necessary across each .env.environment file.

In a twelve-factor app, env vars are granular controls, each fully orthogonal to other env vars. They are never grouped together as “environments”, but instead are independently managed for each deploy. This is a model that scales up smoothly as the app naturally expands into more deploys over its lifetime.

The Twelve-Factor App

Additionally, we recommend using dotenvx to encrypt and manage these.

How do I use dotenv with `import`?

Simply..

// index.mjs (ESM)
import 'dotenv/config' // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
import express from 'express'

A little background..

When you run a module containing an import declaration, the modules it imports are loaded first, then each module body is executed in a depth-first traversal of the dependency graph, avoiding cycles by skipping anything already executed.

ES6 In Depth: Modules

What does this mean in plain language? It means you would think the following would work but it won't.

errorReporter.mjs:

class Client {
  constructor (apiKey) {
    console.log('apiKey', apiKey)

    this.apiKey = apiKey
  }
}

export default new Client(process.env.API_KEY)

index.mjs:

// Note: this is INCORRECT and will not work
import * as dotenv from 'dotenv'
dotenv.config()

import errorReporter from './errorReporter.mjs' // process.env.API_KEY will be blank!

process.env.API_KEY will be blank.

Instead, index.mjs should be written as..

import 'dotenv/config'

import errorReporter from './errorReporter.mjs'

Does that make sense? It's a bit unintuitive, but it is how importing of ES6 modules work. Here is a working example of this pitfall.

There are two alternatives to this approach:

  1. Preload with dotenvx: dotenvx run -- node index.js (Note: you do not need to import dotenv with this approach)
  2. Create a separate file that will execute config first as outlined in this comment on #133
Can I customize/write plugins for dotenv?

Yes! dotenv.config() returns an object representing the parsed .env file. This gives you everything you need to continue setting values on process.env. For example:

const dotenv = require('dotenv')
const variableExpansion = require('dotenv-expand')
const myEnv = dotenv.config()
variableExpansion(myEnv)
What rules does the parsing engine follow?

The parsing engine currently supports the following rules:

  • BASIC=basic becomes {BASIC: 'basic'}
  • empty lines are skipped
  • lines beginning with # are treated as comments
  • # marks the beginning of a comment (unless when the value is wrapped in quotes)
  • empty values become empty strings (EMPTY= becomes {EMPTY: ''})
  • inner quotes are maintained (think JSON) (JSON={"foo": "bar"} becomes {JSON:"{\"foo\": \"bar\"}")
  • whitespace is removed from both ends of unquoted values (see more on trim) (FOO= some value becomes {FOO: 'some value'})
  • single and double quoted values are escaped (SINGLE_QUOTE='quoted' becomes {SINGLE_QUOTE: "quoted"})
  • single and double quoted values maintain whitespace from both ends (FOO=" some value " becomes {FOO: ' some value '})
  • double quoted values expand new lines (MULTILINE="new\nline" becomes
{MULTILINE: 'new
line'}
  • backticks are supported (BACKTICK_KEY=`This has 'single' and "double" quotes inside of it.`)
What about syncing and securing .env files?

Use dotenvx to unlock syncing encrypted .env files over git.

What if I accidentally commit my `.env` file to code?

Remove it, remove git history and then install the git pre-commit hook to prevent this from ever happening again.

npm i -g @dotenvx/dotenvx
dotenvx precommit --install
What happens to environment variables that were already set?

By default, we will never modify any environment variables that have already been set. In particular, if there is a variable in your .env file which collides with one that already exists in your environment, then that variable will be skipped.

If instead, you want to override process.env use the override option.

require('dotenv').config({ override: true })
How can I prevent committing my `.env` file to a Docker build?

Use the docker prebuild hook.

# Dockerfile
...
RUN curl -fsS https://dotenvx.sh/ | sh
...
RUN dotenvx prebuild
CMD ["dotenvx", "run", "--", "node", "index.js"]
How come my environment variables are not showing up for React?

Your React code is run in Webpack, where the fs module or even the process global itself are not accessible out-of-the-box. process.env can only be injected through Webpack configuration.

If you are using react-scripts, which is distributed through create-react-app, it has dotenv built in but with a quirk. Preface your environment variables with REACT_APP_. See this stack overflow for more details.

If you are using other frameworks (e.g. Next.js, Gatsby...), you need to consult their documentation for how to inject environment variables into the client.

Why is the `.env` file not loading my environment variables successfully?

Most likely your .env file is not in the correct place. See this stack overflow.

Turn on debug mode and try again..

require('dotenv').config({ debug: true })

You will receive a helpful error outputted to your console.

Why am I getting the error `Module not found: Error: Can't resolve 'crypto|os|path'`?

You are using dotenv on the front-end and have not included a polyfill. Webpack < 5 used to include these for you. Do the following:

npm install node-polyfill-webpack-plugin

Configure your webpack.config.js to something like the following.

require('dotenv').config()

const path = require('path');
const webpack = require('webpack')

const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: './src/index.ts',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new NodePolyfillPlugin(),
    new webpack.DefinePlugin({
      'process.env': {
        HELLO: JSON.stringify(process.env.HELLO)
      }
    }),
  ]
};

Alternatively, just use dotenv-webpack which does this and more behind the scenes for you.

 

Docs

Dotenv exposes four functions:

  • config
  • parse
  • populate

Config

config will read your .env file, parse the contents, assign it to
process.env,
and return an Object with a parsed key containing the loaded content or an error key if it failed.

const result = dotenv.config()

if (result.error) {
  throw result.error
}

console.log(result.parsed)

You can additionally, pass options to config.

Options

path

Default: path.resolve(process.cwd(), '.env')

Specify a custom path if your file containing environment variables is located elsewhere.

require('dotenv').config({ path: '/custom/path/to/.env' })

By default, config will look for a file called .env in the current working directory.

Pass in multiple files as an array, and they will be parsed in order and combined with process.env (or option.processEnv, if set). The first value set for a variable will win, unless the options.override flag is set, in which case the last value set will win. If a value already exists in process.env and the options.override flag is NOT set, no changes will be made to that value.

require('dotenv').config({ path: ['.env.local', '.env'] })
quiet

Default: false

Suppress runtime logging message.

// index.js
require('dotenv').config({ quiet: false }) // change to true to suppress
console.log(`Hello ${process.env.HELLO}`)
# .env
HELLO=World
$ node index.js
[dotenv@17.0.0] injecting env (1) from .env
Hello World
encoding

Default: utf8

Specify the encoding of your file containing environment variables.

require('dotenv').config({ encoding: 'latin1' })
debug

Default: false

Turn on logging to help debug why certain keys or values are not being set as you expect.

require('dotenv').config({ debug: process.env.DEBUG })
override

Default: false

Override any environment variables that have already been set on your machine with values from your .env file(s). If multiple files have been provided in option.path the override will also be used as each file is combined with the next. Without override being set, the first value wins. With override set the last value wins.

require('dotenv').config({ override: true })
processEnv

Default: process.env

Specify an object to write your environment variables to. Defaults to process.env environment variables.

const myObject = {}
require('dotenv').config({ processEnv: myObject })

console.log(myObject) // values from .env
console.log(process.env) // this was not changed or written to

Parse

The engine which parses the contents of your file containing environment
variables is available to use. It accepts a String or Buffer and will return
an Object with the parsed keys and values.

const dotenv = require('dotenv')
const buf = Buffer.from('BASIC=basic')
const config = dotenv.parse(buf) // will return an object
console.log(typeof config, config) // object { BASIC : 'basic' }

Options

debug

Default: false

Turn on logging to help debug why certain keys or values are not being set as you expect.

const dotenv = require('dotenv')
const buf = Buffer.from('hello world')
const opt = { debug: true }
const config = dotenv.parse(buf, opt)
// expect a debug message because the buffer is not in KEY=VAL form

Populate

The engine which populates the contents of your .env file to process.env is available for use. It accepts a target, a source, and options. This is useful for power users who want to supply their own objects.

For example, customizing the source:

const dotenv = require('dotenv')
const parsed = { HELLO: 'world' }

dotenv.populate(process.env, parsed)

console.log(process.env.HELLO) // world

For example, customizing the source AND target:

const dotenv = require('dotenv')
const parsed = { HELLO: 'universe' }
const target = { HELLO: 'world' } // empty object

dotenv.populate(target, parsed, { override: true, debug: true })

console.log(target) // { HELLO: 'universe' }

options

Debug

Default: false

Turn on logging to help debug why certain keys or values are not being populated as you expect.

override

Default: false

Override any environment variables that have already been set.

 

CHANGELOG

See CHANGELOG.md

 

Who's using dotenv?

These npm modules depend on it.

Projects that expand it often use the keyword "dotenv" on npm.


### Dotenvx

dotenvx

a secure dotenv–from the creator of dotenv.

  • run anywhere (cross-platform)
  • multi-environment
  • encrypted envs

Read the whitepaper

 

Quickstart npm version downloads test suite

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!

with curl 🌐
curl -sfS https://dotenvx.sh | sh
dotenvx help

curl installs

 

with brew 🍺
brew install dotenvx/brew/dotenvx
dotenvx help

brew installs

 

with docker 🐳
docker run -it --rm -v $(pwd):/app dotenv/dotenvx help

docker pulls

 

with github releases 🐙
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 help

github releases

 

or windows 🪟
winget install dotenvx
dotenvx help

 

Run Anywhere

$ 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

see extended quickstart guide

More examples

TypeScript 📘
// 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
Deno 🦕
$ 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 cipher

Instead, use dotenvx as designed, by installing the cli as a binary - via curl, brew, etc.

Bun 🥟
$ 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
Python 🐍
$ echo "HELLO=World" > .env
$ echo 'import os;print("Hello " + os.getenv("HELLO", ""))' > index.py

$ dotenvx run -- python3 index.py
Hello World

see extended python guide

PHP 🐘
$ echo "HELLO=World" > .env
$ echo '<?php echo "Hello {$_SERVER["HELLO"]}\n";' > index.php

$ dotenvx run -- php index.php
Hello World

see extended php guide

Ruby 💎
$ echo "HELLO=World" > .env
$ echo 'puts "Hello #{ENV["HELLO"]}"' > index.rb

$ dotenvx run -- ruby index.rb
Hello World

see extended ruby guide

Go 🐹
$ 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

see extended go guide

Rust 🦀
$ 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

see extended rust guide

Java ☕️
$ 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
Clojure 🌿
$ echo "HELLO=World" > .env
$ echo '(println "Hello" (System/getenv "HELLO"))' > index.clj

$ dotenvx run -- clojure -M index.clj
Hello World
Kotlin 📐
$ 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
.NET 🔵
$ 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
Bash 🖥️
$ echo "HELLO=World" > .env

$ dotenvx run --quiet -- sh -c 'echo Hello $HELLO'
Hello World
Fish 🐠
$ echo "HELLO=World" > .env

$ dotenvx run --quiet -- sh -c 'echo Hello $HELLO'
Hello World
Cron ⏰
# run every day at 8am
0 8 * * * dotenvx run -- /path/to/myscript.sh
Frameworks ▲
$ dotenvx run -- next dev
$ dotenvx run -- npm start
$ dotenvx run -- bin/rails s
$ dotenvx run -- php artisan serve

see framework guides

Docker 🐳
$ 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 ["dotenvx", "run", "--", "echo", "Hello $HELLO"]

see docker guide

CI/CDs 🐙
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 }}

see github actions guide

Platforms
# heroku
heroku buildpacks:add https://github.com/dotenvx/heroku-buildpack-dotenvx

# docker
RUN curl -fsS https://dotenvx.sh/install.sh | sh

# vercel
npm install @dotenvx/dotenvx --save

see platform guides

Process Managers
// pm2
"scripts": {
  "start": "dotenvx run -- pm2-runtime start ecosystem.config.js --env production"
},

see process manager guides

npx
# alternatively use npx
$ npx @dotenvx/dotenvx run -- node index.js
$ npx @dotenvx/dotenvx run -- next dev
$ npx @dotenvx/dotenvx run -- npm start
npm
$ 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

[dotenvx@1.X.X] injecting env (1) from .env.production
Hello World
asdf
# use dotenvx with asdf
$ asdf plugin add dotenvx
$ asdf install dotenvx latest

thank you @jgburet of Paris 🇫🇷

Git
# use as a git submodule
$ git dotenvx run -- node index.js
$ git dotenvx run -- next dev
$ git dotenvx run -- npm start
Variable Expansion

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
[dotenvx@0.14.1] injecting env (2) from .env
DATABASE_URL postgres://username@localhost/my_database
Command Substitution

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
[dotenvx@0.14.1] injecting env (1) from .env
DATABASE_URL postgres://yourusername@localhost/my_database

 

Multiple Environments

Create a .env.production file and use -f to 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
[dotenvx@1.X.X] injecting env (1) from .env.production
Hello production
> ^^

More examples

multiple `.env` files
$ echo "HELLO=local" > .env.local

$ echo "HELLO=World" > .env

$ dotenvx run -f .env.local -f .env -- node index.js
[dotenvx@1.X.X] injecting env (1) from .env.local,.env
Hello local

Note 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.

`--overload` flag
$ echo "HELLO=local" > .env.local

$ echo "HELLO=World" > .env

$ dotenvx run -f .env.local -f .env --overload -- node index.js
[dotenvx@1.X.X] injecting env (1) from .env.local,.env
Hello World

Note that with --overload subsequent files DO override pre-existing variables defined in previous files.

`--verbose` flag
$ 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
[dotenvx@1.X.X] injecting env (1) from .env.production
Hello production
`--debug` flag
$ 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
[dotenvx@1.X.X] injecting env (1) from .env.production
Hello production
`--quiet` flag

Use --quiet to suppress all output (except errors).

$ echo "HELLO=production" > .env.production

$ dotenvx run -f .env.production --quiet -- node index.js
Hello production
`--log-level` flag

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

`--convention` flag

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)

 

Encryption

Add encryption to your .env files with a single command. Use dotenvx encrypt.

$ dotenvx encrypt
✔ encrypted (.env)

encrypted .env

A DOTENV_PUBLIC_KEY (encryption key) and a DOTENV_PRIVATE_KEY (decryption key) are generated using the same public-key cryptography as Bitcoin.

More examples

`.env`
$ echo "HELLO=World" > .env
$ dotenvx encrypt
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -- node index.js
[dotenvx@1.X.X] injecting env (2) from .env
Hello World
`.env.production`
$ 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
[dotenvx@1.X.X] 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.

`.env.ci`
$ 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
[dotenvx@1.X.X] 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?

combine multiple encrypted .env files
$ 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
[dotenvx@1.X.X] 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?

combine multiple encrypted .env files for monorepo
$ mkdir app1
$ mkdir app2
$ dotenvx set HELLO app1 -f app1/.env.ci
$ dotenvx set HELLO app2 -f app2/.env.ci
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ DOTENV_PRIVATE_KEY_CI="<app1/privat ci key>,<app2/private ci key>" dotenvx run -f app1/.env.ci -f app2/.env.ci -- node index.js
[dotenvx@1.X.X] injecting env (2) from app1/.env.ci,app2/.env.ci
Hello app1

$ DOTENV_PRIVATE_KEY_CI="<app1/privat ci key>,<app2/private ci key>" dotenvx run -f app1/.env.ci -f app2/.env.ci --overload -- node index.js
[dotenvx@1.X.X] injecting env (2) from app1/.env.ci,app2/.env.ci
Hello app2

Note the DOTENV_PRIVATE_KEY_CI (and any DOTENV_PRIVATE_KEY*) can take multiple private keys by simply comma separating them.

`--stdout`
$ echo "HELLO=World" > .env
$ dotenvx encrypt --stdout
$ dotenvx encrypt --stdout > .env.encrypted
other curves

secp256k1 is a well-known and battle tested curve, in use with Bitcoin and other cryptocurrencies, but we are open to adding support for more curves.

If your organization's compliance department requires NIST approved curves or other curves like curve25519, please reach out at security@dotenvx.com.

 

Advanced

Become a dotenvx power user.

CLI 📟

Advanced CLI commands.

`run` - Variable Expansion

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
[dotenvx@1.X.X] injecting env (2) from .env
DATABASE_URL postgres://username@localhost/my_database
`run` - Default Values

Use default values when environment variables are unset or empty.

# .env
# Default value syntax: use value if set, otherwise use default
DATABASE_HOST=${DB_HOST:-localhost}
DATABASE_PORT=${DB_PORT:-5432}

# Alternative syntax (no colon): use value if set, otherwise use default
API_URL=${API_BASE_URL-https://api.example.com}
// index.js
console.log('DATABASE_HOST', process.env.DATABASE_HOST)
console.log('DATABASE_PORT', process.env.DATABASE_PORT)
console.log('API_URL', process.env.API_URL)
$ dotenvx run --debug -- node index.js
[dotenvx@1.X.X] injecting env (3) from .env
DATABASE_HOST localhost
DATABASE_PORT 5432
API_URL https://api.example.com
`run` - Alternate Values

Use alternate values when environment variables are set and non-empty.

# .env
NODE_ENV=production

# Alternate value syntax: use alternate if set and non-empty, otherwise empty
DEBUG_MODE=${NODE_ENV:+false}
LOG_LEVEL=${NODE_ENV:+error}

# Alternative syntax (no colon): use alternate if set, otherwise empty  
CACHE_ENABLED=${NODE_ENV+true}
// index.js
console.log('NODE_ENV', process.env.NODE_ENV)
console.log('DEBUG_MODE', process.env.DEBUG_MODE)
console.log('LOG_LEVEL', process.env.LOG_LEVEL)
console.log('CACHE_ENABLED', process.env.CACHE_ENABLED)
$ dotenvx run --debug -- node index.js
[dotenvx@1.X.X] injecting env (4) from .env
NODE_ENV production
DEBUG_MODE false
LOG_LEVEL error
CACHE_ENABLED true
`run` - Interpolation Syntax Summary (Variable Expansion, Default/Alternate Values)

Complete reference for variable interpolation patterns supported by dotenvx:

# .env
DEFINED_VAR=hello
EMPTY_VAR=
# UNDEFINED_VAR is not set

# Default value syntax - use variable if set/non-empty, otherwise use default
TEST1=${DEFINED_VAR:-fallback}     # Result: "hello"
TEST2=${EMPTY_VAR:-fallback}       # Result: "fallback"  
TEST3=${UNDEFINED_VAR:-fallback}   # Result: "fallback"

# Default value syntax (no colon) - use variable if set, otherwise use default
TEST4=${DEFINED_VAR-fallback}      # Result: "hello"
TEST5=${EMPTY_VAR-fallback}        # Result: "" (empty, but set)
TEST6=${UNDEFINED_VAR-fallback}    # Result: "fallback"

# Alternate value syntax - use alternate if variable is set/non-empty, otherwise empty
TEST7=${DEFINED_VAR:+alternate}    # Result: "alternate"
TEST8=${EMPTY_VAR:+alternate}      # Result: "" (empty)
TEST9=${UNDEFINED_VAR:+alternate}  # Result: "" (empty)

# Alternate value syntax (no colon) - use alternate if variable is set, otherwise empty  
TEST10=${DEFINED_VAR+alternate}    # Result: "alternate"
TEST11=${EMPTY_VAR+alternate}      # Result: "alternate" (empty but set)
TEST12=${UNDEFINED_VAR+alternate}  # Result: "" (empty)

Key differences:

  • :- vs -: The colon makes empty values trigger the fallback
  • :+ vs +: The colon makes empty values not trigger the alternate
  • Default syntax (-): Use variable value or fallback
  • Alternate syntax (+): Use alternate value or empty string
`run` - Command Substitution

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
[dotenvx@1.X.X] injecting env (1) from .env
DATABASE_URL postgres://yourusername@localhost/my_database
`run` - Shell Expansion

Prevent your shell from expanding inline $VARIABLES before dotenvx has a chance to inject it. Use a subshell.

$ dotenvx run --env="HELLO=World" -- sh -c 'echo Hello $HELLO'
Hello World
`run` - Multiline

Dotenvx supports multiline values. This is particularly useful in conjunction with Docker - which does not support multiline values.

# .env
MULTILINE_PEM="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
u4QuUoobAgMBAAE=
-----END PUBLIC KEY-----"
// index.js
console.log('MULTILINE_PEM', process.env.MULTILINE_PEM)
$ dotenvx run -- node index.js
MULTILINE_PEM -----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
u4QuUoobAgMBAAE=
-----END PUBLIC KEY-----
`run` - Contextual Help

Unlike other dotenv libraries, dotenvx attempts to unblock you with contextual help.

For example, when missing a custom .env file:

$ dotenvx run -f .env.missing -- echo $HELLO
[MISSING_ENV_FILE] missing .env.missing file (/Users/scottmotte/Code/dotenvx/playground/apr-16/.env.missing)
[MISSING_ENV_FILE] https://github.com/dotenvx/dotenvx/issues/484 and re-run [dotenvx run -- echo]

or when missing a KEY:

$ echo "HELLO=World" > .env
$ dotenvx get GOODBYE
[MISSING_KEY] missing GOODBYE key
`run` - multiple `-f` flags

Compose multiple .env files for environment variables loading, as you need.

$ echo "HELLO=local" > .env.local
$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.local -f .env -- node index.js
[dotenvx@1.X.X] injecting env (1) from .env.local, .env
Hello local

Note 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.

`run --env HELLO=String`

Set environment variables as a simple KEY=value string pair.

$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run --env HELLO=String -f .env -- node index.js
[dotenvx@1.X.X] injecting env (1) from .env, and --env flag
Hello String
`run --overload`

Override existing env variables. These can be variables already on your machine or variables loaded as files consecutively. The last variable seen will 'win'.

$ echo "HELLO=local" > .env.local
$ echo "HELLO=World" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.local -f .env --overload -- node index.js
[dotenvx@1.X.X] injecting env (1) from .env.local, .env
Hello World

Note that with --overload subsequent files DO override pre-existing variables defined in previous files.

`run` - Environment Variable Precedence (Container/Cloud Deployments)

When deploying applications in containers or cloud environments, you often need to override specific environment variables at runtime without modifying committed .env files. By default, dotenvx follows the historic dotenv principle: environment variables already present take precedence over .env files.

# .env.prod contains: MODEL_REGISTRY=registry.company.com/models/v1
$ echo "MODEL_REGISTRY=registry.company.com/models/v1" > .env.prod
$ echo "console.log('MODEL_REGISTRY:', process.env.MODEL_REGISTRY)" > app.js

# Without environment variable set - uses .env.prod value
$ dotenvx run -f .env.prod -- node app.js
MODEL_REGISTRY: registry.company.com/models/v1

# With environment variable set (e.g., via Azure Container Service) - environment variable takes precedence
$ MODEL_REGISTRY=registry.azure.com/models/v2 dotenvx run -f .env.prod -- node app.js
MODEL_REGISTRY: registry.azure.com/models/v2

# To force .env.prod to override environment variables, use --overload
$ MODEL_REGISTRY=registry.azure.com/models/v2 dotenvx run -f .env.prod --overload -- node app.js
MODEL_REGISTRY: registry.company.com/models/v1

For container deployments: Set environment variables through your cloud provider's UI/configuration (Azure Container Service, AWS ECS, etc.) to override specific values from committed .env files without rebuilding your application.

`DOTENV_PRIVATE_KEY=key run`

Decrypt your encrypted .env by setting DOTENV_PRIVATE_KEY before dotenvx run.

$ touch .env
$ dotenvx set HELLO encrypted
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

# check your .env.keys files for your privateKey
$ DOTENV_PRIVATE_KEY="122...0b8" dotenvx run -- node index.js
[dotenvx@1.X.X] injecting env (2) from .env
Hello encrypted
`DOTENV_PRIVATE_KEY_PRODUCTION=key run`

Decrypt your encrypted .env.production by setting DOTENV_PRIVATE_KEY_PRODUCTION before dotenvx run. Alternatively, this can be already set on your server or cloud provider.

$ touch .env.production
$ dotenvx set HELLO "production encrypted" -f .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

# check .env.keys for your privateKey
$ DOTENV_PRIVATE_KEY_PRODUCTION="122...0b8" dotenvx run -- node index.js
[dotenvx@1.X.X] injecting env (2) from .env.production
Hello production encrypted

Note the DOTENV_PRIVATE_KEY_PRODUCTION ends with _PRODUCTION. This instructs dotenvx run to load the .env.production file.

`DOTENV_PRIVATE_KEY_CI=key dotenvx run`

Decrypt your encrypted .env.ci by setting DOTENV_PRIVATE_KEY_CI before dotenvx run. Alternatively, this can be already set on your server or cloud provider.

$ touch .env.ci
$ dotenvx set HELLO "ci encrypted" -f .env.ci
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

# check .env.keys for your privateKey
$ DOTENV_PRIVATE_KEY_CI="122...0b8" dotenvx run -- node index.js
[dotenvx@1.X.X] injecting env (2) from .env.ci
Hello ci encrypted

Note the DOTENV_PRIVATE_KEY_CI ends with _CI. This instructs dotenvx run to load the .env.ci file. See the pattern?

`DOTENV_PRIVATE_KEY=key DOTENV_PRIVATE_KEY_PRODUCTION=key run` - Combine Multiple

Decrypt your encrypted .env and .env.production files by setting DOTENV_PRIVATE_KEY and DOTENV_PRIVATE_KEY_PRODUCTION before dotenvx run.

$ touch .env
$ touch .env.production
$ dotenvx set HELLO encrypted
$ dotenvx set HELLO "production encrypted" -f .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

# check .env.keys for your privateKeys
$ DOTENV_PRIVATE_KEY="122...0b8" DOTENV_PRIVATE_KEY_PRODUCTION="122...0b8" dotenvx run -- node index.js
[dotenvx@1.X.X] injecting env (3) from .env, .env.production
Hello encrypted

$ DOTENV_PRIVATE_KEY_PRODUCTION="122...0b8" DOTENV_PRIVATE_KEY="122...0b8" dotenvx run -- node index.js
[dotenvx@1.X.X] injecting env (3) from .env.production, .env
Hello production encrypted

Compose any encrypted files you want this way. As long as a DOTENV_PRIVATE_KEY_${environment} is set, the values from .env.${environment} will be decrypted at runtime.

`run --verbose`

Set log level to verbose. (log levels)

$ echo "HELLO=production" > .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.production --verbose -- node index.js
loading env from .env.production (/path/to/.env.production)
HELLO set
[dotenvx@1.X.X] injecting env (1) from .env.production
Hello production
`run --debug`

Set log level to debug. (log levels)

$ echo "HELLO=production" > .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.production --debug -- node index.js
process command [node index.js]
options: {"env":[],"envFile":[".env.production"]}
loading env from .env.production (/path/to/.env.production)
{"HELLO":"production"}
HELLO set
HELLO set to production
[dotenvx@1.X.X] injecting env (1) from .env.production
executing process command [node index.js]
expanding process command to [/opt/homebrew/bin/node index.js]
Hello production
`run --quiet`

Use --quiet to suppress all output (except errors). (log levels)

$ echo "HELLO=production" > .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.production --quiet -- node index.js
Hello production
`run --log-level`

Set --log-level to whatever you wish. For example, to suppress warnings (risky), set log level to error:

$ echo "HELLO=production" > .env.production
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.production --log-level=error -- node index.js
Hello production

Available log levels are error, warn, info, verbose, debug, silly (source)

`run --strict`

Exit with code 1 if any errors are encountered - like a missing .env file or decryption failure.

$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.missing --strict -- node index.js
[MISSING_ENV_FILE] missing .env.missing file (/path/to/.env.missing)
[MISSING_ENV_FILE] ? add one with [echo "HELLO=World" > .env.missing]

This can be useful in ci scripts where you want to fail the ci if your .env file could not be decrypted at runtime.

`run --ignore`

Ignore errors like MISSING_ENV_FILE.

$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run -f .env.missing --ignore=MISSING_ENV_FILE -- node index.js
...
`run --convention=nextjs`

Load envs using Next.js' convention. Set --convention to nextjs:

$ echo "HELLO=development local" > .env.development.local
$ echo "HELLO=local" > .env.local
$ echo "HELLO=development" > .env.development
$ echo "HELLO=env" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx run --convention=nextjs -- node index.js
[dotenvx@1.X.X] injecting env (1) from .env.development.local, .env.local, .env.development, .env
Hello development local

(more conventions available upon request)

`run --convention=flow`

Load envs using dotenv-flow's convention. Set --convention to flow:

$ echo "HELLO=development local" > .env.development.local
$ echo "HELLO=development" > .env.development
$ echo "HELLO=local" > .env.local
$ echo "HELLO=env" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ NODE_ENV=development dotenvx run --convention=flow -- node index.js 
[dotenvx@1.X.X] injecting env (1) from .env.development.local, .env.development, .env.local, .env
Hello development local

Further, we recommend using DOTENV_ENV over NODE_ENV– as dotenvx works everywhere, not just node.

$ DOTENV_ENV=development dotenvx run --convention=flow -- node index.js 
[dotenvx@1.X.X] injecting env (1) from .env.development.local, .env.development, .env.local, .env
Hello development local
`run -fk`

Specify path to .env.keys. This is useful with monorepos.

$ mkdir -p apps/app1
$ touch apps/app1/.env
$ dotenvx set HELLO world -fk .env.keys -f apps/app1/.env

$ dotenvx run -fk .env.keys -f apps/app1/.env -- yourcommand
`run --ops-off`

Turn off Dotenvx Ops features.

$ dotenvx run --ops-off -- yourcommand
`get KEY`

Return a single environment variable's value.

$ echo "HELLO=World" > .env

$ dotenvx get HELLO
World
`get KEY -f`

Return a single environment variable's value from a specific .env file.

$ echo "HELLO=World" > .env
$ echo "HELLO=production" > .env.production

$ dotenvx get HELLO -f .env.production
production
`get KEY -fk`

Specify path to .env.keys. This is useful with monorepos.

$ mkdir -p apps/app1
$ touch apps/app1/.env
$ dotenvx set HELLO world -fk .env.keys -f apps/app1/.env

$ dotenvx get HELLO -fk .env.keys -f apps/app1/.env
world
`get KEY --env`

Return a single environment variable's value from a --env string.

$ dotenvx get HELLO --env HELLO=String -f .env.production
String
`get KEY --overload`

Return a single environment variable's value where each found value is overloaded.

$ echo "HELLO=World" > .env
$ echo "HELLO=production" > .env.production

$ dotenvx get HELLO -f .env.production --env HELLO=String -f .env --overload
World
`get KEY --strict`

Exit with code 1 if any errors are encountered - like a missing key, missing .env file, or decryption failure.

$ dotenvx get DOES_NOT_EXIST --strict
[MISSING_KEY] missing DOES_NOT_EXIST key
`get KEY --convention=nextjs`

Return a single environment variable's value using Next.js' convention. Set --convention to nextjs:

$ echo "HELLO=development local" > .env.development.local
$ echo "HELLO=local" > .env.local
$ echo "HELLO=development" > .env.development
$ echo "HELLO=env" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ dotenvx get HELLO --convention=nextjs
development local
`get KEY --convention=flow`

Return a single environment variable's value using dotenv-flow's convention. Set --convention to flow:

$ echo "HELLO=development local" > .env.development.local
$ echo "HELLO=development" > .env.development
$ echo "HELLO=local" > .env.local
$ echo "HELLO=env" > .env
$ echo "console.log('Hello ' + process.env.HELLO)" > index.js

$ NODE_ENV=development dotenvx get HELLO --convention=flow
development local

Further, we recommend using DOTENV_ENV over NODE_ENV– as dotenvx works everywhere, not just node.

$ DOTENV_ENV=development dotenvx get HELLO --convention=flow
development local
`get` (json)

Return a json response of all key/value pairs in a .env file.

$ echo "HELLO=World" > .env

$ dotenvx get
{"HELLO":"World"}
`get --format shell`

Return a shell formatted response of all key/value pairs in a .env file.

$ echo "HELLO=World" > .env
$ echo "KEY=value" >> .env

$ dotenvx get --format shell
HELLO=World KEY=value

This can be useful when combined with env on the command line.

$ echo "console.log('Hello ' + process.env.KEY + ' ' + process.env.HELLO)" > index.js
$ env $(dotenvx get --format=shell) node index.js
Hello value World

or with export.

$ echo "console.log('Hello ' + process.env.KEY + ' ' + process.env.HELLO)" > index.js
$ export $(dotenvx get --format=shell)
$ node index.js
Hello value World
`get --format eval`

Return an eval-ready shell formatted response of all key/value pairs in a .env file.

$ echo "HELLO=World" > .env
$ echo "KEY=value" >> .env

$ dotenvx get --format eval
HELLO="World"
KEY="value"

Note that this exports newlines and quoted strings.

This can be useful for more complex .env values (spaces, escaped characters, quotes, etc) combined with eval on the command line.

$ echo "console.log('Hello ' + process.env.KEY + ' ' + process.env.HELLO)" > index.js
$ eval $(dotenvx get --format=eval) node index.js
Hello value World

Be careful with eval as it allows for arbitrary execution of commands. Prefer dotenvx run -- but in some cases eval is a sharp knife that is useful to have.

`get --all`

Return preset machine envs as well.

$ echo "HELLO=World" > .env

$ dotenvx get --all
{"PWD":"/some/file/path","USER":"username","LIBRARY_PATH":"/usr/local/lib", ..., "HELLO":"World"}
`get --all --pretty-print`

Make the output more readable - pretty print it.

$ echo "HELLO=World" > .env

$ dotenvx get --all --pretty-print
{
  "PWD": "/some/filepath",
  "USER": "username",
  "LIBRARY_PATH": "/usr/local/lib",
  ...,
  "HELLO": "World"
}
`set KEY value`

Set an encrypted key/value (on by default).

$ touch .env

$ dotenvx set HELLO World
set HELLO with encryption (.env)
`set KEY value -f`

Set an (encrypted) key/value for another .env file.

$ touch .env.production

$ dotenvx set HELLO production -f .env.production
set HELLO with encryption (.env.production)
`set KEY value -fk`

Specify path to .env.keys. This is useful with monorepos.

$ mkdir -p apps/app1
$ touch apps/app1/.env

$ dotenvx set HELLO world -fk .env.keys -f apps/app1/.env
set HELLO with encryption (.env)

Put it to use.

$ dotenvx get -fk .env.keys -f apps/app1/.env

Use it with a relative path.

$ cd apps/app1
$ dotenvx get -fk ../../.env.keys -f .env
`set KEY "value with spaces"`

Set a value containing spaces.

$ touch .env.ci

$ dotenvx set HELLO "my ci" -f .env.ci
set HELLO with encryption (.env.ci)
`set KEY -- "- + * ÷"`

If your value starts with a dash (-), then place two dashes instructing the cli that there are no more flag arguments.

$ touch .env.ci

$ dotenvx set HELLO -f .env.ci -- "- + * ÷"
set HELLO with encryption (.env.ci)
`set KEY value --plain`

Set a plaintext key/value.

$ touch .env

$ dotenvx set HELLO World --plain
set HELLO (.env)
`encrypt`

Encrypt the contents of a .env file to an encrypted .env file.

$ echo "HELLO=World" > .env

$ dotenvx encrypt
✔ encrypted (.env)
✔ key added to .env.keys (DOTENV_PRIVATE_KEY)
⮕  next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys
⮕  next run [DOTENV_PRIVATE_KEY='122...0b8' dotenvx run -- yourcommand] to test decryption locally
`encrypt -f`

Encrypt the contents of a specified .env file to an encrypted .env file.

$ echo "HELLO=World" > .env
$ echo "HELLO=Production" > .env.production

$ dotenvx encrypt -f .env.production
✔ encrypted (.env.production)
✔ key added to .env.keys (DOTENV_PRIVATE_KEY_PRODUCTION)
⮕  next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys
⮕  next run [DOTENV_PRIVATE_KEY='bff...bc4' dotenvx run -- yourcommand] to test decryption locally
`encrypt -fk`

Specify path to .env.keys. This is useful with monorepos.

$ mkdir -p apps/app1
$ echo "HELLO=World" > apps/app1/.env

$ dotenvx encrypt -fk .env.keys -f apps/app1/.env
✔ encrypted (apps/app1/.env)

Put it to use.

$ dotenvx run -fk .env.keys -f apps/app1/.env

Use with a relative path.

$ cd apps/app1
$ dotenvx run -fk ../../.env.keys -f .env
`encrypt -k`

Specify the key(s) to encrypt by passing --key.

$ echo "HELLO=World\nHELLO2=Universe" > .env

$ dotenvx encrypt -k HELLO2
✔ encrypted (.env)

Even specify a glob pattern.

$ echo "HELLO=World\nHOLA=Mundo" > .env

$ dotenvx encrypt -k "HE*"
✔ encrypted (.env)
`encrypt -ek`

Specify the key(s) to NOT encrypt by passing --exclude-key.

$ echo "HELLO=World\nHELLO2=Universe" > .env

$ dotenvx encrypt -ek HELLO
✔ encrypted (.env)

Even specify a glob pattern.

$ echo "HELLO=World\nHOLA=Mundo" > .env

$ dotenvx encrypt -ek "HO*"
✔ encrypted (.env)
`encrypt --stdout`

Encrypt the contents of a .env file and send to stdout.

$ echo "HELLO=World" > .env
$ dotenvx encrypt --stdout
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/            public-key encryption for .env files          /
#/       [how it works](https://dotenvx.com/encryption)     /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY="034af93e93708b994c10f236c96ef88e47291066946cce2e8d98c9e02c741ced45"
# .env
HELLO="encrypted:BDqDBibm4wsYqMpCjTQ6BsDHmMadg9K3dAt+Z9HPMfLEIRVz50hmLXPXRuDBXaJi/LwWYEVUNiq0HISrslzQPaoyS8Lotg3gFWJTsNCdOWnqpjF2xNUX2RQiP05kAbEXM6MWVjDr"

or send to a file:

$ echo "HELLO=World" > .env
$ dotenvx encrypt --stdout > somefile.txt
`decrypt`

Decrypt the contents of an encrypted .env file to an unencrypted .env file.

$ echo "HELLO=World" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx decrypt
✔ decrypted (.env)
`decrypt -f`

Decrypt the contents of a specified encrypted .env file to an unencrypted .env file.

$ echo "HELLO=World" > .env
$ echo "HELLO=Production" > .env.production

$ dotenvx encrypt -f .env.production
✔ encrypted (.env.production)
$ dotenvx decrypt -f .env.production
✔ decrypted (.env.production)
`decrypt -fk`

Specify path to .env.keys. This is useful with monorepos.

$ mkdir -p apps/app1
$ echo "HELLO=World" > apps/app1/.env

$ dotenvx encrypt -fk .env.keys -f apps/app1/.env
✔ encrypted (apps/app1/.env)
$ dotenvx decrypt -fk .env.keys -f apps/app1/.env
✔ decrypted (apps/app1/.env)
`decrypt -k`

Decrypt the contents of a specified key inside an encrypted .env file.

$ echo "HELLO=World\nHOLA=Mundo" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx decrypt -k HELLO
✔ decrypted (.env)

Even specify a glob pattern.

$ echo "HELLO=World\nHOLA=Mundo" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx decrypt -k "HE*"
✔ encrypted (.env)
`decrypt -ek`

Decrypt the contents inside an encrypted .env file except for an excluded key.

$ echo "HELLO=World\nHOLA=Mundo" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx decrypt -ek HOLA
✔ decrypted (.env)

Even specify a glob pattern.

$ echo "HELLO=World\nHOLA=Mundo" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx decrypt -ek "HO*"
✔ encrypted (.env)
`decrypt --stdout`

Decrypt the contents of an encrypted .env file and send to stdout.

$ dotenvx decrypt --stdout
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/            public-key encryption for .env files          /
#/       [how it works](https://dotenvx.com/encryption)     /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY="034af93e93708b994c10f236c96ef88e47291066946cce2e8d98c9e02c741ced45"
# .env
HELLO="World"

or send to a file:

$ dotenvx decrypt --stdout > somefile.txt
`keypair`

Print public/private keys for .env file.

$ echo "HELLO=World" > .env
$ dotenvx encrypt

$ dotenvx keypair
{"DOTENV_PUBLIC_KEY":"<publicKey>","DOTENV_PRIVATE_KEY":"<privateKey>"}
`keypair -f`

Print public/private keys for .env.production file.

$ echo "HELLO=Production" > .env.production
$ dotenvx encrypt -f .env.production

$ dotenvx keypair -f .env.production
{"DOTENV_PUBLIC_KEY_PRODUCTION":"<publicKey>","DOTENV_PRIVATE_KEY_PRODUCTION":"<privateKey>"}
`keypair -fk`

Specify path to .env.keys. This is useful for printing public/private keys for monorepos.

$ mkdir -p apps/app1
$ echo "HELLO=World" > apps/app1/.env
$ dotenvx encrypt -fk .env.keys -f apps/app1/.env

$ dotenvx keypair -fk .env.keys -f apps/app1/.env
{"DOTENV_PUBLIC_KEY":"<publicKey>","DOTENV_PRIVATE_KEY":"<privateKey>"}
`keypair DOTENV_PRIVATE_KEY`

Print specific keypair for .env file.

$ echo "HELLO=World" > .env
$ dotenvx encrypt

$ dotenvx keypair DOTENV_PRIVATE_KEY
<privateKey>
`keypair --format shell`

Print a shell formatted response of public/private keys.

$ echo "HELLO=World" > .env
$ dotenx encrypt

$ dotenvx keypair --format shell
DOTENV_PUBLIC_KEY=<publicKey> DOTENV_PRIVATE_KEY=<privateKey>
`ls`

Print all .env files in a tree structure.

$ touch .env
$ touch .env.production
$ mkdir -p apps/backend
$ touch apps/backend/.env

$ dotenvx ls
├─ .env.production
├─ .env
└─ apps
   └─ backend
      └─ .env
`ls directory`

Print all .env files inside a specified path to a directory.

$ touch .env
$ touch .env.production
$ mkdir -p apps/backend
$ touch apps/backend/.env

$ dotenvx ls apps/backend
└─ .env
`ls -f`

Glob .env filenames matching a wildcard.

$ touch .env
$ touch .env.production
$ mkdir -p apps/backend
$ touch apps/backend/.env
$ touch apps/backend/.env.prod

$ dotenvx ls -f **/.env.prod*
├─ .env.production
└─ apps
   └─ backend
      └─ .env.prod
`ls -ef`

Glob .env filenames excluding a wildcard.

$ touch .env
$ touch .env.production
$ mkdir -p apps/backend
$ touch apps/backend/.env
$ touch apps/backend/.env.prod

$ dotenvx ls -ef '**/.env.prod*'
├─ .env
└─ apps
   └─ backend
      └─ .env
`rotate`

Rotate public/private keys for .env file and re-encrypt all encrypted values.

$ echo "HELLO=World" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx rotate
✔ rotated (.env)
`rotate -f`

Rotate public/private keys for a specified encrypted .env file and re-encrypt all encrypted values.

$ echo "HELLO=World" > .env
$ echo "HELLO=Production" > .env.production

$ dotenvx encrypt -f .env.production
✔ encrypted (.env.production)
$ dotenvx rotate -f .env.production
✔ rotated (.env.production)
`rotate -fk`

Specify path to .env.keys. This is useful with monorepos.

$ mkdir -p apps/app1
$ echo "HELLO=World" > apps/app1/.env

$ dotenvx encrypt -fk .env.keys -f apps/app1/.env
✔ encrypted (apps/app1/.env)
$ dotenvx rotate -fk .env.keys -f apps/app1/.env
✔ rotated (apps/app1/.env)
`rotate -k`

Rotate the contents of a specified key inside an encrypted .env file.

$ echo "HELLO=World\nHOLA=Mundo" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx rotate -k HELLO
✔ rotated (.env)

Even specify a glob pattern.

$ echo "HELLO=World\nHOLA=Mundo" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx rotate -k "HE*"
✔ rotated (.env)
`rotate -ek`

Rotate the encrypted contents inside an encrypted .env file except for an excluded key.

$ echo "HELLO=World\nHOLA=Mundo" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx rotate -ek HOLA
✔ rotated (.env)

Even specify a glob pattern.

$ echo "HELLO=World\nHOLA=Mundo" > .env
$ dotenvx encrypt
✔ encrypted (.env)
$ dotenvx rotate -ek "HO*"
✔ rotated (.env)
`rotate --stdout`

Rotate the contents of an encrypted .env file and send to stdout.

$ dotenvx rotate --stdout
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
#/            public-key encryption for .env files          /
#/       [how it works](https://dotenvx.com/encryption)     /
#/----------------------------------------------------------/
DOTENV_PUBLIC_KEY="034af93e93708b994c10f236c96ef88e47291066946cce2e8d98c9e02c741ced45"
# .env
HELLO="encrypted:12345"

or send to a file:

$ dotenvx rotate --stdout > somefile.txt
`help`

Output help for dotenvx.

$ dotenvx help
Usage: dotenvx run -- yourcommand

a secure dotenv–from the creator of `dotenv`

Options:
  -l, --log-level <level>      set log level (default: "info")
  -q, --quiet                  sets log level to error
  -v, --verbose                sets log level to verbose
  -d, --debug                  sets log level to debug
  -V, --version                output the version number
  -h, --help                   display help for command

Commands:
  run                inject env at runtime [dotenvx run -- yourcommand]
  get [KEY]          return a single environment variable
  set <KEY> <value>  set a single environment variable
  encrypt            convert .env file(s) to encrypted .env file(s)
  decrypt            convert encrypted .env file(s) to plain .env file(s)
  keypair [KEY]      print public/private keys for .env file(s)
  ls [directory]     print all .env files in a tree structure
 
Advanced: 
  pro                          🏆 pro
  ext                          🔌 extensions

You can get more detailed help per command with dotenvx help COMMAND.

$ dotenvx help run
Usage: @dotenvx/dotenvx run [options]

inject env at runtime [dotenvx run -- yourcommand]

Options:
  -e, --env <strings...>            environment variable(s) set as string (example: "HELLO=World") (default: [])
  -f, --env-file <paths...>         path(s) to your env file(s) (default: [])
  -fv, --env-vault-file <paths...>  path(s) to your .env.vault file(s) (default: [])
  -o, --overload                    override existing env variables
  --convention <name>               load a .env convention (available conventions: ['nextjs'])
  -h, --help                        display help for command

Examples:

  $ dotenvx run -- npm run dev
  $ dotenvx run -- flask --app index run
  $ dotenvx run -- php artisan serve
  $ dotenvx run -- bin/rails s

Try it:

  $ echo "HELLO=World" > .env
  $ echo "console.log('Hello ' + process.env.HELLO)" > index.js

  $ dotenvx run -- node index.js
  [dotenvx@1.X.X] injecting env (1) from .env
  Hello World
`--version`

Check current version of dotenvx.

$ dotenvx --version
X.X.X

Extensions 🔌

CLI extensions.

`ext genexample`

In one command, generate a .env.example file from your current .env file contents.

$ echo "HELLO=World" > .env

$ dotenvx ext genexample
✔ updated .env.example (1)
# .env.example
HELLO=""
`ext genexample -f`

Pass multiple .env files to generate your .env.example file from the combination of their contents.

$ echo "HELLO=World" > .env
$ echo "DB_HOST=example.com" > .env.production

$ dotenvx ext genexample -f .env -f .env.production
✔ updated .env.example (2)
# .env.example
HELLO=""
DB_HOST=""
`ext genexample directory`

Generate a .env.example file inside the specified directory. Useful for monorepos.

$ echo "HELLO=World" > .env
$ mkdir -p apps/backend
$ echo "HELLO=Backend" > apps/backend/.env

$ dotenvx ext genexample apps/backend
✔ updated .env.example (1)
# apps/backend/.env.example
HELLO=""
`ext gitignore`

Gitignore your .env files.

$ dotenvx ext gitignore
✔ ignored .env* (.gitignore)
`ext gitignore --pattern`

Gitignore specific pattern(s) of .env files.

$ dotenvx ext gitignore --pattern .env.keys
✔ ignored .env.keys (.gitignore)
`ext precommit`

Prevent .env files from being committed to code.

$ dotenvx ext precommit
[dotenvx][precommit] .env files (1) protected (encrypted or gitignored)
`ext precommit --install`

Install a shell script to .git/hooks/pre-commit to prevent accidentally committing any .env files to source control.

$ dotenvx ext precommit --install
[dotenvx][precommit] dotenvx ext precommit installed [.git/hooks/pre-commit]
`ext precommit directory`

Prevent .env files from being committed to code inside a specified path to a directory.

$ echo "HELLO=World" > .env
$ mkdir -p apps/backend
$ echo "HELLO=Backend" > apps/backend/.env

$ dotenvx ext precommit apps/backend
[dotenvx][precommit] apps/backend/.env not protected (encrypted or gitignored)
`ext prebuild`

Prevent .env files from being built into your docker containers.

Add it to your Dockerfile.

# Dockerfile
RUN curl -fsS https://dotenvx.sh | sh

...

RUN dotenvx ext prebuild
CMD ["dotenvx", "run", "--", "node", "index.js"]
`ext prebuild directory`

Prevent .env files from being built into your docker containers inside a specified path to a directory.

Add it to your Dockerfile.

# Dockerfile
RUN curl -fsS https://dotenvx.sh | sh

...

RUN dotenvx ext prebuild apps/backend
CMD ["dotenvx", "run", "--", "node", "apps/backend/index.js"]
`ext scan`

Scan for leaked secrets.

$ dotenvx ext scan
100 commits scanned.
no leaks found

Uses gitleaks under the hood.

Library 📦

Use dotenvx directly in code.

`config()`

Use directly in node.js code.

# .env
HELLO="World"
// index.js
require('@dotenvx/dotenvx').config()

console.log(`Hello ${process.env.HELLO}`)
$ node index.js
[dotenvx@1.X.X] injecting env (1) from .env
Hello World

It defaults to looking for a .env file.

`config(path: ['.env.local', '.env'])` - multiple files

Specify path(s) to multiple .env files.

# .env.local
HELLO="Me"
# .env
HELLO="World"
// index.js
require('@dotenvx/dotenvx').config({path: ['.env.local', '.env']})

// esm
// import dotenvx from "@dotenvx/dotenvx";
// dotenvx.config({path: ['.env.local', '.env']});

console.log(`Hello ${process.env.HELLO}`)
$ node index.js
[dotenvx@1.X.X] injecting env (1) from .env.local, .env
Hello Me
`config(overload: true)` - overload

Use overload to overwrite the prior set value.

# .env.local
HELLO="Me"
# .env
HELLO="World"
// index.js
require('@dotenvx/dotenvx').config({path: ['.env.local', '.env'], overload: true})

// esm
// import dotenvx from "@dotenvx/dotenvx";
// dotenvx.config({path: ['.env.local', '.env'], overload: true});

console.log(`Hello ${process.env.HELLO}`)
$ node index.js
[dotenvx@1.X.X] injecting env (1) from .env.local, .env
Hello World
`config(quiet: true)` - quiet

Suppress all output (except errors).

# .env
HELLO="World"
// index.js
require('@dotenvx/dotenvx').config({path: ['.env.missing', '.env'], quiet: true})

// esm
// import dotenvx from "@dotenvx/dotenvx";
// dotenvx.config({path: ['.env.missing', '.env'], quiet: true});

console.log(`Hello ${process.env.HELLO}`)
$ node index.js
Error: [MISSING_ENV_FILE] missing .env.missing file (/path/to/.env.missing)
Hello World
`config(strict: true)` - strict

Exit with code 1 if any errors are encountered - like a missing .env file or decryption failure.

# .env
HELLO="World"
// index.js
require('@dotenvx/dotenvx').config({path: ['.env.missing', '.env'], strict: true})

// esm
// import dotenvx from "@dotenvx/dotenvx";
// dotenvx.config({path: ['.env.missing', '.env'], strict: true});

console.log(`Hello ${process.env.HELLO}`)
$ node index.js
Error: [MISSING_ENV_FILE] missing .env.missing file (/path/to/.env.missing)
`config(ignore:)` - ignore

Use ignore to suppress specific errors like MISSING_ENV_FILE.

# .env
HELLO="World"
// index.js
require('@dotenvx/dotenvx').config({path: ['.env.missing', '.env'], ignore: ['MISSING_ENV_FILE']})

// esm
// import dotenvx from "@dotenvx/dotenvx";
// dotenvx.config({path: ['.env.missing', '.env'], ignore: ['MISSING_ENV_FILE']});

console.log(`Hello ${process.env.HELLO}`)
$ node index.js
[dotenvx@1.X.X] injecting env (1) from .env
Hello World
`config(envKeysFile:)` - envKeysFile

Use envKeysFile to customize the path to your .env.keys file. This is useful with monorepos.

# .env
HELLO="World"
// index.js
require('@dotenvx/dotenvx').config({path: ['.env'], envKeysFile: '../../.env.keys'})
`config(convention:)` - convention

Set a convention when using dotenvx.config(). This allows you to use the same file loading order as the CLI without needing to specify each file individually.

# Setup environment files
$ echo "HELLO=development local" > .env.development.local
$ echo "HELLO=local" > .env.local
$ echo "HELLO=development" > .env.development
$ echo "HELLO=env" > .env
// index.js
require('@dotenvx/dotenvx').config({ convention: 'nextjs' })

console.log(`Hello ${process.env.HELLO}`)
$ NODE_ENV=development node index.js
[dotenvx@1.28.0] injecting env (1) from .env.development.local, .env.local, .env.development, .env
Hello development local

This is equivalent to using --convention=nextjs with the CLI:

$ dotenvx run --convention=nextjs -- node index.js
`config(opsOff:)` - opsOff

Turn off Dotenvx Ops features.

// index.js
require('@dotenvx/dotenvx').config({opsOff: true})
`parse(src)`

Parse a .env string directly in node.js code.

// index.js
const dotenvx = require('@dotenvx/dotenvx')
const src = 'HELLO=World'
const parsed = dotenvx.parse(src)
console.log(`Hello ${parsed.HELLO}`)
$ node index.js
Hello World
`parse(src, {processEnv:})`

Sometimes, you want to run parse without it accessing process.env. (You can pass a fake processEnv this way as well - sometimes useful.)

// index.js
const dotenvx = require('@dotenvx/dotenvx')
const src = 'USER=Me'
const parsed = dotenvx.parse(src, { processEnv: {} })
console.log(`Hello ${parsed.USER}`)
$ node index.js
Hello Me
`parse(src, {privateKey:})`

Decrypt an encrypted .env string with privateKey.

// index.js
const dotenvx = require('@dotenvx/dotenvx')
const src = 'HELLO="encrypted:BE9Y7LKANx77X1pv1HnEoil93fPa5c9rpL/1ps48uaRT9zM8VR6mHx9yM+HktKdsPGIZELuZ7rr2mn1gScsmWitppAgE/1lVprNYBCqiYeaTcKXjDUXU5LfsEsflnAsDhT/kWG1l"'
const parsed = dotenvx.parse(src, { privateKey: 'a4547dcd9d3429615a3649bb79e87edb62ee6a74b007075e9141ae44f5fb412c' })
console.log(`Hello ${parsed.HELLO}`)
$ node index.js
Hello World
`set(KEY, value)`

Programmatically set an environment variable.

// index.js
const dotenvx = require('@dotenvx/dotenvx')
dotenvx.set('HELLO', 'World', { path: '.env' })
`set(KEY, value, {plain:})`

Programmatically set a plaintext environment variable.

// index.js
const dotenvx = require('@dotenvx/dotenvx')
dotenvx.set('HELLO', 'World', { plain: true })
`get(KEY)` - Decryption at Access

Programmatically get an environment variable at access/runtime.

// index.js
const dotenvx = require('@dotenvx/dotenvx')
const decryptedValue = dotenvx.get('HELLO')
console.log(decryptedValue)

This is known as Decryption at Access and is written about in the whitepaper.

 

AS2 🔐

dotenvx as2

agentic secret storage.

Secrets designed for agents. No logins. No consoles. Pure cryptography.

Quickstart

Install vestauth and initialize your agent. (AS2 uses vestauth to authenticate agents.)

npm i -g vestauth
vestauth agent init

Your agent can set secrets.

vestauth agent curl -X POST https://as2.dotenvx.com/set '{"KEY": "value"}'

Your agent can get secrets.

vestauth agent curl https://as2.dotenvx.com/get?key=KEY

 

Ops 🏰

dotenvx-ops

production grade dotenvx–with operational primitives.

As dotenvx spreads inside companies, we're learning—through enterprise engagements—that dotenvx is missing an operations layer.

Dotenvx Ops is our answer.

It's production grade dotenvx–with operational primitives for teams, infrastructure, and agents. Private key management, access controls, and more.

Quickstart

Install it and gain ops commands.

$ curl -sfS https://dotenvx.sh/ops | sh
$ dotenvx ops backup
✔ backed up [username/project]
⮕ next run [dotenvx-ops open] to view

CLI

`ops backup`

Back up .env.keys.

$ dotenvx-ops backup
✔ backed up [username/project]
`ops login`

Log in.

$ dotenvx-ops login
press Enter to open [https://ops.dotenvx.com/login/device] and enter code [D9C1-03BC]... (Y/n)
⠹ waiting on browser authorization
✔ logged in [username] to this device and activated token [dxo_6kjPifI…]
`ops logout`

Log out.

$ dotenvx ops logout
✔ logged out [username] from this device and revoked token [dxo_5ZrwRXV…]
`settings`

Check and configure various settings for Ops - username, token, and more.

$ dotenvx-ops settings
Usage: dotenvx-ops settings [options] [command]

⚙️  settings

Options:
  -h, --help        display help for command

Commands:
  username          print your username
  token [options]   print your access token (--unmask)
  device [options]  print your device pubkey (--unmask)
  hostname          print hostname
  path              print path to settings file
  help [command]    display help for command
`ops status`

Check current status of Ops - on or off (logged in or out).

$ dotenvx ops status
on

 

Whitepaper

Dotenvx: Reducing Secrets Risk with Cryptographic Separation

Abstract. An ideal secrets solution would not only centralize secrets but also contain the fallout of a breach. While secrets managers offer centralized storage and distribution, their design creates a large blast radius, risking exposure of thousands or even millions of secrets. We propose a solution that reduces the blast radius by splitting secrets management into two distinct components: an encrypted secrets file and a separate decryption key.

...

Read the whitepaper

 

Guides

Go deeper with dotenvx – detailed framework and platform guides.

 

FAQ

How does encryption work?

Dotenvx uses Elliptic Curve Integrated Encryption Scheme (ECIES) to encrypt each secret with a unique ephemeral key, while ensuring it can be decrypted using a long-term private key.

When you initialize encryption, a DOTENV_PUBLIC_KEY (encryption key) and DOTENV_PRIVATE_KEY (decryption key) are generated. The DOTENV_PUBLIC_KEY is used to encrypt secrets, and the DOTENV_PRIVATE_KEY is securely stored in your cloud secrets manager or .env.keys file.

Your encrypted .env file is then safely committed to code. Even if the file is exposed, secrets remain protected since decryption requires the separate DOTENV_PRIVATE_KEY, which is never stored alongside it. Read the whitepaper for more details.

Is it safe to commit an encrypted .env file to code?

Yes. Dotenvx encrypts secrets using AES-256 with ephemeral keys, ensuring that even if the encrypted .env file is exposed, its contents remain secure. The encryption keys themselves are protected using Secp256k1 elliptic curve cryptography, which is widely used for secure key exchange in technologies like Bitcoin.

This means that every secret in the .env file is encrypted with a unique AES-256 key, and that key is further encrypted using a public key (Secp256k1). Even if an attacker obtains the encrypted .env file, they would still need the corresponding private key—stored separately in a secrets manager—to decrypt anything.

Breaking this encryption would require brute-forcing both AES-256 and elliptic curve cryptography, which is computationally infeasible with current technology. Read the whitepaper for more details.

Why am I getting the error node: .env: not found?

You are using Node 20 or greater and it adds a differing implementation of --env-file flag support. Rather than warn on a missing .env file (like dotenv has historically done), it raises an error: node: .env: not found.

This fix is easy. Replace --env-file with -f.

# from this:
./node_modules/.bin/dotenvx run --env-file .env -- yourcommand
# to this:
./node_modules/.bin/dotenvx run -f .env -- yourcommand

more context

What happened to the .env.vault file?

I've decided we should sunset it as a technological solution to this.

The .env.vault file got us far, but it had limitations such as:

  • Pull Requests - it was difficult to tell which key had been changed
  • Security - there was no mechanism to give a teammate the ability to encrypt without also giving them the ability to decrypt. Sometimes you just want to let a contractor encrypt a new value, but you don't want them to know the rest of the secrets.
  • Conceptual - it takes more mental energy to understand the .env.vault format. Encrypted values inside a .env file is easier to quickly grasp.
  • Combining Multiple Files - there was simply no mechanism to do this well with the .env.vault file format.

That said, the .env.vault tooling will still stick around for at least 1 year under dotenvx vault parent command. I'm still using it in projects as are many thousands of other people.

How do I migrate my .env.vault file(s) to encrypted .env files?

Run $ dotenvx ext vault migrate and follow the instructions.

 

Contributing

You can fork this repo and create pull requests or if you have questions or feedback:

…tmatter

Co-authored-by: motdotla <3848+motdotla@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot hmm, shouldn't the skill files just be the same as each of their readmes though? the readmes are so good and complete:

dotenv

...

Done in e68383e.

skills/dotenv/SKILL.md is now the dotenv README.md verbatim with YAML frontmatter prepended (only the npm/download badge images and logo <img> tag are stripped — everything else including all <details> sections, Docs, FAQ, Agents/AS2 is preserved).

skills/dotenvx/SKILL.md mirrors the dotenvx README (Quickstart, Run Anywhere, Multiple Environments, Encryption, Variable Expansion, get/set, pre-commit hook, CI/CD, AS2, FAQ) with YAML frontmatter prepended.

Both skills cross-link each other at the bottom.

@motdotla motdotla marked this pull request as ready for review March 11, 2026 18:27
@charliecreates charliecreates bot requested a review from CharlieHelps March 11, 2026 18:27
@charliecreates
Copy link

I ran into an issue while reviewing PR #940.

Visible progress:

Blockers found:

  • No functional/test/code errors—these are documentation and structure additions.
  • Some docs in SKILL.md still reference badges, external images, or links that might not render as expected in skills.sh. Consider trimming any markdown/image syntax that’s not supported.
  • If full README content is being mirrored, ensure future README edits stay in sync, or automate this copy to avoid drift.
  • There’s an open question in the PR: does skills.sh auto-discover skills in this folder layout? Can you link to certain docs/code proving it will work with skills/dotenv/SKILL.md, or does it expect a different path?

Quick clarification: Can you confirm that skills.sh (or any agent system) actually auto-discovers and processes nested skills/*/SKILL.md files as you’ve arranged? If possible, please link the discovery logic or docs.

You can see my work on the copilot/explore-skill-md-file branch.

@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (4e5f0e9) to head (e68383e).

Additional details and impacted files
@@            Coverage Diff            @@
##            master      #940   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            3         3           
  Lines          476       476           
=========================================
  Hits           476       476           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Explore SKILL.md file

2 participants