mrkeyoor.com_
Sat 08 Aug 20:58 UTC
npmCLI & Toolingupdated 08 Aug 2026

exec-sh

exec-sh is a CommonJS helper for running shell command strings from Node. It launches `cmd /C` on Windows and `sh -c` elsewhere, inherits the parent terminal by default, can collect stdout and stderr when asked, accepts one command or a semicolon-joined list, returns the ChildProcess from its callback API, and provides a separate Promise interface. It is shell execution made shorter, not a safer replacement for spawning an executable with an argument array.

Verdict

Useful only for trusted, compact shell scripts where terminal forwarding is the feature. For application code, CI helpers that handle dynamic values, or any new TypeScript tool, direct spawn or execa gives clearer and safer control.

API stability4/5The package has stayed on a tiny API: one callback function, a `promise` function, standard spawn options, and a true shorthand for piped stdio. The v0 version number means no formal compatibility promise, but the implementation and README show little churn. The main surprises are semantic rather than changing: arrays mean semicolon-separated commands, and default inherited stdio produces no captured output.
Docs3/5The README documents platform shell selection, inherited versus collected output, callback arguments, the ChildProcess return, and the Promise interface with working examples. It does not put the security consequence of shell strings front and center, explain array failure semantics, cover ChildProcess error events, promise cancellation, TypeScript module interop, output growth, or the many syntax differences between `cmd` and `sh`.
Maintenance2/5The repository is not archived, uses an MIT license, and GitHub currently reports no open issues or pull requests. However, npm 0.4.0 was published in March 2021 and the last repository push was February 2024. With no runtime dependencies there is little dependency upkeep to perform, but the long release gap also means modern Node cancellation, ESM packaging, improved error handling, and updated type precision have not arrived.
Ecosystem3/5The package recorded 3,797,171 downloads for the measured week and interoperates with the standard ChildProcess options rather than inventing a large configuration system. Its own ecosystem is effectively nonexistent: there are no plugins, integrations, or related packages, and its 64 GitHub stars indicate limited direct mindshare. Most usage is likely indirect through older tooling where the small API is already embedded.

Use it if

  • You are maintaining a small CommonJS build script that already works in shell-command strings and needs Windows `cmd` versus Unix `sh` selection
  • You need to launch an interactive command with stdin, stdout, and stderr inherited from the current terminal
  • You want a callback API that returns the ChildProcess so existing code can send signals or inspect its PID
  • Your command text is completely controlled by the application and does not include user, file, branch, URL, or environment-derived values
Skip it if

Setup reality

`npm install exec-sh` adds no runtime dependencies or native build. The package is CommonJS and ships a small declaration file whose default export works most naturally with TypeScript interop settings. Runtime behavior has two modes that are easy to confuse. With no options, stdio defaults to `inherit`: output appears in the terminal, input remains interactive, and callback output strings are empty because nothing is piped. Passing literal `true`, or an options object with `stdio: null`, creates pipes and aggregates stdout and stderr as strings. The callback interface returns a ChildProcess, but the Promise interface returns only `{ stdout, stderr }`, so it gives you no child handle for cancellation. Nonzero exits call back with an Error carrying `code`, or reject the Promise with `code`, `stdout`, and `stderr`. The implementation does not listen for the ChildProcess `error` event; it only catches synchronous exceptions from `spawn()` and handles `close`, which is a thin error model compared with modern runners. Every command is shell text. On Unix it is executed by `/bin/sh`, not necessarily Bash, while Windows uses `cmd.exe`; Bash arrays, `[[ ... ]]`, brace expansion, quoting, redirection, environment assignment, and variable expansion are not portable assumptions. An input array is joined with `;`, so later commands run even if an earlier command fails and only the shell's final exit code is reported. Prefer `spawn(command, args, { shell: false })` or execa whenever values need to be passed as data rather than parsed again as code.

Patterns

Run a trusted command in the current terminalrun-interactive-command

