env-cmd vs cross-env vs dotenv-cli: Cross-Platform Environment Variables (2026)
TL;DR
cross-env sets environment variables cross-platform in npm scripts — cross-env NODE_ENV=production node server.js works on Windows, macOS, and Linux. env-cmd loads environment variables from .env files before running a command — env-cmd -f .env.production npm start. dotenv-cli is similar to env-cmd but uses dotenv under the hood — dotenv -e .env.production -- npm start. In 2026: cross-env for inline env vars in scripts, env-cmd or dotenv-cli for loading from .env files — though Node.js 21+ has built-in --env-file support.
Key Takeaways
- cross-env: ~8M weekly downloads — sets env vars inline, cross-platform, zero config
- env-cmd: ~2M weekly downloads — loads
.envfiles before commands, supports env-specific files - dotenv-cli: ~1M weekly downloads — wraps dotenv, loads
.envfiles, cascade support - The problem:
NODE_ENV=production node server.jsfails on Windows (PowerShell/CMD syntax differs) - cross-env solves the syntax problem —
cross-env NODE_ENV=productionworks everywhere - Node.js 21+ has
node --env-file=.env— may replace env-cmd/dotenv-cli for simple cases
The Problem
# This works on macOS/Linux:
NODE_ENV=production node server.js
# This FAILS on Windows CMD:
# 'NODE_ENV' is not recognized as an internal or external command
# This FAILS on Windows PowerShell:
# The term 'NODE_ENV=production' is not recognized
# Each platform has different syntax:
# Linux/macOS: VAR=value command
# Windows CMD: set VAR=value && command
# PowerShell: $env:VAR="value"; command
# Solution: cross-env normalizes this
cross-env NODE_ENV=production node server.js
# ✅ Works on all platforms
cross-env
cross-env — cross-platform env vars:
Setup
npm install -D cross-env
Usage in package.json
{
"scripts": {
"build": "cross-env NODE_ENV=production webpack",
"dev": "cross-env NODE_ENV=development tsx watch src/index.ts",
"test": "cross-env NODE_ENV=test vitest",
"start": "cross-env NODE_ENV=production PORT=3000 node dist/index.js",
"lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint src/"
}
}
Multiple variables
{
"scripts": {
"start:staging": "cross-env NODE_ENV=production API_URL=https://staging-api.example.com PORT=3001 node dist/index.js"
}
}
With other tools
{
"scripts": {
"db:migrate": "cross-env DATABASE_URL=postgresql://localhost:5432/pkgpulse prisma migrate deploy",
"db:seed": "cross-env DATABASE_URL=postgresql://localhost:5432/pkgpulse tsx prisma/seed.ts"
}
}
cross-env-shell (for shell features)
{
"scripts": {
"greet": "cross-env-shell \"echo Hello $USER from $NODE_ENV\"",
"build:all": "cross-env-shell NODE_ENV=production \"npm run build:api && npm run build:web\""
}
}
env-cmd
env-cmd — load .env files:
Setup
npm install -D env-cmd
Basic usage
{
"scripts": {
"dev": "env-cmd -f .env.development tsx watch src/index.ts",
"start": "env-cmd -f .env.production node dist/index.js",
"test": "env-cmd -f .env.test vitest"
}
}
.env files
# .env.development
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/pkgpulse_dev
REDIS_URL=redis://localhost:6379
LOG_LEVEL=debug
# .env.production
NODE_ENV=production
PORT=8080
DATABASE_URL=postgresql://prod-db:5432/pkgpulse
REDIS_URL=redis://prod-redis:6379
LOG_LEVEL=warn
# .env.test
NODE_ENV=test
PORT=3001
DATABASE_URL=postgresql://localhost:5432/pkgpulse_test
LOG_LEVEL=silent
Fallback files
{
"scripts": {
"start": "env-cmd -f .env.local --fallback node dist/index.js"
}
}
# --fallback uses .env if the specified file doesn't exist
# Loads .env.local if it exists, otherwise falls back to .env
.env-cmdrc (environment map)
// .env-cmdrc.json — all environments in one file:
{
"development": {
"NODE_ENV": "development",
"PORT": "3000",
"DATABASE_URL": "postgresql://localhost:5432/pkgpulse_dev"
},
"production": {
"NODE_ENV": "production",
"PORT": "8080",
"DATABASE_URL": "postgresql://prod-db:5432/pkgpulse"
},
"test": {
"NODE_ENV": "test",
"PORT": "3001"
}
}
{
"scripts": {
"dev": "env-cmd -e development tsx watch src/index.ts",
"start": "env-cmd -e production node dist/index.js",
"test": "env-cmd -e test vitest"
}
}
dotenv-cli
dotenv-cli — dotenv-powered env loading:
Setup
npm install -D dotenv-cli
Usage
{
"scripts": {
"dev": "dotenv -e .env.development -- tsx watch src/index.ts",
"start": "dotenv -e .env.production -- node dist/index.js",
"test": "dotenv -e .env.test -- vitest",
"db:migrate": "dotenv -e .env.production -- prisma migrate deploy"
}
}
Cascade (multiple files)
{
"scripts": {
"dev": "dotenv -e .env.development -e .env.local -- tsx watch src/index.ts"
}
}
# Load order: .env.development first, .env.local overrides
# .env.development: PORT=3000, DATABASE_URL=...
# .env.local: PORT=3001 (override for your machine)
# Result: PORT=3001
Variable expansion
# .env.production
BASE_URL=https://pkgpulse.com
API_URL=${BASE_URL}/api
CDN_URL=${BASE_URL}/static
# dotenv-cli expands ${} references:
# API_URL becomes https://pkgpulse.com/api
Node.js 21+ --env-file (Built-in)
# Node.js 21+ has native .env file support:
node --env-file=.env dist/index.js
# Multiple files:
node --env-file=.env --env-file=.env.local dist/index.js
# In package.json:
{
"scripts": {
"start": "node --env-file=.env.production dist/index.js",
"dev": "node --env-file=.env.development --watch src/index.ts"
}
}
Limitations of --env-file:
❌ No variable expansion (${VAR} not supported)
❌ No comments inline (# only at start of line)
❌ Requires Node.js 21+
✅ Zero dependencies
✅ Works with --watch
Feature Comparison
| Feature | cross-env | env-cmd | dotenv-cli | node --env-file |
|---|---|---|---|---|
| Inline env vars | ✅ | ❌ | ❌ | ❌ |
| Load .env files | ❌ | ✅ | ✅ | ✅ |
| Multiple .env files | ❌ | ✅ (fallback) | ✅ (cascade) | ✅ |
| Variable expansion | ❌ | ❌ | ✅ (${VAR}) | ❌ |
| Environment map | ❌ | ✅ (.env-cmdrc) | ❌ | ❌ |
| Cross-platform | ✅ (core feature) | ✅ | ✅ | ✅ |
| Dependencies | 0 | 0 | dotenv | 0 (built-in) |
| Node.js version | Any | Any | Any | 21+ |
| Weekly downloads | ~8M | ~2M | ~1M | built-in |
Recommended Setup for 2026
// For most projects — combine cross-env + dotenv-cli:
{
"scripts": {
"dev": "dotenv -e .env.development -- tsx watch src/index.ts",
"build": "cross-env NODE_ENV=production tsup",
"start": "dotenv -e .env.production -- node dist/index.js",
"test": "dotenv -e .env.test -- vitest",
"lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint src/"
}
}
// Or if using Node.js 21+ — use built-in:
{
"scripts": {
"dev": "node --env-file=.env.development --watch src/index.ts",
"start": "node --env-file=.env.production dist/index.js"
}
}
When to Use Each
Use cross-env if:
- Setting env vars inline in npm scripts (
NODE_ENV=production) - Need cross-platform compatibility (Windows team members)
- Don't need .env file loading — just inline variables
Use env-cmd if:
- Loading environment from
.envfiles per environment - Want
.env-cmdrcfor all environments in one config file - Need fallback file support
Use dotenv-cli if:
- Loading from
.envfiles with variable expansion (${VAR}) - Need cascade (multiple .env files, later files override earlier)
- Already using dotenv elsewhere in your project
Use node --env-file if:
- Node.js 21+ is your minimum version
- Simple .env loading without variable expansion
- Zero dependency preference
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on cross-env v7.x, env-cmd v10.x, dotenv-cli v7.x, and Node.js 22.
Compare environment management and developer tooling on PkgPulse →