Nodemon's --exec Mode: Replacing the Default Node Runner Without Losing Restart Control
Nodemon's --exec flag replaces the default Node interpreter with any shell command, turning the watcher into a generic trigger—but it also shifts process lifecycle management onto you.
23 Jan 2026, 15:55 UTC

The problem: nodemon assumes you only run Node
You use nodemon to restart your app when files change. Out of the box, it runs node your-script.js. That works until your workflow needs a build step, a test runner, or a script in another language. The default runner is too narrow, and switching tools means losing the watch-and-restart loop you already rely on.
The useful takeaway: nodemon's --exec flag replaces the default Node interpreter with any shell command, turning the watcher into a generic trigger. But it also shifts process lifecycle management onto you.
How --exec replaces the runner
When you pass --exec "some-command", nodemon stops invoking node directly. Instead, it passes the string to your system shell. That means shell operators like &&, ||, and pipes work as expected. You can chain a compile step and a run step, or pipe output through a formatter.
You can set this in a nodemon.json file rather than the command line:
{
"exec": "node dist/app.js",
"watch": ["src"],
"ignore": ["*.log"],
"ext": "js,json"
}
Run nodemon from your project root with normal user permissions. The expected check is that editing a file under src/ triggers a restart, while changes to *.log files do not.
Worked example: running a test suite on change
Suppose you want to run a linter and then your test suite whenever source files change, but you do not want a long-running server. The default node runner does not help here.
From your project root, run:
nodemon --exec "npm run lint && npm test" --watch src --ext js,ts --ignore 'coverage/*'
Breakdown:
--exec "npm run lint && npm test": the shell runs the linter first; only if it succeeds does the test suite start.--watch src: limits restart triggers to the source directory.--ext js,ts: watches only JavaScript and TypeScript files.--ignore 'coverage/*': prevents test output from triggering another run.
Expected check: save a file under src/ and confirm nodemon terminates the previous command and starts the lint-and-test chain again. If the linter fails, the tests should not run because && short-circuits.
Risk: if npm test spawns worker processes, nodemon may only signal the top-level npm process. Those workers can survive and hold ports or file locks. Before and after a file change, inspect the process list with ps aux | grep node on Unix-like systems or Task Manager on Windows to confirm the old chain has exited.
Injecting environment variables without editing the command
Nodemon's --env flag sets environment variables for the exec process. This keeps your command string clean when you need different runtime configurations:
nodemon --exec "node server.js" --env NODE_ENV=development PORT=3000 --watch server.js
Here, NODE_ENV and PORT are injected into the environment. The expected check is that your application logs or uses those values on restart. This avoids embedding environment-specific values directly in nodemon.json, which helps when the same config is shared across machines.
The trade-off: portability and orphaned processes
The biggest limitation is that --exec strings are not portable across operating systems. A command using Unix-style path expansion or single quotes may fail on Windows, and vice versa. Verify the exact command on each target platform rather than assuming the shell behaves identically.
A second limitation is process cleanup. Nodemon manages only the top-level spawned process. When files change, it sends a termination signal to that process and starts a new one. If your exec command spawns child processes—common with test runners, bundlers, or servers that fork workers—those children may survive unless you explicitly handle them. This can lead to port conflicts or stale file handles.
If you hit this, you have two practical options. First, wrap your command in a script that traps termination signals and kills its own children. Second, use --delay to add a small debounce (for example, --delay 2 for two seconds) so rapid successive saves do not spawn overlapping processes while the old ones are still shutting down.
Actionable closing
Before adopting --exec, decide whether your workflow truly needs a non-Node command. If it does, start with a minimal nodemon.json that pins exec, watch, and ignore. Then verify two things: that a file change inside your watch scope restarts the process, and that the previous process is gone from the system process list. If child processes linger, add signal handling to your wrapper script or increase --delay. Treat the exec string as platform-specific configuration, not a universal constant.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.