const execSh = require('exec-sh');

const child = execSh('npm init', (error) => {
  if (error) process.exitCode = error.code || 1;
});

console.log('child pid:', child.pid);

Stdio is inherited by default, so the command can read from the terminal but the callback receives empty output strings.

Collect stdout and stderr with a callbackcapture-command-output

execSh('git status --short', true, (error, stdout, stderr) => {
  if (error) {
    console.error(stderr);
    return;
  }
  console.log(stdout);
});

Passing literal `true` changes stdio from inherited streams to pipes. The complete output is retained in memory as strings.

Use the Promise interfaceawait-command-output

const execShPromise = require('exec-sh').promise;

try {
  const { stdout, stderr } = await execShPromise('pwd', true);
  console.log(stdout.trim(), stderr);
} catch (error) {
  console.error(error.code, error.stderr);
}

Rejected errors receive `code`, `stdout`, and `stderr`; the Promise result does not expose the ChildProcess, so it cannot be killed through this API.

Execute inside a chosen directoryset-working-directory

execSh('npm test', {
  cwd: '/srv/project',
  stdio: 'inherit',
}, (error) => {
  if (error) process.exitCode = error.code || 1;
});

The options object is forwarded to `spawn()`. Explicit stdio makes the otherwise implicit inherited-terminal behavior easier to review.

Pass environment variables to the shellextend-command-environment

execSh('node build.js', {
  env: { ...process.env, NODE_ENV: 'production' },
  stdio: 'inherit',
}, done);

Preserve `process.env` so the selected shell and commands retain PATH. Do not interpolate an untrusted value into the command string.

Terminate a callback-style childstop-running-command

const child = execSh('node server.js', { stdio: 'inherit' }, (error) => {
  if (error && error.code !== null) console.error(error.message);
});

setTimeout(() => child.kill('SIGTERM'), 10_000);

Only the callback API returns the ChildProcess. Signal behavior differs on Windows, and killing a shell does not always terminate every process it started.

Run several fixed shell commandsrun-command-sequence

execSh([
  'npm run lint',
  'npm test',
], { stdio: 'inherit' }, (error) => {
  if (error) console.error('final shell exit:', error.code);
});

The array is joined with semicolons, not executed as command and arguments. Later entries run after earlier failures, and only the final shell status is reported.

Stop a trusted Unix sequence after failurestop-sequence-on-failure

execSh('npm run lint && npm test && npm run build', {
  stdio: 'inherit',
}, (error) => {
  if (error) process.exitCode = error.code || 1;
});

This uses POSIX shell syntax and is not a portable Windows command. For cross-platform sequencing, await separate direct process calls in JavaScript.

Read failure output from a rejected Promisehandle-nonzero-exit

try {
  await execSh.promise('node -e "process.stderr.write(\"bad\"); process.exit(2)"', true);
} catch (error) {
  console.error({ code: error.code, stdout: error.stdout, stderr: error.stderr });
}

The error reports the shell's exit code, which may be the final command's status rather than the command you care about in a semicolon-separated sequence.

Use built-in spawn for dynamic valuesspawn-dynamic-arguments-safely

const { spawn } = require('node:child_process');

const child = spawn('git', ['show', '--stat', '--', userBranch], {
  shell: false,
  stdio: 'inherit',
});

This deliberately does not use exec-sh. Dynamic values belong in an argument array so a shell never interprets spaces, quotes, substitutions, or separators inside them.

Alternatives

PackageRegistryPick it when
execanpmChoose it for typed ESM, argument-safe execution, rich errors, cancellation, timeouts, and controlled output collection
zxnpmChoose it for readable shell-style automation with escaped template substitutions and useful scripting helpers
shelljsnpmChoose it when you want portable JavaScript functions for common shell operations rather than platform-specific command syntax
cross-spawnnpmChoose it for Windows-compatible direct process spawning while retaining argument arrays and the complete ChildProcess interface