Running Scripts, the CLI & the REPL

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Node.js 26 (26.10.0, Current, entering LTS on 2026-10-28), with the differences from Node.js 24 (24.21.0, Active LTS) and Node.js 22 (22.23.3, Maintenance) called out, and is current as of 2026-10-03. Every example was run on all three lines; the output shown is real output from Node 26 unless a line is labelled otherwise. Stability, as the official documentation states it: the command-line interface and the node:repl module are Stability 2 (Stable), watch mode has been Stable since Node 22.0 (20.13), --watch-kill-signal is 1.1 (Active development), --import still carries Stability 1 (Experimental) in the CLI reference, and --run carries no stability index.

The node executable is the entry point of everything else in this section. It can run a file, run a string of code, check syntax, restart your program when a file changes, run a script from package.json or open an interactive session. This page covers all of these, and the flags that are specific to each. The full catalogue of flags and environment variables is on CLI Options, Environment & Config File.

What node does when it starts

When you run node, the launcher decides what kind of program it is starting before any of your code runs. The decision is made from the arguments, and each path is a small piece of Node’s own JavaScript (the node:internal/main/* modules, which you can see at the bottom of stack traces):

Table 1. How the node executable picks an entry mode
You run Node starts Notes

node app.js

The file as the main module.

Everything after the file name belongs to your program, not to Node.

node app (a directory)

The main field of app/package.json, or index.js.

Same rules as for a package entry point.

node -e "code" / node -p "code"

The string as a script (eval_string in stack traces).

-p also prints the result.

node -c file.js

A syntax check only (check_syntax in stack traces).

The file is parsed, never executed.

node with piped stdin, or node -

The text on stdin as a script.

- is the explicit spelling of "read the script from stdin".

node in a terminal, or node -i

The REPL.

-i opens it even when stdin is not a terminal.

node --watch app.js

The file, restarted when watched files change.

Needs a file; see watch mode below.

node --run name

The command of scripts.name in the nearest package.json.

Runs a shell command; your JavaScript is not loaded by this Node process.

Running a file

The simplest and most common invocation is node followed by a path. The file is the main module, its exit code becomes the process exit code, and the process ends when the event loop has nothing left to do (see Introduction & Architecture for the loop).

A script that reports how it was started
// hello.js
const [, , ...args] = process.argv;
console.log(process.argv.length, args);
console.log(process.argv0, process.execArgv);
Running it with arguments, and with a Node option
$ node hello.js a --b=1 -c "d e"
6 [ 'a', '--b=1', '-c', 'd e' ]
node []

$ node --no-warnings hello.js x
3 [ 'x' ]
node [ '--no-warnings' ]

Source: Node.js documentation: process (process.argv, process.argv0, process.execArgv).

Whether a .js file is treated as CommonJS or as an ES module is decided by the nearest package.json "type" field, and, when there is none, by looking at the source. Node 22.7 and later (and every later line) detect ES module syntax in an ambiguous file and run it as ESM; this is why a file with import statements and no "type" field just works on Node 22, 24 and 26. Setting "type": "module" explicitly is still the better practice, because it is faster and does not depend on detection. The module systems are covered in CommonJS Modules and ECMAScript Modules, and the language side in JavaScript modules.

A directory is a valid entry point. Node reads its package.json and starts the main file, or index.js when there is none:

Running a directory
$ cat app/package.json
{"main":"main.js"}
$ node app
dir entry app

Running TypeScript files

Since Node 22.18 and 24.3 the entry point may be a .ts file with no flag and no warning, because type stripping is enabled by default (Stable in Node 24.12 and 25.2 and later). It only erases type annotations; it does not read tsconfig.json and does not support syntax that needs code generation. Details and the limits are on TypeScript in Node.js, and the language itself on TypeScript getting started.

A TypeScript entry point
// greet.ts
const name: string = process.argv[2] ?? 'world';
console.log(`hello ${name}`);
Running it directly
$ node greet.ts Ada
hello Ada

This printed the same output on Node 26, 24 and 22.23.3. Earlier 22.x releases (before 22.18) needed --experimental-strip-types.

Script arguments and process.argv

Where an option appears decides who owns it. Options placed before the script name are read by Node; everything after the script name is passed to your program untouched, including things that look like Node options and a literal --.

Table 2. How process.argv is built
Element Value

process.argv[0]

The absolute path of the node executable that is running (process.execPath).

process.argv[1]

The absolute path of the script. With -e/-p or the REPL there is no script, so the first user argument takes index 1.

process.argv.slice(2)

Your arguments, as strings, in order.

process.argv0

The original argv[0] as typed, for example node.

process.execArgv

The Node options that appeared before the script, such as --no-warnings.

The following run shows both rules, plus the -e case where there is no script path:

Arguments after the script belong to the script
$ node hello.js -- --x
4 [ '--', '--x' ]

$ node -e "console.log(process.argv)" a b
[
  '/Users/me/.nvm/versions/node/v26.10.0/bin/node',
  'a',
  'b'
]

Do not parse process.argv by hand beyond trivial cases. util.parseArgs() (Stable) turns process.argv.slice(2) into values and positionals, validates unknown flags and supports short and boolean options. It is used in the executable-script example below, and it is the subject of The util Module and Building CLI Tools. Exit codes, including process.exitCode, are covered on Process, Signals & Exit Codes.

Setting the exit code without ending the process abruptly
$ node -e "process.exitCode = 3"
$ echo $?
3

Inline code: -e, -p and -c

Three flags run or check code that is not in a file. They are the quickest way to inspect the runtime, and they are the building blocks of shell one-liners in CI scripts.

Table 3. Inline code flags
Flag What it does Notes

-e, --eval "script"

Evaluates the argument as JavaScript.

The built-in modules that are predefined in the REPL (fs, os, path and so on) are available without require. Type annotations work since 22.6 (experimental then, stable with type stripping).

-p, --print "script"

Same as -e, and prints the value of the last expression.

Cannot be used with ES module input.

-c, --check

Parses the file and exits with 0 or 1 without running it.

--require is honoured when checking since v10.0.0.

--input-type=type

Interprets -e or stdin input as commonjs, module, module-typescript or commonjs-typescript.

Since 22.7 and 20.19 syntax detection is on by default, so it is needed only when detection cannot decide.

Evaluating, printing and checking
$ node -e "console.log(1+1)"
2
$ node -p "process.versions.node"
26.10.0
$ node -p "[1,2,3].map(x=>x*2)"
[ 2, 4, 6 ]
$ node -p "os.platform() + ' ' + typeof fs.readFileSync"
darwin function
$ node --print --eval=-42
-42

A script that starts with - must be attached with =, as in the last line, or it is read as an option. Quote the script for your shell: cmd.exe understands only double quotes, while PowerShell and Git Bash accept both.

A syntax check, with exit codes
$ echo 'const x = ;' > bad.js
$ node -c bad.js
/path/to/bad.js:1
const x = ;
          ^

SyntaxError: Unexpected token ';'
    at checkSyntax (node:internal/main/check_syntax:84:5)

Node.js v26.10.0
$ echo $?
1
$ node -c hello.js; echo $?
0

Source: Node.js documentation: Command-line API (-e, -p, -c, --input-type).

ESM in one-liners

Code passed with -e is detected as CommonJS first, then as ESM, so a one-liner can use import or top-level await directly. --print cannot print an ES module’s result, and says so with a specific error code:

ESM with -e, and the -p limitation
$ node --input-type=module -e "import os from 'node:os'; console.log(typeof os.cpus)"
function

$ node --input-type=module -p "1"
node:internal/process/execution:77
    throw new ERR_EVAL_ESM_CANNOT_PRINT();
    ^

Error [ERR_EVAL_ESM_CANNOT_PRINT]: --print cannot be used with ESM input

Source: Node.js documentation: Command-line API (--input-type, --print).

Reading the script from stdin

With no file name, or with -, Node reads the program from stdin when stdin is not a terminal. Arguments after the - land in process.argv as usual. This is how a here-document or a generated script can be run without a temporary file.

Piping a script into node
$ echo "console.log('stdin:', process.argv.slice(2))" | node - a b
stdin: [ 'a', 'b' ]

Shebangs and executable scripts

A script becomes a command when its first line is a shebang and the file has the execute permission. The portable form is #!/usr/bin/env node, which finds node on the PATH instead of hard-coding where it is installed (so version managers keep working). Node accepts and ignores a shebang line, so the same file also runs as node bin/shelf.

An executable script with argument parsing
#!/usr/bin/env node
import { parseArgs } from 'node:util';

const { values, positionals } = parseArgs({
  allowPositionals: true,
  options: { json: { type: 'boolean', short: 'j' } },
});
const [command = 'help'] = positionals;
console.log(values.json ? JSON.stringify({ command }) : `command: ${command}`);
Making it executable and running it
$ chmod +x bin/shelf
$ ./bin/shelf list -j
{"command":"list"}
$ ./bin/shelf
command: help

The file above has no extension and no package.json "type" field, and it uses import. It ran on Node 26, 24 and 22.23.3 because of syntax detection; in a real package put "type": "module" in package.json or give the file an .mjs extension.

Passing Node flags from a shebang

Most operating systems pass everything after the interpreter on a shebang line as a single argument, so #!/usr/bin/env node --some-flag does not work. Use the -S option of GNU and BSD env, which splits the string:

A shebang that passes a Node flag
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
// bin/env-s (no extension; made executable with chmod +x)
console.log(process.execArgv, process.argv.slice(2));
Running it
$ ./bin/env-s a b
[ '--disable-warning=ExperimentalWarning' ] [ 'a', 'b' ]

env -S is not available on every system (it is on current macOS and Linux distributions), and Windows ignores shebangs altogether; npm creates small launcher shims for the bin entries of installed packages there. Publishing a command line tool with a bin field is part of Building CLI Tools.

Source: Node.js documentation: Command-line API (--disable-warning).

Am I the entry point?

A file that is both a module and a script needs to know which role it is playing. In ES modules use import.meta.main (added in Node 24.2 and 22.18); in CommonJS compare require.main with module.

Detecting the main module in ESM and CommonJS
// bin/mod.mjs
console.log(import.meta.main, typeof require);

// bin/c.cjs
console.log(require.main === module);
Running them
$ node bin/mod.mjs
true undefined
$ node bin/c.cjs
true

Source: Node.js documentation: ES modules (import.meta.main) and Node.js documentation: CommonJS modules (require.main).

Preloading modules: -r and --import

--require (-r) and --import run a module before the main script, in the main thread and in worker threads, forked processes and cluster workers. Use them for instrumentation, polyfills, loading configuration or registering hooks. -r follows require() rules, and since Node 22.12 (20.19) it can load an ES module; --import follows ES module resolution. All -r modules run before all --import modules.

Preload order
$ echo "globalThis.preloaded = 'cjs'; console.log('-r ran')" > pre.cjs
$ echo "console.log('--import ran')" > pre.mjs
$ echo "console.log('main', globalThis.preloaded)" > main.mjs
$ node -r ./pre.cjs --import ./pre.mjs main.mjs
-r ran
--import ran
main cjs

The output was identical on Node 26, 24 and 22. NODE_OPTIONS can carry both flags for a whole process tree, which is the subject of CLI Options, Environment & Config File.

Source: Node.js documentation: Command-line API (--require, --import).

Watch mode: --watch and --watch-path

node --watch runs the program and restarts it when a watched file changes. It replaces nodemon for the common case. By default it watches the entry point and every module the program `require`s or `import`s. It became Stable in Node 22.0 (20.13).

A program with two modules
// src/app.js
import { greeting } from './greeting.js';
console.log(`${new Date().toISOString().slice(11, 19)} ${greeting}`);
setInterval(() => {}, 1000);

// src/greeting.js
export const greeting = 'hello';
Editing greeting.js while node --watch --watch-preserve-output src/app.js runs
21:06:03 hello
Change detected in '/path/to/src/greeting.js'
Restarting 'src/app.js'
21:06:05 bye

--watch-preserve-output stops Node from clearing the terminal at each restart, so history stays visible. A crash does not end the session either; the process waits for the next change:

A failing script is restarted after it is fixed
starting
file:///path/crash.js:2
throw new Error('boom');
      ^

Error: boom
    ...
Node.js v26.10.0
Failed running 'crash.js'. Waiting for file changes before restarting...
Change detected in './crash.js'
Restarting 'crash.js'
starting
fixed
Completed running 'crash.js'. Waiting for file changes before restarting...

Source: Node.js documentation: Command-line API (--watch, --watch-preserve-output).

Choosing what to watch

--watch-path takes the paths to watch and implicitly enables watch mode. It switches off the automatic watching of imported modules, even when combined with --watch, so you must list everything you care about. It can be given several times. In this run only config/ is watched; the change to src/greeting.js is ignored until a watched file changes and forces a restart, which then loads the new module:

Watching a directory only
$ node --watch-path=./config --watch-preserve-output src/app.js
21:06:07 bye
# edit src/greeting.js  -> nothing happens
# edit config/settings.txt
Change detected in '/path/to/config/settings.txt'
Restarting 'src/app.js'
21:06:11 ignored

Per the CLI reference --watch-path is supported on macOS and Windows only, and throws ERR_FEATURE_UNAVAILABLE_ON_PLATFORM elsewhere; the run above is from macOS. Plain --watch (entry point and imports) works everywhere.

Source: Node.js documentation: Command-line API (--watch-path).

Watch mode limits

Table 4. What watch mode does not do
Situation Behaviour

--watch with --check, --eval, --interactive or the REPL

Not allowed together.

--watch with --run or inline script input

--run takes precedence and watch mode is ignored. Watch mode needs a file path.

--watch-kill-signal=SIGINT

Chooses the signal sent to the old process on restart (added 24.4 and 22.18; Stability 1.1, Active development).

Files that are not imported

A text file, a JSON file read with fs, or an .env file is not tracked unless it is listed with --watch-path.

node --test --watch

The test runner has its own watch mode (see The Test Runner).

Running package scripts with node --run

node --run name runs the command in scripts.name of the nearest package.json, without starting npm. It was added in Node 22.0 and is meant to be fast for the common case: a few direct commands such as node server.js or tsc --noEmit.

A package.json with scripts
{
  "name": "bookshelf-scripts",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js",
    "check": "node --check server.js",
    "hello": "echo hello from $NODE_RUN_SCRIPT_NAME",
    "args": "node args.js",
    "prehello": "echo never runs"
  }
}
Running scripts, passing arguments and finding the nearest package.json
$ node --run hello
hello from hello

$ node --run args -- --verbose a
[ '--verbose', 'a' ] package.json

$ (cd sub && node --run hello)
hello from hello

Arguments after -- are appended to the script’s command. Without the --, Node would read --verbose itself and fail with bad option: --verbose. The search for package.json walks up from the current directory, the command runs in the directory that holds that file, and ./node_modules/.bin of each ancestor is prepended to PATH, so installed tools such as tsc or vitest are found. Two variables are set for the command: NODE_RUN_SCRIPT_NAME and NODE_RUN_PACKAGE_JSON_PATH (both added in 22.3).

Listing and failing

From Node 26.9, node --run without a name lists the scripts and exits non-zero. A missing script prints the same list after the error:

Listing scripts, and a missing script
$ node --run
Available scripts are:
  start: node server.js
  dev: node --watch server.js
  check: node --check server.js
  hello: echo hello from $NODE_RUN_SCRIPT_NAME
  args: node args.js
  prehello: echo never runs
$ echo $?
9

$ node --run nope
Missing script: "nope" for /path/to/package.json

Available scripts are:
  ...
$ echo $?
1

On Node 24.21 and 22.23.3 the first command only prints node: --run requires an argument and exits with 9, so do not rely on the listing in scripts that must run on older lines.

Source: Node.js documentation: Command-line API (--run, history).

How node --run differs from npm run

It is intentionally smaller. The differences below were measured, not just read from the reference:

Table 5. node --run compared with npm run
Feature node --run npm run

pre and post scripts

Not run. prehello above never printed.

Run automatically.

Package-manager variables (npm_package_*, npm_config_*)

Not defined.

Defined.

--env-file

Not applied to the command that --run starts: node --env-file=.env --run showenv printed undefined for PORT, while PORT=1 node --run showenv printed 1.

Not applicable; load the file in the command itself, for example "start": "node --env-file=.env server.js".

Features beyond running one script

Intentionally not provided, per the reference.

Workspaces and other npm features are available.

Startup cost

None beyond Node itself.

Starts the npm CLI first.

A script that needs .env should therefore carry the flag in its own command: "start": "node --env-file=.env server.js". Using node --run instead of npm-run-all for sequences is possible by chaining with the shell (&&), but there is no parallel runner; keep a package manager for that.

Source: Node.js documentation: Command-line API (--run, Intentional limitations).

The REPL

Running node with no script in a terminal (or node -i anywhere) starts the REPL (Read-Eval-Print Loop), an interactive session that evaluates each line, prints the value, and keeps your variables. It is implemented by the node:repl module, which the node binary uses directly, so the same machinery that you drive from a terminal can be embedded in your own program (next section). It is a fast way to try an API, to inspect objects, to test a regular expression, or to poke a running application.

The default session
$ node
Welcome to Node.js v26.10.0.
Type ".help" for more information.
> const shelf = ['Dune', 'Emma']
undefined
> shelf.length
2
> _
2
> _ * 10
20
> os.platform()
'darwin'
> function add(a, b) {
| return a + b
| }
undefined
> add(2, 3)
5

Four behaviours are worth knowing:

  • Built-in modules are global. Unless you declared the name yourself, os, fs, path and the other core modules are loaded on first use (os.platform() above needed no require).

  • Multi-line input works. A line that is incomplete continues on the next, with a | prompt in Node 24 and later (…​ in Node 22); since Node 24 you can also edit earlier lines of a multi-line entry and recover a multi-line command with a syntax error from history. The multi-line indicator was changed in v24.0.0.

  • holds the last result and _error the last error. Assigning to yourself switches the behaviour off for the rest of the session, as the next transcript shows.

  • The REPL catches uncaught exceptions and prints them as Uncaught …​, so a thrown error never ends the session.

The underscore variables
> [ 'a', 'b', 'c' ]
[ 'a', 'b', 'c' ]
> _.length
3
> _ += 1
Expression assignment to _ now disabled.
4
> 1 + 1
2
> _
4
> nope
Uncaught ReferenceError: nope is not defined
> _error.message
'nope is not defined'

Source: Node.js documentation: REPL (Assignment of the _ variable).

REPL commands

Commands begin with a dot and are not JavaScript. .help lists them; a custom REPL can add more.

Table 6. The default dot-commands
Command Effect

.help

Prints this list.

.break (or Ctrl+C)

Abandons a multi-line expression you are entering. .clear is its alias in the default REPL; in a REPL created with a custom context it also resets the context to an empty object.

.editor

Enters editor mode: paste or type several lines, then Ctrl+D to evaluate or Ctrl+C to cancel.

.load file

Evaluates a file inside the current session, so its top-level declarations become available.

.save file

Writes the lines you have entered in this session to a file.

.exit (or Ctrl+D)

Closes the REPL. Ctrl+C twice on a blank line does the same.

Editor mode
> .editor
// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)
function total(prices) {
  return prices.reduce((sum, p) => sum + p, 0);
}

total([5, 7.5])

12.5

.load and .save make the REPL a scratchpad. Given a helper file, the load is equivalent to typing it:

.load and .save
// helpers.js
const titles = ['Dune', 'Emma', 'Ulysses'];
function longest(list) {
  return list.reduce((a, b) => (b.length > a.length ? b : a));
}
> .load helpers.js
> longest(titles)
'Ulysses'
> .save session.js
Session saved to: session.js

The saved file contains exactly the lines you entered (without the output), which makes it a quick way to turn an experiment into a script. Keys: Tab completes a name (on an empty line it lists the globals and the variables in scope), Ctrl+R searches the history backwards and Ctrl+S forwards, and the up arrow recalls earlier lines. The history is persisted in ~/.node_repl_history; NODE_REPL_HISTORY chooses another file (an empty string turns persistence off) and NODE_REPL_HISTORY_SIZE sets the number of lines kept (default 1000). NODE_REPL_MODE selects sloppy (default) or strict.

Source: Node.js documentation: REPL (Commands and special keys, Environment variable options).

Top-level await in the REPL

await works at the top level of the REPL on all three lines, and is controlled by --no-experimental-repl-await (added 16.6). That makes the REPL ideal for trying promise APIs, including fetch:

Awaiting in the REPL
> const { setTimeout: sleep } = require('node:timers/promises')
undefined
> await sleep(20).then(() => 'done')
'done'
> const res = await fetch('data:text/plain,hi'); await res.text()
'hi'

The docs list one known limitation: a const declared through an await expression loses its lexical protection against being assigned again, but it still cannot be declared twice:

const with await
> const total = await Promise.resolve(3)
undefined
> total = await Promise.resolve(4)
4
> const total = 5
Uncaught SyntaxError: Identifier 'total' has already been declared

Source: Node.js documentation: REPL (await keyword).

A custom REPL with node:repl

repl.start() returns a REPLServer, an event emitter you can configure and extend. Exposing your application’s objects on context makes them globals in the session, so a REPL becomes a debugging console for your own program, in the way Rails or Django consoles are used.

A Bookshelf console
import repl from 'node:repl';
import { homedir } from 'node:os';
import { join } from 'node:path';

const books = [
  { id: 1, title: 'Dune', year: 1965 },
  { id: 2, title: 'Emma', year: 1815 },
];

const server = repl.start({ prompt: 'shelf> ', useColors: true });

// Everything on `context` becomes a global inside the session.
server.context.books = books;
server.context.byId = (id) => books.find((b) => b.id === id);

// `.stats` is a custom dot-command.
server.defineCommand('stats', {
  help: 'Print the number of books on the shelf',
  action() {
    this.clearBufferedCommand();
    console.log(`${books.length} books`);
    this.displayPrompt();
  },
});

// Persist history between sessions.
server.setupHistory(join(homedir(), '.shelf_repl_history'), (err) => {
  if (err) console.error('history unavailable:', err.message);
});

server.on('exit', () => console.log('bye'));
Using it
$ node shelf-repl.js
shelf> byId(2)
{ id: 2, title: 'Emma', year: 1815 }
shelf> books.map(b => b.title)
[ 'Dune', 'Emma' ]
shelf> .stats
2 books
shelf> .help
.break    Sometimes you get stuck, this gets you out
.clear    Break, and also clear the local context
.editor   Enter editor mode
.exit     Exit the REPL
.help     Print this help message
.load     Load JS from a file into the REPL session
.save     Save all evaluated commands in this REPL session to a file
.stats    Print the number of books on the shelf
shelf> .exit
bye

The history file received .help, .stats, books.map(b ⇒ b.title) and byId(2), newest first. The example ran unchanged on Node 26, 24 and 22. A command’s action runs with this bound to the REPLServer; call displayPrompt() when it finishes, and clearBufferedCommand() first so a half-typed multi-line statement is not mixed with it.

Source: Node.js documentation: REPL (repl.start(), replServer.defineCommand(), replServer.setupHistory()).

repl.start() options

Table 7. The options that matter in practice
Option Purpose

prompt

The prompt string; default `> `.

input, output, terminal

The streams to read and write, and whether to treat them as a terminal. Defaults are process.stdin and process.stdout.

useColors

Colours the default writer’s output.

useGlobal

When true, evaluates in the real global context rather than a fresh one.

eval

A custom evaluation function (cmd, context, filename, callback), for example to accept a different language.

writer

A function that formats the result for printing, in place of util.inspect().

preview

Whether to show the inline preview of the completion or result (default true).

replMode

Sloppy or strict evaluation.

breakEvalOnSigint

Stops the running evaluation on Ctrl+C.

handleError

Added in 25.9: customises how uncaught exceptions are handled, including forwarding them as uncaughtException by returning 'unhandled'.

setupHistory() takes a file path, or, from Node 24.2, an object with filePath, size, removeHistoryDuplicates and onHistoryFileLoaded.

Source: Node.js documentation: REPL (repl.start([options])).

A REPL is remote code execution

A REPL evaluates whatever it receives with the privileges of your process. The API can attach a REPLServer to a socket (the reference shows a net.Server example), but do that only on a Unix domain socket or a loopback address that is protected by file permissions or a tunnel, never on a public interface, and never in a production service that nobody has hardened. For debugging a running production process prefer the Inspector (see Debugging).

Source: Node.js documentation: REPL (Full-featured "terminal" REPL over net.Server and net.Socket).

What changed recently

Table 8. Changes relevant to this page
Feature Change Version

node --run

Added; finds the nearest package.json and sets PATH and the two NODE_RUN_* variables (22.3).

22.0, 22.3

node --run with no argument

Lists the available scripts and exits with a non-zero code.

26.9

Watch mode (--watch, --watch-path)

Stable. --watch-kill-signal added (Active development).

22.0 and 20.13; 24.4 and 22.18

--env-file, --env-file-if-exists

No longer experimental.

24.10, 22.21

Syntax detection for -e and stdin

On by default (--input-type rarely needed).

22.7, 20.19

Type stripping for .ts entry points and -e

Enabled by default; no warning from 22.18 and 24.3; Stable from 24.12 and 25.2. --experimental-transform-types was removed in 26.0.

22.6 to 26.0

REPL multi-line editing

| replaces …​ as the continuation prompt; multi-line history; edit lines of a multi-line command.

24.0, 24.1

replServer.setupHistory()

Accepts an options object.

24.2

repl.start() handleError

Customises uncaught-exception handling.

25.9

import.meta.main

Added for ES modules.

24.2, 22.18

-r loads ES modules

require() of ESM no longer needs a flag.

22.12, 20.19

Where the books are outdated

Two O’Reilly books cover this material: Efficient Node.js by Samer Buna (2025), whose second chapter is about scripts and modules, the CLI and the REPL, and Node.js Projects by Jonathan Wexler (2025), which relies on the command line throughout. They are good companions, with these points of drift as of Node 26:

Table 9. Where the books are outdated
Topic Current practice

nodemon for restarts

node --watch is Stable since Node 22.0 and needs no dependency. Use --watch-path for directories that the program does not import.

npm-run-all and similar script runners

node --run runs a script from package.json without starting npm, though it has no pre and post scripts and no package-manager variables.

"20.x or higher" and "18+" baselines

Node 18 and 20 are end of life. Write against Node 22, 24 or 26, where syntax detection, require(esm), --env-file and type stripping are available without flags.

REPL transcripts with a …​ continuation prompt

Node 24 and later print |; the sessions behave the same, but the output text differs.

dotenv for loading .env

--env-file and process.loadEnvFile() are part of Node; remember that node --run does not forward --env-file to the command it starts.

Compiling TypeScript before running a script

A .ts file can be the entry point from Node 22.18 and 24.3; ts-node is rarely needed.

No text, listing or figure from either book is reproduced here. Their publisher pages are listed in the section bibliography.

Pitfalls

Table 10. Common mistakes with the CLI and the REPL
Mistake What happens, and the fix

Putting a Node flag after the script name: node app.js --watch

The flag goes to process.argv and is ignored by Node. Put Node options first: node --watch app.js.

Passing a flag to a --run script without --

Node reads it and fails (bad option). Use node --run name — --flag.

Expecting --env-file to reach a --run command

It does not. Put the flag in the script’s command.

Using -p with ES module syntax

ERR_EVAL_ESM_CANNOT_PRINT. Use -e and console.log.

#!/usr/bin/env node --flag

Fails on most systems. Use #!/usr/bin/env -S node --flag.

--watch with --watch-path, expecting imports to be watched too

--watch-path disables automatic import watching. List the paths you need.

Forgetting chmod +x

Permission denied for ./bin/tool; node bin/tool still works.

Exposing a REPL on a public port

Anyone who connects can run code as your user. Do not.

Related pages: Installing Node.js for getting node on your machine, Release Lines & LTS for choosing a line, Globals & Web Platform APIs for what exists without an import, and NestJS debugging for the same flags used on a Nest app.