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.
Windows Command Prompt understands
set for environment variables.Unix shells don't recognize
set as a variable assignment command.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.
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. Works perfectly — on Windows.
On macOS and Linux
Bash and Zsh 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.
The Fix: cross-env
The cross-env package is the cleanest solution. 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
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.
VAR=value
npm start
&
Translates Syntax
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
npm install --save-dev cross-env
What I Actually Changed in PixelPerfect
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"
}
}
• Windows-only
set command• Fails on macOS and Linux
• Production builds broken on Render
• CI/CD pipelines fail
• Works on Windows, macOS, Linux
• Cleaner — no
&& between variables• Render.com builds work immediately
• CI/CD just works
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
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.
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.
Only cross-env works across all three major operating systems without modification.
Things That Will Still Trip You Up
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. 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.
Other shell-specific things
cross-env only solves the environment variable problem. If you've got other shell-specific commands — rm -rf (fails on Windows), cp -r — those are separate issues. For file deletion, rimraf is the cross-platform equivalent of rm -rf.
"clean": "rimraf build coverage"
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.
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.
Comments
Post a Comment