Motivation ("The Why")
npm run spawns scripts through a shell (/bin/sh -c), creating an unnecessary intermediate process that interferes with signal propagation. This causes Node.js applications to terminate abruptly without completing their cleanup procedures when receiving SIGTERM/SIGINT signals.
This is a fundamental Unix process management issue that affects:
- Docker containers (not just Kubernetes)
- Systemd services
- Process managers (PM2, Forever, etc.)
- CI/CD environments
- Any production deployment using
npm run
The problem: When a termination signal is sent, the shell may exit immediately without waiting for its child process, causing npm to exit prematurely and killing the Node.js application before graceful shutdown completes.
Example
Consider a basic Node.js server with cleanup logic:
// server.js
const server = require('http').createServer();
process.on('SIGTERM', () => {
console.log('Graceful shutdown started');
server.close(() => {
console.log('Cleanup complete');
process.exit(0);
});
});
server.listen(3000);
Problem occurs in any of these scenarios:
- Docker container:
docker stop <container>
- Systemd service:
systemctl stop myapp
- Process manager:
pm2 stop app
- Manual termination:
kill <pid>
In all cases, if started with npm run start, the graceful shutdown may not complete because the shell intermediary disrupts signal propagation.
How
Add an opt-in flag to control how npm run spawns processes, eliminating the problematic shell intermediary when not needed.
Current Behaviour
# Current process tree with npm run
npm → /bin/sh -c "node server.js" → node server.js
↓ ↓
SIGTERM (shell may exit without waiting)
↓
(npm exits because child is gone)
(node process terminated abruptly)
The shell (/bin/sh) behavior varies:
- Some shells (ash, dash) exit immediately on SIGTERM
- This causes npm to detect child exit and terminate
- Node.js process is killed before cleanup completes
Current workaround requires modifying code:
{
"scripts": {
"start": "exec node server.js" // Must add exec manually
}
}
This workaround is:
- Not documented clearly
- Not obvious to developers
- Required in every project
- Easy to forget
Desired Behaviour
Provide an opt-in mechanism that works without code changes:
Option 1 - Environment variable (for deployments):
NPM_PREFER_DIRECT_EXEC=1 npm run start
Option 2 - CLI flag (for specific runs):
npm run start --prefer-direct-exec
Option 3 - Config file (for projects):
# .npmrc
prefer-direct-exec=true
When enabled:
- Simple commands: Spawn directly without shell
- Complex commands (with pipes, &&, etc.): Auto-prepend
exec to replace shell
- Windows: No change (different process model)
- Default: Current behavior (backward compatible)
Result:
# With flag enabled
npm → node server.js (direct, no shell)
↓ ↓
SIGTERM (node receives signal)
↓ ↓
(waits) (graceful shutdown completes)
References
Motivation ("The Why")
npm runspawns scripts through a shell (/bin/sh -c), creating an unnecessary intermediate process that interferes with signal propagation. This causes Node.js applications to terminate abruptly without completing their cleanup procedures when receiving SIGTERM/SIGINT signals.This is a fundamental Unix process management issue that affects:
npm runThe problem: When a termination signal is sent, the shell may exit immediately without waiting for its child process, causing npm to exit prematurely and killing the Node.js application before graceful shutdown completes.
Example
Consider a basic Node.js server with cleanup logic:
Problem occurs in any of these scenarios:
docker stop <container>systemctl stop myapppm2 stop appkill <pid>In all cases, if started with
npm run start, the graceful shutdown may not complete because the shell intermediary disrupts signal propagation.How
Add an opt-in flag to control how
npm runspawns processes, eliminating the problematic shell intermediary when not needed.Current Behaviour
The shell (
/bin/sh) behavior varies:Current workaround requires modifying code:
{ "scripts": { "start": "exec node server.js" // Must add exec manually } }This workaround is:
Desired Behaviour
Provide an opt-in mechanism that works without code changes:
Option 1 - Environment variable (for deployments):
Option 2 - CLI flag (for specific runs):
Option 3 - Config file (for projects):
When enabled:
execto replace shellResult:
# With flag enabled npm → node server.js (direct, no shell) ↓ ↓ SIGTERM (node receives signal) ↓ ↓ (waits) (graceful shutdown completes)References
npm runwait for the actual app process (direct exec / exec-replace) for graceful shutdown on Kubernetes run-script#237 (implementation tracking)--prefer-direct-execfornpm runand add a production note (tracking; blocked by @npmcli/run-script https://github.com/npm/run-script/issues/237) cli#8509 (original feature request)