Cross-Platform npm Scripts: Why Your Windows Commands Fail on macOS/Linux (and How to Fix Them)

I'll be honest — this one caught me completely off guard. I was deep into building PixelPerfect Screenshot API, everything was running beautifully on my Windows machine, and then I tried to spin up the frontend on a Linux server and got slapped with this:

'set' is not recognized as an internal or external command

At first I thought something was broken with my Node installation. Then I thought it was a Render.com configuration issue. It took me an embarrassingly long time to realize the problem was sitting right there in my package.json — a single word: set.

If you've hit this wall, this article is for you. And if you haven't hit it yet, read this anyway — because you will.

The Platform Compatibility Problem
✅ Windows (Works)
"start": "set REACT_APP_API_URL=http://localhost:8000 && react-scripts start"
Why it works:
Windows Command Prompt understands set for environment variables.
✓ Executes Successfully
❌ macOS/Linux (Fails)
"start": "set REACT_APP_API_URL=http://localhost:8000 && react-scripts start"
Why it fails:
Unix shells don't recognize set as a variable assignment command.
✗ Command Not Found

The Core Issue: Windows uses set VAR=value, while Unix systems use VAR=value or export VAR=value. They don't speak the same shell language.

Why This Happens at All

The short version: Windows and Unix-based operating systems (macOS, Linux) evolved completely separately, and they handle environment variables differently at the shell level. There's no technical reason one is better — they just made different choices decades ago, and we're still dealing with the consequences in 2026.

On Windows

The traditional approach in Command Prompt is:

set VARIABLE=value && npm start

This creates a temporary variable that lives only for that command's execution. Clean, gone when done. Works perfectly — on Windows.

On macOS and Linux

Bash and Zsh, which power most Unix terminals, use a completely different syntax:

VARIABLE=value npm start
# or
export VARIABLE=value && npm start

When a Unix shell sees set VARIABLE=value at the start of a command, it doesn't recognize set as a variable assignment — it looks for a program called set, can't find one, and crashes. That's your error.

The practical impact: In a team with mixed operating systems, a developer can commit working npm scripts, and their teammate — running macOS or Linux — literally cannot start the project. I've seen this stall entire sprints while everyone points fingers at "environment issues." It's always this. It's almost always this.

The Fix: cross-env

The cross-env package is the cleanest solution I've found, and at this point it's basically a standard in serious JavaScript projects. The idea is simple: instead of writing platform-specific syntax, you prefix your variables with cross-env and let the package figure out the translation.

cross-env VARIABLE=value npm start

That's it. One line, works everywhere. cross-env detects your OS and emits the right syntax under the hood — set on Windows, inline assignment on Unix. You never think about it again.

How cross-env Works Across Platforms
Your Script
cross-env
VAR=value
npm start
cross-env
Detects OS
&
Translates Syntax
Platform Output
Windows: set VAR=value
macOS/Linux: VAR=value

Key Benefit: Write the syntax once. cross-env handles the translation. No conditional logic, no separate scripts per OS.

Getting It Installed

Since cross-env is only needed when running scripts (not at runtime in production), install it as a dev dependency:

npm install --save-dev cross-env

Done. Now you can use it in any script in package.json.

What I Actually Changed in PixelPerfect

Let me show you the real transformation — not a contrived example, the actual scripts from the project. I had three npm scripts that all used set, and fixing them took about four minutes once I knew what I was doing.

Before: what I had (Windows only)

{
  "scripts": {
    "start": "set GENERATE_SOURCEMAP=false && react-scripts start",
    "build": "set GENERATE_SOURCEMAP=false && set ESLINT_NO_DEV_ERRORS=true && react-scripts build"
  }
}

This worked on my Windows development machine but failed anywhere else — including Render.com's Linux containers, which is where the app actually deploys. So every single production build was broken before it started. That's a bad situation to be in.

Before and After: Script Transformation
❌ Before (Windows Only)
"scripts": { "start": "set GENERATE_SOURCEMAP=false && react-scripts start", "start:lan": "set REACT_APP_API_URL=http://192.168.1.185:8000 && set REACT_APP_API_BASE_URL=http://192.168.1.185:8000 && react-scripts start", "build": "set GENERATE_SOURCEMAP=false && set ESLINT_NO_DEV_ERRORS=true && react-scripts build" }
Problems:
• Windows-only set command
• Fails on macOS and Linux
• Production builds broken on Render
• CI/CD pipelines fail
Platform-Specific
✅ After (Cross-Platform)
"scripts": { "start": "cross-env GENERATE_SOURCEMAP=false react-scripts start", "start:lan": "cross-env GENERATE_SOURCEMAP=false REACT_APP_API_URL=http://192.168.1.185:8000 REACT_APP_API_BASE_URL=http://192.168.1.185:8000 react-scripts start", "build": "cross-env GENERATE_SOURCEMAP=false ESLINT_NO_DEV_ERRORS=true react-scripts build" }
Benefits:
• Works on Windows, macOS, Linux
• Cleaner — no && between variables
• Render.com builds work immediately
• CI/CD just works
Universal Compatibility

