child_process vs cross-env vs execa vs node-cmd vs shell-quote vs shelljs
Executing Shell Commands and Managing Environment Variables in Node.js
child_processcross-envexecanode-cmdshell-quoteshelljsSimilar Packages:

Executing Shell Commands and Managing Environment Variables in Node.js

child_process is Node.js's built-in module for spawning child processes and executing system commands. cross-env is a utility for setting environment variables in a cross-platform way, commonly used in npm scripts. execa is a popular, promise-based wrapper around child_process that simplifies command execution with better defaults and cross-platform compatibility. node-cmd provides a minimal promise/callback interface for running shell commands but lacks active maintenance and advanced features. shell-quote safely escapes and formats command arguments as shell strings to prevent injection vulnerabilities. shelljs emulates Unix shell commands (like cp, ls, grep) in JavaScript with cross-platform support, allowing shell-like scripting without relying on the system shell.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
child_process0165-410 years agoISC
cross-env06,52620.2 kB17 months agoMIT
execa07,493325 kB165 months agoMIT
node-cmd0285-75 years agoMIT
shell-quote05323.7 kB11a year agoMIT
shelljs014,404152 kB100a year agoBSD-3-Clause

Executing Shell Commands in Node.js: A Practical Guide for Frontend Engineers

When building frontend tooling, CI scripts, or development workflows in Node.js, you’ll often need to run shell commands — whether it’s launching a build, checking Git status, or interacting with system utilities. The JavaScript ecosystem offers several packages for this, each with different trade-offs in safety, cross-platform support, and ease of use. Let’s compare the real-world differences between child_process, cross-env, execa, node-cmd, shell-quote, and shelljs.

⚙️ Core Purpose: What Each Package Actually Does

It’s critical to understand that not all these packages do the same thing. Some execute commands, others just help format them, and one solves a completely different problem.

  • child_process: Built into Node.js. Lets you spawn child processes (e.g., run ls, git, npm). Low-level but powerful.
  • cross-env: Doesn’t run commands at all. It sets environment variables in a way that works on Windows, macOS, and Linux.
  • execa: A user-friendly wrapper around child_process with better defaults, promise support, and cross-platform fixes.
  • node-cmd: A minimal promise-based wrapper for running shell commands via child_process.exec.
  • shell-quote: Doesn’t execute anything. It safely escapes and formats command arguments as shell strings.
  • shelljs: Emulates Unix shell commands (like cp, mkdir, grep) in JavaScript, with cross-platform support.

💡 Key insight: You might use multiple of these together — e.g., shell-quote to safely build a command string, then execa to run it.

🖥️ Running Shell Commands: Direct Execution Compared

Let’s see how each execution-focused package actually runs a command like git log --oneline -5.

Using child_process (Node.js built-in)

You have three methods: spawn, exec, and execFile. spawn is preferred for long-running or large-output processes.

import { spawn } from 'child_process';

const git = spawn('git', ['log', '--oneline', '-5']);

git.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

git.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

git.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

This gives full control but requires manual stream handling and error management.

Using execa

execa wraps child_process with promises, better defaults, and automatic stdout/stderr buffering.

import { execa } from 'execa';

try {
  const { stdout } = await execa('git', ['log', '--oneline', '-5']);
  console.log(stdout);
} catch (error) {
  console.error('Command failed:', error.stderr || error.message);
}

Much cleaner. Also supports shell: true if you need shell features like pipes.

Using node-cmd

A simpler promise wrapper, but only uses child_process.exec (which buffers all output in memory).

import cmd from 'node-cmd';

cmd.get(
  'git log --oneline -5',
  (err, data, stderr) => {
    if (err) {
      console.error('Error:', stderr || err);
      return;
    }
    console.log(data);
  }
);

Note: Uses callbacks by default, though you can promisify it. Also, because it uses exec, it’s risky for commands with large output.

Using shelljs

shelljs doesn’t run arbitrary shell commands by default — it provides JavaScript equivalents of common Unix tools.

import shell from 'shelljs';

// This runs `git log --oneline -5` using shell.exec()
const result = shell.exec('git log --oneline -5', { silent: true });

if (result.code === 0) {
  console.log(result.stdout);
} else {
  console.error(result.stderr);
}

But for many tasks, you’d use its built-ins instead:

// Instead of `cp src dest`, use:
shell.cp('src', 'dest');

// Instead of `mkdir -p build`, use:
shell.mkdir('-p', 'build');

This avoids shell injection risks and works consistently across platforms.

🔒 Safety: Avoiding Shell Injection

Passing untrusted input to shell commands is dangerous. Consider this naive approach:

// DANGEROUS — don't do this!
execa(`grep ${userInput} file.txt`, { shell: true });

If userInput is '; rm -rf /', you’re in trouble.

Safe approaches:

  • Use argument arrays (not shell strings): execa('grep', [userInput, 'file.txt'])
  • Escape inputs with shell-quote when you must build a shell string:
import quote from 'shell-quote';

const safeCmd = quote(['grep', userInput, 'file.txt']);
// Produces: "grep 'safe\ input' file.txt"

await execa(safeCmd, { shell: true });

shelljs avoids this entirely by not using the shell for its built-in commands (cp, ls, etc.). But if you use shell.exec(), you’re back in shell territory — so escape inputs there too.

🌍 Cross-Platform Compatibility

Frontend tooling must work on Windows, macOS, and Linux. Here’s how each package handles it.

child_process

Works everywhere, but you must handle platform differences:

  • Windows uses .exe, .bat, .cmd extensions
  • Path separators differ (\ vs /)
  • Commands like ls don’t exist on Windows

execa

Automatically appends common extensions (.cmd, .bat) on Windows when needed. Also normalizes signals and exit codes.

