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):
| You run | Node starts | Notes |
|---|---|---|
|
The file as the main module. |
Everything after the file name belongs to your program, not to Node. |
|
The |
Same rules as for a package entry point. |
|
The string as a script ( |
|
|
A syntax check only ( |
The file is parsed, never executed. |
|
The text on stdin as a script. |
|
|
The REPL. |
|
|
The file, restarted when watched files change. |
Needs a file; see watch mode below. |
|
The command of |
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).
// hello.js
const [, , ...args] = process.argv;
console.log(process.argv.length, args);
console.log(process.argv0, process.execArgv);
$ 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:
$ 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.
// greet.ts
const name: string = process.argv[2] ?? 'world';
console.log(`hello ${name}`);
$ 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 --.
| Element | Value |
|---|---|
|
The absolute path of the |
|
The absolute path of the script. With |
|
Your arguments, as strings, in order. |
|
The original |
|
The Node options that appeared before the script, such as |
The following run shows both rules, plus the -e case where there is no script path:
$ 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.
$ node -e "process.exitCode = 3"
$ echo $?
3
Source: Node.js documentation: process.
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.
| Flag | What it does | Notes |
|---|---|---|
|
Evaluates the argument as JavaScript. |
The built-in modules that are predefined in the REPL ( |
|
Same as |
Cannot be used with ES module input. |
|
Parses the file and exits with 0 or 1 without running it. |
|
|
Interprets |
Since 22.7 and 20.19 syntax detection is on by default, so it is needed only when detection cannot decide. |
$ 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.
$ 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:
-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.
$ 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.
#!/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}`);
$ 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.
Source: Node.js Learn: Run Node.js scripts from the command line and Node.js documentation: util (util.parseArgs).
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:
#!/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));
$ ./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.
// bin/mod.mjs
console.log(import.meta.main, typeof require);
// bin/c.cjs
console.log(require.main === module);
$ 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.
$ 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).
// 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';
greeting.js while node --watch --watch-preserve-output src/app.js runs21: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:
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:
$ 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
| Situation | Behaviour |
|---|---|
|
Not allowed together. |
|
|
|
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 |
|
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.
{
"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"
}
}
$ 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).
Source: Node.js documentation: Command-line API (--run).
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:
$ 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:
| Feature | node --run |
npm run |
|---|---|---|
|
Not run. |
Run automatically. |
Package-manager variables ( |
Not defined. |
Defined. |
|
Not applied to the command that |
Not applicable; load the file in the command itself, for example |
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.
$ 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,pathand the other core modules are loaded on first use (os.platform()above needed norequire). -
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_errorthe last error. Assigning toyourself 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.
> [ '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.
| Command | Effect |
|---|---|
|
Prints this list. |
|
Abandons a multi-line expression you are entering. |
|
Enters editor mode: paste or type several lines, then Ctrl+D to evaluate or Ctrl+C to cancel. |
|
Evaluates a file inside the current session, so its top-level declarations become available. |
|
Writes the lines you have entered in this session to a file. |
|
Closes the REPL. Ctrl+C twice on a blank line does the same. |
> .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:
> 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.
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'));
$ 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
| Option | Purpose |
|---|---|
|
The prompt string; default `> `. |
|
The streams to read and write, and whether to treat them as a terminal. Defaults are |
|
Colours the default writer’s output. |
|
When |
|
A custom evaluation function |
|
A function that formats the result for printing, in place of |
|
Whether to show the inline preview of the completion or result (default |
|
Sloppy or strict evaluation. |
|
Stops the running evaluation on Ctrl+C. |
|
Added in 25.9: customises how uncaught exceptions are handled, including forwarding them as |
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
| Feature | Change | Version |
|---|---|---|
|
Added; finds the nearest |
22.0, 22.3 |
|
Lists the available scripts and exits with a non-zero code. |
26.9 |
Watch mode ( |
Stable. |
22.0 and 20.13; 24.4 and 22.18 |
|
No longer experimental. |
24.10, 22.21 |
Syntax detection for |
On by default ( |
22.7, 20.19 |
Type stripping for |
Enabled by default; no warning from 22.18 and 24.3; Stable from 24.12 and 25.2. |
22.6 to 26.0 |
REPL multi-line editing |
|
24.0, 24.1 |
|
Accepts an options object. |
24.2 |
|
Customises uncaught-exception handling. |
25.9 |
|
Added for ES modules. |
24.2, 22.18 |
|
|
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:
| Topic | Current practice |
|---|---|
|
|
|
|
"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, |
REPL transcripts with a |
Node 24 and later print |
|
|
Compiling TypeScript before running a script |
A |
No text, listing or figure from either book is reproduced here. Their publisher pages are listed in the section bibliography.
Pitfalls
| Mistake | What happens, and the fix |
|---|---|
Putting a Node flag after the script name: |
The flag goes to |
Passing a flag to a |
Node reads it and fails ( |
Expecting |
It does not. Put the flag in the script’s command. |
Using |
|
|
Fails on most systems. Use |
|
|
Forgetting |
|
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.