# dotenv loads a .env file into process.env, and the CLI does it without code

> The zero-dependency Node module for keeping configuration out of your source, plus the newer `dotenv run` command that injects the same variables around any process.

**motdotla/dotenv** — Loads environment variables from .env for nodejs projects.

- Repository: https://github.com/motdotla/dotenv
- Website: https://www.dotenv.org
- Stars: 20,545 · Forks: 974
- Language: JavaScript
- License: BSD-2-Clause
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/motdotla-dotenv

## The three lines that wire a .env file into process.env

Installation is one command, and the README is explicit that the module has zero dependencies:

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

Then a `.env` file in the project root, written as ordinary key and value pairs:

```ini
# .env
HELLO="Dotenv"
OPENAI_API_KEY="your-api-key-goes-here"
```

And a call to `config()` as early as possible in your application, before anything reads the environment:

```javascript
// index.js
require('dotenv').config()
// or import 'dotenv/config' // for esm

console.log(`Hello ${process.env.HELLO}`)
```

The package description ties this to the Twelve-Factor App config methodology, which is where the pattern comes from: configuration lives in the environment, separate from the code, so the same build runs in every environment. `package.json` sets `main` to `dist/index.cjs` and also exposes `./config` as a subpath export, which is what makes the bare `import 'dotenv/config'` side-effect form work without pulling in the rest of the module.

## Running a command with the variables injected, no code changes

The newer part of the project is a CLI, and the README presents it as being for coding agents as much as for people. It injects the variables around an arbitrary command, so an application that has no dotenv code in it at all can still be run against a `.env` file:

```sh
npx dotenv run -- node index.js
```

The output includes a line reporting how many variables were injected and from which file, which the README shows as `injected env (2) from .env`. That message is useful for a first run and noisy afterwards, so `-q` or `--quiet` suppresses it.

The `--` separator is optional. Options go before the command and everything after the command is passed through as its arguments, which is the behaviour you want when wrapping something like a test runner that takes its own flags. `-f` or `--file` selects one or more files, comma separated or with the flag repeated, loaded in the order given, with the first value winning unless `--override` is passed:

```sh
dotenv run --file .env.local,.env node index.js
```

`--override` overwrites variables that already exist in the environment and `--debug` turns on debug logging. That combination inverts the default, which is that an existing environment variable is never clobbered by a file.

## Configuration precedence across DOTENV_* variables

Six environment variables set defaults for both `config()` and `dotenv run`: `DOTENV_ENCODING`, `DOTENV_PATH`, `DOTENV_QUIET`, `DOTENV_DEBUG`, `DOTENV_OVERRIDE` and `DOTENV_FAST`. Options and flags passed directly take precedence, followed by these, followed by the older `DOTENV_CONFIG_*` names, which still work as fallbacks for backwards compatibility.

```sh
DOTENV_PATH=./.env.local DOTENV_QUIET=true dotenv run -- node index.js
```

One detail worth knowing is stated plainly in the README: empty values and false values do not fall back. Setting `DOTENV_QUIET=false` is not treated as unset, so it will not silently fall through to the `DOTENV_CONFIG_*` variable underneath it.

This layering is what lets the same `.env` file work for the CommonJS entry point, the ESM import, and the CLI without any of them needing to know about the others.

## Multiline values and the comment change that breaks old files

Private keys and certificates are the reason multiline support exists. Since v15.0.0 a value can span lines inside quotes:

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

The alternative is a double quoted single line with literal `\n` sequences, which the README also documents.

The same version changed comment handling, and this is the one to check when upgrading. Comments can sit on their own line or trail a value:

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

A comment begins wherever a `#` appears, so a value containing a hash must be quoted. The README labels this a breaking change from v15.0.0 onward, which means a `.env` file that worked on v14 with an unquoted `#` in a token will parse differently now. Worth grepping for before you bump the version.

## Parsing a file's contents without touching process.env