After: what actually works everywhere

{
  "scripts": {
    "start": "cross-env GENERATE_SOURCEMAP=false react-scripts start",

    "start:lan": "cross-env GENERATE_SOURCEMAP=false REACT_APP_API_URL=http://192.168.1.185:8000 REACT_APP_API_BASE_URL=http://192.168.1.185:8000 react-scripts start",

    "build": "cross-env GENERATE_SOURCEMAP=false ESLINT_NO_DEV_ERRORS=true react-scripts build"
  }
}

Notice something else changed: I dropped the && separators between variables. With cross-env, you just list multiple variables inline one after another. The syntax is cleaner, and there's less surface area for typos.

A Note on the start:lan Script

This one deserves a separate mention because it's a pattern I find genuinely useful and don't see documented enough.

When building PixelPerfect, I needed to test the screenshot functionality on real mobile devices — not just browser dev tools' device emulation, but an actual iPhone and Android phone on my home network. The problem is that localhost:3000 only works on the machine running the server. From your phone, localhost means the phone itself.

The solution is to start the React dev server pointing at your machine's LAN IP instead of localhost. That's what start:lan does — it tells the frontend to talk to the backend at 192.168.1.185:8000 (my computer's IP on the local network), which both my development machine and my phone can reach. The phone connects to 192.168.1.185:3000, everything works, and I can test real mobile browser behavior with hot reload still functioning.

One gotcha: Your LAN IP (the 192.168.x.x address) can change if your router reassigns it, especially after a reboot. If the mobile connection suddenly stops working, check your IP first. On Windows: ipconfig. On macOS/Linux: ifconfig or ip addr. Update the script accordingly.

Platform Compatibility Matrix
Method
Windows
macOS
Linux
set VAR=value &&
export VAR=value &&
VAR=value (inline)
cross-env VAR=value RECOMMENDED

Only cross-env works across all three major operating systems without modification.

Things That Will Still Trip You Up

Using cross-env is not a complete shield against cross-platform issues in npm scripts. A few things still bite people:

Forgetting it's a devDependency

If you run npm install --production on your server and cross-env is only in devDependencies, the build command will fail because cross-env won't be installed. For most workflows this isn't a problem — builds happen in CI, not on the production server. But if you're running npm run build directly on the server, either move cross-env to dependencies or run npm install without the --production flag.

Mixing old and new syntax

If you fix three scripts and leave one with set, that one will still break. Go through every script in your package.json when you make this change. I missed one the first time around and spent 20 minutes wondering why only the build script was failing.

Other shell-specific things

cross-env only solves the environment variable problem. If you've got other shell-specific commands in your scripts — rm -rf (fails on Windows), cp -r, pipe characters — those are separate issues. For file deletion, rimraf is the cross-platform equivalent of rm -rf. For running multiple scripts, npm-run-all handles that cleanly.

In PixelPerfect I also added rimraf for cleanup:

"clean": "rimraf build coverage"

Same idea — one line, works everywhere.

Cross-Platform Development: What I Actually Do Now
1
Install cross-env in every new project
Even if you're currently working solo on Windows. The day you deploy to a Linux server, or someone else joins the project, you'll be glad it's already there.
2
Test your build script on Render/Vercel early
Don't wait until you're ready to launch to discover your build command doesn't work in a Linux container. Run a test deploy early — it takes 5 minutes and can save hours later.
3
Add a note in your README
Something like "This project uses cross-env for cross-platform npm scripts" saves the next person from spending 45 minutes debugging the same issue you already solved.
4
Audit your scripts when onboarding someone new
The first time a new team member on a different OS runs the project is a great forcing function for finding platform-specific scripts you missed.

Wrapping Up

This is one of those problems that's genuinely frustrating the first time you hit it because the error message doesn't point you at the real cause. 'set' is not recognized doesn't say "your npm script uses Windows-only syntax." You have to know to look there.

Once you know, the fix is five minutes: install cross-env, replace set VAR=value && with cross-env VAR=value in every script, commit it. From that point on, anyone on any OS can run your project without touching the configuration.

For PixelPerfect, the production build that was silently broken started working on the first deploy after this change. No other configuration needed. That's the kind of fix I like.

The one-line takeaway: If your npm scripts set environment variables, use cross-env. Install it today, use it everywhere, never think about it again.


I write about real problems I hit while building PixelPerfect Screenshot API — a developer tool for capturing website screenshots via API. If you're building something similar or just enjoy debugging production issues after the fact, more articles like this at OneTechly.

Comments

Popular posts from this blog