cross-env, dotenv, and env-cmd are essential utilities for handling environment variables in JavaScript projects, but they solve different parts of the problem. dotenv loads variables from a .env file into process.env at runtime, making them available to your application code. cross-env ensures that setting environment variables in package.json scripts works consistently across Windows, macOS, and Linux. env-cmd acts as a wrapper that loads specific environment files before executing a command, often used to swap configurations for different deployment targets without changing code.
Managing configuration in JavaScript applications often leads to confusion because cross-env, dotenv, and env-cmd appear to do similar things. In reality, they operate at different stages of the execution lifecycle and solve distinct problems. Understanding where each tool fits prevents architectural bugs, especially when moving from local development to production CI/CD pipelines.
When you write scripts in package.json, you often need to set flags like NODE_ENV. The syntax for doing this differs between operating systems. Unix-based systems (macOS, Linux) allow VAR=value command, while Windows CMD requires set VAR=value && command.
cross-env solves this by providing a unified syntax that works everywhere. It sets the variable in the current process before running your command, ensuring your build tools receive the correct flags regardless of the developer's OS.
// package.json
{
"scripts": {
"build": "cross-env NODE_ENV=production webpack",
"start": "cross-env API_PORT=3000 node server.js"
}
}
dotenv does not handle script-level variable setting. If you try to use it to set NODE_ENV in a script command, it will fail on Windows because dotenv is a library loaded inside JavaScript, not a shell utility.
// package.json (Incorrect usage for setting flags)
{
"scripts": {
"build": "dotenv NODE_ENV=production webpack"
}
}
env-cmd can set variables, but its primary design is loading files. While it can accept key-value pairs, using it just for simple OS compatibility is often overkill compared to the lightweight cross-env.
// package.json (Possible but less common for simple flags)
{
"scripts": {
"build": "env-cmd -e production webpack"
}
}
The core distinction lies in when and how the .env file is loaded.
dotenv is a module you import inside your code. It reads the .env file and populates process.env immediately. This gives you full control over when loading happens, but you must remember to import it at the very top of your entry file.
// server.js
require('dotenv').config();
// Or with ES Modules: import 'dotenv/config';
console.log(process.env.DB_PASSWORD); // Available here
env-cmd acts as an executable wrapper. You don't import it in your code. Instead, you run your script through env-cmd, which loads the specified file into the shell environment before your Node process even starts. This keeps your source code clean of loading logic.
# Terminal usage
npx env-cmd -f .env.staging node server.js
// server.js (No import needed)
console.log(process.env.DB_PASSWORD); // Already available
cross-env does not load .env files by default. It only sets variables you explicitly pass to it in the command line. If you need file loading with cross-env, you still need to combine it with dotenv in your code.
// server.js (Must still require dotenv)
require('dotenv').config();
// package.json
// "start": "cross-env PORT=3000 node server.js"
In complex workflows, you often need to swap configuration files based on the target environment (e.g., .env.local, .env.ci, .env.production).
env-cmd excels here. It allows you to define named environments in a config file or simply pass different file paths. This is extremely useful in CI/CD pipelines where you want to enforce a specific file without modifying code.
# Run with specific file
env-cmd -f ./config/staging.env npm run test
# Run with named environment (if configured in .env-cmdrc)
env-cmd -e staging npm run test
dotenv requires you to manually specify the path in your code or pass a path option. This often leads to cluttered entry files or complex conditional logic to decide which file to load.
// server.js
const path = process.env.NODE_ENV === 'production'
? '.env.production'
: '.env.local';
require('dotenv').config({ path });
cross-env handles this by setting a variable that your code then uses to decide which file to load (as shown above), or by passing the path directly if combined with other tools. It doesn't natively swap files itself.
# Cross-env sets the variable, code handles the rest
NODE_ENV=staging node server.js
Security practices differ slightly regarding how these tools handle missing files or existing variables.
dotenv by default will not overwrite existing variables in process.env. This is a safety feature to prevent accidentally overriding system-level variables. It also silently fails if the .env file is missing, which can be dangerous if you expect secrets to be present.
// dotenv default behavior
require('dotenv').config(); // Won't crash if .env is missing
// Strict mode (crashes if file missing)
require('dotenv').config({ path: '.env', override: false });
env-cmd throws an error by default if the specified file cannot be found. This is often preferred in production pipelines to fail fast rather than running with missing configuration.
# env-cmd will exit with error code if .env.production is missing
env-cmd -f .env.production node server.js
cross-env simply sets the variable. It has no concept of file security or existence checks because it doesn't read files. It relies entirely on the arguments you provide.
# Sets the variable regardless of context
cross-env SECRET_KEY=123 node server.js
You are building a Node.js API. You want developers to just clone the repo and run npm start without worrying about OS differences or manual file loading.
dotenv + cross-envdotenv loads the .env file in code. cross-env ensures the NODE_ENV flag in package.json works for Windows devs.// package.json
"scripts": {
"start": "cross-env NODE_ENV=development node src/index.js"
}
// src/index.js
require('dotenv').config();
Your pipeline needs to run tests against a specific .env.test file and deploy using .env.prod. You don't want to modify code for each stage.
env-cmd# .github/workflows/deploy.yml
steps:
- name: Deploy Staging
run: npx env-cmd -f .env.staging npm run deploy
- name: Deploy Production
run: npx env-cmd -f .env.prod npm run deploy
You are using Webpack or Vite. You need to pass a mode flag to the bundler. The team uses mixed operating systems.
cross-envprocess.env.NODE_ENV. cross-env is the most reliable way to inject this variable before the bundler starts.// package.json
"scripts": {
"build": "cross-env NODE_ENV=production vite build"
}
| Feature | cross-env | dotenv | env-cmd |
|---|---|---|---|
| Primary Goal | OS-agnostic variable setting | Load .env file in code | Execute command with env file |
| Execution Level | Shell / Process Spawn | Runtime (Inside JS) | Shell Wrapper |
| File Loading | β No | β
Yes (.env) | β Yes (Any file) |
| OS Compatibility | β Excellent (Win/Mac/Linux) | β N/A (JS Library) | β Good (Node based) |
| Code Changes | None (package.json only) | Requires require() | None (CLI only) |
| Fail on Missing | N/A | β No (Silent) | β Yes (Default) |
These tools are not mutually exclusive; they are often used together.
dotenv in your Node.js applications to load local secrets during development. It is the standard for accessing .env variables within code.cross-env to your package.json scripts if your team includes Windows users. It is a small dependency that prevents "works on my machine" issues related to environment flags.env-cmd when your deployment strategy requires swapping entire configuration files dynamically (e.g., in Docker containers or CI pipelines) without altering your application's entry point logic.The Golden Rule: Use cross-env to set flags in package.json, dotenv to load defaults in your code, and env-cmd to override those defaults in automated pipelines.
Choose cross-env when you need to set environment variables directly in your package.json scripts (e.g., NODE_ENV=production) and must support Windows developers. It is the industry standard for solving the syntax incompatibility between Unix shells and Windows CMD/PowerShell. Use it as a prefix for your build or start commands to ensure reliable execution on any OS.
Choose dotenv when your application needs to read configuration values from a .env file into process.env during runtime. It is the foundational tool for local development, allowing you to keep secrets out of your codebase. It is best suited for backend services or Node.js scripts where you control the entry point and can load the file before your app logic runs.
Choose env-cmd when you need to switch between multiple distinct environment configurations (e.g., staging vs. production) by passing a flag to your CLI commands. It is ideal for CI/CD pipelines or complex monorepos where you want to enforce loading a specific file (like .env.staging) before a script runs, rather than relying on the default .env file or inline variable setting.
Run scripts that set and use environment variables across platforms
π NOTICE: cross-env is "done" as in it does what it does and there's no need for new features. Learn more
Most Windows command prompts will choke when you set environment variables with
NODE_ENV=production like that. (The exception is Bash on Windows,
which uses native Bash.) Similarly, there's a difference in how windows and
POSIX commands utilize environment variables. With POSIX, you use: $ENV_VAR
and on windows you use %ENV_VAR%.
cross-env makes it so you can have a single command without worrying about
setting or using the environment variable properly for the platform. Just set it
like you would if it's running on a POSIX system, and cross-env will take care
of setting it properly.
This module is distributed via npm which is bundled with node and
should be installed as one of your project's devDependencies:
npm install --save-dev cross-env
WARNING! Make sure that when you're installing packages that you spell things correctly to avoid mistakenly installing malware
NOTE : Version 8 of cross-env only supports Node.js 20 and higher, to use it on Node.js 18 or lower install version 7
npm install --save-dev cross-env@7
I use this in my npm scripts:
{
"scripts": {
"build": "cross-env NODE_ENV=production node ./start.js --enable-turbo-mode"
}
}
Ultimately, the command that is executed (using cross-spawn)
is:
node ./start.js --enable-turbo-mode
The NODE_ENV environment variable will be set by cross-env
You can set multiple environment variables at a time:
{
"scripts": {
"build": "cross-env FIRST_ENV=one SECOND_ENV=two node ./my-program"
}
}
You can also split a command into several ones, or separate the environment variables declaration from the actual command execution. You can do it this way:
{
"scripts": {
"parentScript": "cross-env GREET=\"Joe\" npm run childScript",
"childScript": "cross-env-shell \"echo Hello $GREET\""
}
}
Where childScript holds the actual command to execute and parentScript sets
the environment variables to use. Then instead of run the childScript you run
the parent. This is quite useful for launching the same command with different
env variables or when the environment variables are too long to have everything
in one line. It also means that you can use $GREET env var syntax even on
Windows which would usually require it to be %GREET%.
If you precede a dollar sign with an odd number of backslashes the expression
statement will not be replaced. Note that this means backslashes after the JSON
string escaping took place. "FOO=\\$BAR" will not be replaced.
"FOO=\\\\$BAR" will be replaced though.
Lastly, if you want to pass a JSON string (e.g., when using ts-loader), you can do as follows:
{
"scripts": {
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} node some_file.test.ts"
}
}
Pay special attention to the triple backslash (\\\) before the
double quotes (") and the absence of single quotes ('). Both of
these conditions have to be met in order to work both on Windows and UNIX.
cross-env vs cross-env-shellThe cross-env module exposes two bins: cross-env and cross-env-shell. The
first one executes commands using cross-spawn, while the second
one uses the shell option from Node's spawn.
The main use case for cross-env-shell is when you need an environment variable
to be set across an entire inline shell script, rather than just one command.
For example, if you want to have the environment variable apply to several
commands in series then you will need to wrap those in quotes and use
cross-env-shell instead of cross-env.
{
"scripts": {
"greet": "cross-env-shell GREETING=Hi NAME=Joe \"echo $GREETING && echo $NAME\""
}
}
The rule of thumb is: if you want to pass to cross-env a command that contains
special shell characters that you want interpreted, then use
cross-env-shell. Otherwise stick to cross-env.
On Windows you need to use cross-env-shell, if you want to handle
signal events
inside of your program. A common case for that is when you want to capture a
SIGINT event invoked by pressing Ctrl + C on the command-line interface.
Please note that npm uses cmd by default and that doesn't support command
substitution, so if you want to leverage that, then you need to update your
.npmrc to set the script-shell to powershell.
Learn more here.
I originally created this to solve a problem I was having with my npm scripts in angular-formly. This made contributing to the project much easier for Windows users.
env-cmd - Reads environment
variables from a file instead@naholyr/cross-env -
cross-env with support for setting default valuesMIT