# dotenv-expand: interpolation, command substitution and decryption on top of dotenv

> dotenv-expand is a zero-dependency module that expands variables, runs command substitutions and decrypts encrypted values in the result dotenv produces, and it deliberately refuses to expand the ambient process environment. Its maintainers steer users who need more to dotenvx, and the version number in the manifest is 1000.0.1.

**dotenvx/dotenv-expand** — Expand variables already on your machine for use in your .env file.

- Repository: https://github.com/dotenvx/dotenv-expand
- Website: https://dotenvx.com/open-source
- Stars: 1,057 · Forks: 99
- Language: JavaScript
- License: BSD-3-Clause
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/dotenvx-dotenv-expand

## One function, two inputs, and an object it writes into

The SDK section is the shortest part of the documentation and it defines the entire surface area: dotenv-expand exposes one function, expand. It expands, evaluates and decrypts values in a dotenv result, and it takes two options. The first is parsed, the parsed key-value object to expand, which is normally the result of dotenv.config(). The second is processEnv, which defaults to process.env and which specifies an object to read existing values from and write expanded values to. The result object carries a parsed property you can inspect. That is a small API by any standard, and the design point worth noting is that the module does not load anything itself. It is a transform applied to a parse result, which is why it can be used standalone, preloaded, or pointed at an object you own.

## The basic path: dotenv.config() then expand, with the result in process.env

The usage section is a three step tutorial. You install dotenv and dotenv-expand together, you create a .env file in the root of the project, and as early as possible in your application you configure dotenv and expand its result. The .env example uses a USERNAME variable and a DATABASE_URL that interpolates it with the braced syntax, and the JavaScript is the two requires plus a single call, dotenvExpand.expand(dotenv.config()), followed by a console.log of process.env.DATABASE_URL. Running the script prints the expanded URL. After that, process.env contains the expanded values from the .env file. The order matters and the documentation is explicit about it, because expansion reads existing values, so dotenv must have parsed the file before expand runs over it.

```sh
npm install dotenv dotenv-expand --save
```

```ini
# .env
USERNAME="dotenv"
DATABASE_URL="postgres://${USERNAME}@localhost/my_database"
```

```javascript
// index.js
const dotenv = require('dotenv')
const dotenvExpand = require('dotenv-expand')

dotenvExpand.expand(dotenv.config())

console.log(process.env.DATABASE_URL)
```

## Preload: the config entry is self-contained and needs no separate dotenv

The Advanced section has a Preload option, and it is the one most projects will use, because it removes the need to touch application code at all. Node's require option, -r, loads and expands the .env file before your application starts, with the command node -r dotenv-expand/config your_script.js. The documentation states that the preload entry is self-contained and that you do not need to separately install dotenv, which is a meaningful difference from the manual path above. Configuration options can be passed as command line arguments, for example appending dotenv_config_path=/custom/path/to/.env to the command, or as environment variables such as DOTENV_CONFIG_ENCODING=latin1. The precedence rule is stated plainly: command line arguments take precedence over environment variables. That is the whole configuration story, and it is a familiar dotenv convention rather than something new.

```sh
node -r dotenv-expand/config your_script.js
```

```sh
node -r dotenv-expand/config your_script.js dotenv_config_path=/custom/path/to/.env
```

```sh
DOTENV_CONFIG_ENCODING=latin1 node -r dotenv-expand/config your_script.js
```

## Command substitution runs real commands, and the documentation warns about it

The Command Substitution section is the feature with the sharpest edge. Using the syntax $(command) in a .env file replaces the expression with the output of that command, and the example given is NODE_VERSION=$(node --version). The warning is direct: only use command substitution with .env files you trust, because commands run with the permissions of the current process. That is the whole risk in one sentence. A .env file is a file that people copy between machines, receive from colleagues and check into repositories, and any of those paths can put a command in a place where it will be executed with the privileges of whatever process loads it. The interpolation features, by contrast, only rearrange text. The distinction matters, and it is worth deciding separately which of the two you are willing to accept in a file that travels.

## Interpolation covers braced, unbraced, default and alternate forms

The Interpolation Syntax section lists the forms the engine understands, and the set is small enough to read at a glance. BASIC is a plain value, BRACED is the braced form referring to it, UNBRACED is the bare dollar form, DEFAULT is the default value form referring to a variable that may be missing, and ALTERNATE is the alternate value form. The complete set of rules is documented separately on the dotenvx site rather than in the README, which is a fair trade since the rules are shared with the wider dotenv family. The default value form is the one that changes behaviour most in practice, because it turns a missing variable into a defined value rather than an undefined reference, and a team adopting interpolation for the first time should be deliberate about where that is applied.

## Encrypted values: the private key stays outside the .env file