The parser underneath is exported on its own, and this is the part that turns out useful beyond the basic load. It accepts a string or a Buffer and returns a plain object:

```javascript
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' }
```

Because it does not mutate the environment, you can validate a file, compare two environments, or read a config for inspection without the side effect of loading it. A monorepo is the case the README calls out for the module form rather than the CLI: with a layout like `apps/backend/app.js`, put the `.env` in the directory where that process actually runs, not at the repository root.

The install section also covers bun, yarn, pnpm and deno with their own add commands, so the module is not tied to npm despite the primary instructions using it.

## The fast parser, and what dotenv deliberately leaves out

There are two parsers. `--fast`, or `config({ fast: true })`, switches to a character scanner that the README describes as roughly twice as fast, with the classic regex parser remaining the default:

```js
require('dotenv').config({ fast: true })
```

Whether that matters depends on how many times the process starts. In a short-lived CLI invocation the parse cost is visible in a startup profile; in a long-running server it is not.

The README is unusually clear about the boundary of the project. Variable expansion, where a `.env` file references a variable already on the machine, is documented as a dotenvx feature. So is command substitution, which would put the output of `whoami` into a database URL. So is encryption of `.env` files so they can travel with the code. The module's own scope stays at reading a file into `process.env`, which is a defensible line: each of those three features adds behaviour to a format whose main virtue is that it is boring.

## Conclusion

dotenv does one small job and keeps doing it: read a `.env` file, put the pairs into `process.env`, get out of the way. The version that matters for most projects is the current one, `18.0.1` in the repository's `package.json`, because the comment handling change from v15 onward will break a `.env` file with an unquoted `#` in a value. The parser is available on its own through `dotenv.parse` when you want the values without touching the process environment, and `--fast` swaps in a character scanner roughly twice as fast when startup time shows up in a profile. Everything beyond loading, including variable expansion, command substitution and encryption, is documented as belonging to dotenvx instead.

## FAQ

### What is dotenv used for?

It reads key and value pairs from a `.env` file and puts them into `process.env` so a Node application can read configuration without hardcoding it. Install with `npm install dotenv --save`, create a `.env` file in the project root, and call `require('dotenv').config()` early in your entry point, or use `import 'dotenv/config'` for ESM. The CLI form, `npx dotenv run -- node index.js`, does the same for any command without code changes.

### Why don't we push .env files?

A `.env` file usually holds secrets such as API keys, so committing one leaks credentials into the repository history permanently. The usual pattern is to commit a `.env.example` with the keys and empty values, keep the real file out of version control, and supply it per environment through the deployment platform. dotenv itself does not enforce this; it is a loader, and what ends up in the file is up to you. Encryption of `.env` files is documented as a dotenvx feature.

### Is dotenv needed anymore?

If your only requirement is a handful of values with no special handling, setting them in the shell or in your platform's configuration is enough and needs no package. dotenv earns its place where you need more control over the file: parsing contents without mutating the environment through `dotenv.parse`, layering several files with first-value-wins semantics, selecting files per invocation with `dotenv run --file`, or reading the same `.env` consistently from CommonJS, ESM, bun and deno entry points.

### How do I use a .env file?

Create the file in the directory your process runs from, with `KEY="value"` pairs, then load it as early as possible in your entry point using `require('dotenv').config()` or `import 'dotenv/config'`. To avoid touching your code at all, run your command through the CLI instead. Use `-f` or `--file` to layer several files, `--override` when a file should win over variables already in the environment, and remember that since v15.0.0 a `#` starts a comment anywhere in an unquoted value, so quote values containing a hash.

## Sources

- [Issues](https://github.com/motdotla/dotenv/issues)
- [License: BSD-2-Clause](https://github.com/motdotla/dotenv/blob/master/LICENSE)
- [motdotla/dotenv on GitHub](https://github.com/motdotla/dotenv)
- [Project website](https://www.dotenv.org)
- [README](https://github.com/motdotla/dotenv/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/motdotla-dotenv
