Skip to main content

env-cmd vs cross-env vs dotenv-cli: Cross-Platform Environment Variables (2026)

·PkgPulse Team

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 .env files before commands, supports env-specific files
  • dotenv-cli: ~1M weekly downloads — wraps dotenv, loads .env files, cascade support
  • The problem: NODE_ENV=production node server.js fails on Windows (PowerShell/CMD syntax differs)
  • cross-env solves the syntax problem — cross-env NODE_ENV=production works 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

Featurecross-envenv-cmddotenv-clinode --env-file
Inline env vars
Load .env files
Multiple .env files✅ (fallback)✅ (cascade)
Variable expansion✅ (${VAR})
Environment map✅ (.env-cmdrc)
Cross-platform✅ (core feature)
Dependencies00dotenv0 (built-in)
Node.js versionAnyAnyAny21+
Weekly downloads~8M~2M~1Mbuilt-in

// 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 .env files per environment
  • Want .env-cmdrc for all environments in one config file
  • Need fallback file support

Use dotenv-cli if:

  • Loading from .env files 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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.