The Encrypted Values section shows the third capability, which is decryption of values that carry an encrypted: prefix. The encryption itself is done by dotenvx, a separate tool, and dotenv-expand decrypts those values automatically when DOTENV_PRIVATE_KEY is set, with the command DOTENV_PRIVATE_KEY="<private key>" node -r dotenv-expand/config your_script.js. The guidance on key storage is the part that matters operationally: keep the private key separate from the encrypted .env file, for example in your cloud platform's secrets manager. The FAQ makes the same point from the other direction, answering whether it is safe to commit an encrypted .env file with yes, as long as the corresponding DOTENV_PRIVATE_KEY is stored separately and never committed with it. That is the correct model, and it is the reason the encryption step exists at all: the encrypted file can live in version control, the key cannot.

## What dotenv-expand refuses to do, and why that is a design decision

Two FAQ answers define the boundary of the module, and both point to dotenvx. The first asks how to avoid expanding pre-existing envs, and the answer is that as of v12.0.0 dotenv-expand no longer expands process.env, with the suggestion to use dotenvx and ship an encrypted .env file with the code so expansion can happen safely at runtime. The second asks how to override an existing environment variable, and the answer is that dotenv-expand does not support it, because it is a separate module without knowledge of the loading of process.env and the .env file, and so cannot reliably know what to override. That reasoning is the interesting part. The module sees a parsed object and a target object, and it has no way of knowing whether a value in the target came from the file, from the shell, or from a container orchestrator. Guessing would make the behaviour of the file unpredictable, so the module declines to touch it.

```javascript
const dotenvExpand = require('dotenv-expand')

const myEnv = {}

dotenvExpand.expand({
  processEnv: myEnv,
  parsed: {
    HELLO: 'World'
  }
})

console.log(myEnv.HELLO) // World
console.log(process.env.HELLO) // undefined
```

## Packaging: esbuild, a biome lint, and a version number of 1000.0.1

The package manifest is a small project with a few decisions worth naming. The build script runs esbuild over lib/main.js and config.js, bundling for node with a cjs format and a node16 target into a dist directory, with legal comments removed and minification on, and the exports map points the main entry at dist/lib/main.js while exposing./config and./config.js as separate entries for the preload path. Linting is done with biome rather than eslint, with a diagnostic level of error, and there is a separate dts-check step running tsc against a test project, which means the TypeScript declarations are verified rather than assumed. The version field reads 1000.0.1, a deliberate joke value that signals a mature, stable module to anyone who has seen the convention used. The licence is BSD-3-Clause, the author is motdotla, the same author as dotenv itself, and the last push to the repository was on 2026-10-15 with no GitHub releases, since the artefact ships through npm.

## Conclusion

dotenv-expand suits a Node project that already uses dotenv and wants braced interpolation, default values, command substitution or encrypted values in its .env file without adding a runtime dependency tree. It is the wrong tool if you need to override a variable that already exists in the environment, because the documentation states dotenv-expand does not support that and explains that the module has no knowledge of how process.env was loaded, and a poor fit if you want one tool across several languages, where the documentation points to dotenvx instead. Before adopting it, read the interpolation rules linked from the project, and if you preload it with node -r remember that command line arguments take precedence over environment variables, since that is where the configuration path is set.

## FAQ

### How do I use dotenv-expand with dotenv?

Install both with npm install dotenv dotenv-expand --save, then call dotenvExpand.expand(dotenv.config()) as early as possible in your application. After that, process.env contains the expanded values from the .env file.

### How do I load .env with dotenv-expand without changing application code?

Use Node's require option with the config entry: node -r dotenv-expand/config your_script.js. The preload entry is self-contained and does not need dotenv installed separately, and configuration such as dotenv_config_path can be passed as a command line argument, which takes precedence over environment variables.

### Can dotenv-expand override a variable that already exists in the environment?

No. The documentation states that dotenv-expand does not support this, because it is a separate module without knowledge of the loading of process.env and the .env file and therefore cannot reliably know what to override. Since v12.0.0 it also no longer expands process.env.

### How does command substitution work in dotenv-expand, and is it safe?

Using $(command) in a .env file replaces the expression with the output of that command. The documentation warns to only use command substitution with .env files you trust, because commands run with the permissions of the current process.

### How do encrypted .env values work with dotenv-expand?

The file is encrypted with dotenvx, and dotenv-expand automatically decrypts encrypted: values when DOTENV_PRIVATE_KEY is set. The documentation advises keeping the private key separate from the encrypted file, for example in a secrets manager, and says committing the encrypted file is safe as long as the key is stored separately and never committed with it.

## Sources

- [dotenvx/dotenv-expand on GitHub](https://github.com/dotenvx/dotenv-expand)
- [Issues](https://github.com/dotenvx/dotenv-expand/issues)
- [License: BSD-3-Clause](https://github.com/dotenvx/dotenv-expand/blob/master/LICENSE)
- [Project website](https://dotenvx.com/open-source)
- [README](https://github.com/dotenvx/dotenv-expand/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dotenvx-dotenv-expand
