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. 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.
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
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.
• 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
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.
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.
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
Post a Comment