// Works on Windows and Unix
await execa('npm', ['run', 'build']);

node-cmd

Relies on the system shell, so it inherits all platform quirks. No special handling for Windows.

shelljs

Explicitly designed for cross-platform scripting. Its built-in commands (cp, mv, cat, which, etc.) work identically everywhere.

// This works on Windows, macOS, Linux
shell.cp('src/*.js', 'dist/');

cross-env: The Special Case

This package solves one specific problem: setting environment variables in package.json scripts.

On Unix:

{
  "scripts": {
    "build": "NODE_ENV=production webpack"
  }
}

On Windows, that fails. So you use cross-env:

{
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack"
  }
}

It doesn’t run your main command — it just ensures the env vars are set correctly before handing off to webpack.

You cannot use cross-env to run arbitrary commands or replace execa. It’s a script-line utility, not a general-purpose execution library.

🛠️ When to Use Which Package

Need maximum control or streaming output?

→ Use child_process.spawn directly. Ideal for long-running processes like dev servers.

Building a CLI tool or dev script with clean async/await?

→ Use execa. It’s the best balance of power, safety, and ergonomics.

Just need to copy files, make directories, or check paths?

→ Use shelljs built-ins. Avoids shell entirely and works everywhere.

Must construct a shell command from dynamic parts?

→ Use shell-quote to escape inputs, then pass the string to execa({ shell: true }).

Setting environment variables in npm scripts?

→ Use cross-env — and only for that.

Avoid node-cmd in new projects

While functional, node-cmd hasn’t seen active maintenance in recent years and lacks the safety and feature set of execa. It also uses child_process.exec, which can hang on large outputs. Prefer execa instead.

🧪 Real-World Example: Safe Git Tagging Script

Imagine you want to create a script that tags the current Git commit with a version from package.json.

Unsafe (don’t do this):

// Vulnerable to injection if pkg.version is malicious
execa(`git tag ${pkg.version}`, { shell: true });

Safe with execa + array args:

import { execa } from 'execa';
import { readFileSync } from 'fs';

const pkg = JSON.parse(readFileSync('package.json'));
// Validate version format here if needed

await execa('git', ['tag', pkg.version]);

Safe with shell-quote + shell:

import quote from 'shell-quote';
import { execa } from 'execa';

const cmd = quote(['git', 'tag', pkg.version]);
await execa(cmd, { shell: true });

Both are safe, but the first is simpler and avoids the shell entirely.

📋 Summary Table

PackageExecutes Commands?Cross-PlatformPromise-BasedShell Injection Safe?Primary Use Case
child_process✅ Yes❌ Manual❌ (callbacks/events)✅ With array argsLow-level control, streaming
execa✅ Yes✅ Yes✅ Yes✅ With array argsGeneral-purpose command execution
node-cmd✅ Yes❌ No⚠️ Callbacks❌ (uses shell by default)Legacy/simple scripts (avoid in new code)
shelljs⚠️ Via .exec()✅ Yes❌ (sync by default)✅ For built-ins, ❌ for .exec()Cross-platform file/system ops
shell-quote❌ No✅ Yes❌ N/A✅ YesSafely formatting command strings
cross-env❌ No✅ Yes❌ N/A✅ YesSetting env vars in npm scripts

💡 Final Advice

  • Default to execa for running external commands in modern Node.js tooling.
  • Reach for shelljs when you’re doing file operations (cp, rm, mkdir) — it’s safer and more portable.
  • Always use argument arrays (not shell strings) unless you absolutely need shell features like pipes or globs.
  • Never concatenate untrusted input into shell commands — use shell-quote if you must build a string.
  • Use cross-env only in package.json scripts, not in application code.
  • Avoid node-cmd — it’s outdated and less safe than alternatives.

By understanding what each tool actually does — and where their boundaries lie — you’ll write more secure, maintainable, and cross-platform frontend infrastructure.

How to Choose: child_process vs cross-env vs execa vs node-cmd vs shell-quote vs shelljs

  • child_process:

    Choose child_process when you need low-level control over process spawning, such as streaming large outputs, managing long-running processes, or fine-tuning stdin/stdout/stderr pipes. It's part of Node.js core, so no dependencies are needed, but you'll handle cross-platform quirks and error management manually.

  • cross-env:

    Choose cross-env exclusively for setting environment variables in npm scripts that must run on both Windows and Unix-like systems. It does not execute arbitrary commands and should not be used programmatically in application code — only in package.json script definitions.

  • execa:

    Choose execa for most command execution needs in modern Node.js tooling. It provides a clean promise-based API, automatic cross-platform executable resolution (e.g., .cmd on Windows), safe defaults, and robust error handling while wrapping child_process effectively.

  • node-cmd:

    Avoid node-cmd in new projects. While it offers a simple callback-based interface for running shell commands, it relies on child_process.exec (which buffers all output in memory), lacks active maintenance, and provides no cross-platform safeguards or shell injection protection.

  • shell-quote:

    Choose shell-quote when you must dynamically construct shell command strings from user input or variables and need to escape arguments safely to prevent shell injection. It does not execute commands — pair it with execa({ shell: true }) or similar when shell features are required.

  • shelljs:

    Choose shelljs when performing common file system and shell-like operations (e.g., cp, mkdir, which, grep) in a cross-platform way. Its built-in commands avoid the system shell entirely, making scripts more portable and secure, though its exec() method still requires careful input handling.

README for child_process

Security holding package

This package name is not currently in use, but was formerly occupied by another package. To avoid malicious use, npm is hanging on to the package name, but loosely, and we'll probably give it to you if you want it.

You may adopt this package by contacting support@npmjs.com and requesting the name.