[RRFC] Add --prefer-direct-exec flag for proper signal handling in npm run
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 777
- Forks
- 267
- PR merge metrics
- No merged PRs in 30d
Description
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
execto 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
- Related to npm/run-script#237 (implementation tracking)
- Related to npm/cli#8509 (original feature request)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the npm run entry point and review the related npm/run-script#237 and npm/cli#8509 discussions. Compare the proposed CLI flag, environment variable, and .npmrc options, including shell commands and Windows behavior; done means an agreed design that preserves current defaults while enabling direct execution and correct signal handling.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js, shell
- Domain
- cli, developer-experience
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100