What is the problem this feature will solve?
Please add first-class pseudo-terminal (PTY) support to child_process.spawn()
(and ideally spawnSync()), so child processes see a real TTY on stdout/stderr/stdin
instead of pipes.
Today, the only practical option is third-party native addons such as
node-pty. That works, but it adds
native compilation, Electron/ABI friction, and an extra dependency for a capability
that shells and other runtimes expose natively.
Built-in PTY support would make Node.js a better cross-platform choice for
developer tools, CI runners, test harnesses, and build orchestrators compared
with wrapping PowerShell or Python just to get a TTY.
Problem
When spawn() uses the default stdio: ['pipe', 'pipe', 'pipe'], the child's
stdout/stderr are not TTYs. Many programs behave differently:
- Block buffering instead of line buffering (bash, make, gcc, pacman, etc.)
isatty() / process.stdout.isTTY is false (no ANSI colors, different log format)
- Interactive prompts break (password prompts,
read, pagers)
- Progress output is delayed until buffers fill or the process exits
Workarounds are incomplete:
stdbuf -oL -eL only affects some libc-buffered programs; grandchild processes
(e.g. make jobs) may still block-buffer.
script -q -f allocates a PTY but adds Script started/done noise to logs.
- Redirecting to a file (
>log.txt) inside the shell has the same non-TTY behavior.
Concrete use case
I maintain a cross-platform bootstrap/build runner (Windows + MSYS/Cygwin) written
in TypeScript/Node.js because it is faster and simpler to ship than PowerShell or
Python for this use case.
The runner uses spawn(bash, ['--login', '-c', script]) and tees output to both
the terminal and a log file. Without a PTY:
- The terminal shows almost nothing for minutes ("stuck" UX).
- The log file updates in large bursts, not line-by-line.
- Some toolchain steps change behavior under non-TTY conditions.
We currently wrap commands in stdbuf -oL -eL and manually tee data events.
That helps a little but is fragile and platform-dependent. A PTY would fix the
root cause.
Minimal desired behavior:
import { spawn } from 'node:child_process';
import { createWriteStream } from 'node:fs';
const log = createWriteStream('build.log');
const child = spawn('bash', ['--login', '-c', 'make -j8'], {
cwd: repoRoot,
env: process.env,
stdio: ['pipe', 'pty', 'pty'], // or a dedicated option; see below
});
child.on('data', (chunk) => {
log.write(chunk);
process.stdout.write(chunk);
});
The child should believe it has a terminal (isatty(1) === true), use
line-oriented output, and flush promptly.
Why not node-pty?
node-pty is widely used (VS Code, etc.)
and I am grateful it exists, but for application-level build/CI tooling it has
drawbacks:
- Native addon: requires node-gyp/prebuilds; breaks or needs rebuilds across
Node/Electron ABI changes.
- Extra dependency for something that feels like core process/spawn behavior.
- API divergence from
child_process.spawn() (different return type, resize
events, etc.).
For terminal emulators, node-pty is fine. For "run this shell script and stream
output live to console + log", PTY belongs in core next to spawn.
Proposed API (sketch)
Option A - extend stdio:
spawn(cmd, args, {
stdio: ['pipe', 'pty', 'pty'],
pty: {
cols: process.stdout.columns ?? 80,
rows: process.stdout.rows ?? 24,
name: 'xterm-256color',
},
});
Option B - dedicated flag:
spawn(cmd, args, {
pty: true,
stdio: ['pipe', 'pipe', 'pipe'],
});
Requirements:
- Works on Linux, macOS, Windows 10+ (ConPTY).
- Document graceful failure on older Windows (throw or fallback to pipes).
- Optional
resize(cols, rows) on the child handle.
- PTY stream emits
data events; stdin remains writable for automation.
- Document interaction with
shell: true, detached, windowsHide.
Platform notes
- POSIX:
forkpty(3) / openpty + session setup (Node already has test helpers in
test/pseudo-tty/pty_helper.py).
- Windows:
CreatePseudoConsole() (ConPTY). Older Windows may need documented
unsupported behavior rather than winpty-level emulation in core.
Prior art / related issues
This request is blocked on libuv PTY landing first; once libuv exposes
spawn-with-PTY, please expose it through child_process.
Why this matters for Node.js
Node is already the default for cross-platform CLI tooling (yarn, vite, eslint,
etc.). Live, faithful subprocess output is a basic expectation for:
- build systems and monorepo orchestrators
- test runners
- dev environment bootstrap scripts
- CI log streaming
Without PTY, authors either accept broken UX, add native deps, or shell out to
PowerShell/Python/bash script hacks. Native PTY would close that gap and reduce
reliance on node-pty for non-terminal-emulator use cases.
What is the feature you are proposing to solve the problem?
Add optional pseudo-terminal (PTY) support to child_process.spawn() (and
spawnSync()) so a spawned child can use a real TTY instead of pipes.
Proposed API (either shape is fine):
spawn(cmd, args, {
stdio: ['pipe', 'pty', 'pty'],
pty: { cols, rows, name: 'xterm-256color' },
});
or:
spawn(cmd, args, { pty: true, stdio: ['pipe', 'pipe', 'pipe'] });
Behavior:
- Child sees isatty(stdout/stderr) === true (line-buffered output, colors,
interactive prompts work as in a real terminal).
- Parent gets a readable/writable PTY stream (emit 'data', write stdin).
- Optional child.resize(cols, rows) for terminal size changes.
- Linux/macOS via POSIX PTY; Windows 10+ via ConPTY; document unsupported
fallback on older Windows.
This should be implemented on top of libuv PTY support (libuv#2640,
libuv PR#4802) and exposed through child_process, similar to how pipe
and inherit stdio modes work today.
What alternatives have you considered?
No response
What is the problem this feature will solve?
Please add first-class pseudo-terminal (PTY) support to
child_process.spawn()(and ideally
spawnSync()), so child processes see a real TTY on stdout/stderr/stdininstead of pipes.
Today, the only practical option is third-party native addons such as
node-pty. That works, but it adds
native compilation, Electron/ABI friction, and an extra dependency for a capability
that shells and other runtimes expose natively.
Built-in PTY support would make Node.js a better cross-platform choice for
developer tools, CI runners, test harnesses, and build orchestrators compared
with wrapping PowerShell or Python just to get a TTY.
Problem
When
spawn()uses the defaultstdio: ['pipe', 'pipe', 'pipe'], the child'sstdout/stderr are not TTYs. Many programs behave differently:
isatty()/process.stdout.isTTYis false (no ANSI colors, different log format)read, pagers)Workarounds are incomplete:
stdbuf -oL -eLonly affects some libc-buffered programs; grandchild processes(e.g.
makejobs) may still block-buffer.script -q -fallocates a PTY but addsScript started/donenoise to logs.>log.txt) inside the shell has the same non-TTY behavior.Concrete use case
I maintain a cross-platform bootstrap/build runner (Windows + MSYS/Cygwin) written
in TypeScript/Node.js because it is faster and simpler to ship than PowerShell or
Python for this use case.
The runner uses
spawn(bash, ['--login', '-c', script])and tees output to boththe terminal and a log file. Without a PTY:
We currently wrap commands in
stdbuf -oL -eLand manually teedataevents.That helps a little but is fragile and platform-dependent. A PTY would fix the
root cause.
Minimal desired behavior:
The child should believe it has a terminal (
isatty(1) === true), useline-oriented output, and flush promptly.
Why not node-pty?
node-pty is widely used (VS Code, etc.)
and I am grateful it exists, but for application-level build/CI tooling it has
drawbacks:
Node/Electron ABI changes.
child_process.spawn()(different return type, resizeevents, etc.).
For terminal emulators,
node-ptyis fine. For "run this shell script and streamoutput live to console + log", PTY belongs in core next to
spawn.Proposed API (sketch)
Option A - extend
stdio:Option B - dedicated flag:
Requirements:
resize(cols, rows)on the child handle.dataevents; stdin remains writable for automation.shell: true,detached,windowsHide.Platform notes
forkpty(3)/ openpty + session setup (Node already has test helpers intest/pseudo-tty/pty_helper.py).CreatePseudoConsole()(ConPTY). Older Windows may need documentedunsupported behavior rather than winpty-level emulation in core.
Prior art / related issues
This request is blocked on libuv PTY landing first; once libuv exposes
spawn-with-PTY, please expose it through
child_process.Why this matters for Node.js
Node is already the default for cross-platform CLI tooling (yarn, vite, eslint,
etc.). Live, faithful subprocess output is a basic expectation for:
Without PTY, authors either accept broken UX, add native deps, or shell out to
PowerShell/Python/bash
scripthacks. Native PTY would close that gap and reducereliance on
node-ptyfor non-terminal-emulator use cases.What is the feature you are proposing to solve the problem?
Add optional pseudo-terminal (PTY) support to child_process.spawn() (and
spawnSync()) so a spawned child can use a real TTY instead of pipes.
Proposed API (either shape is fine):
spawn(cmd, args, {
stdio: ['pipe', 'pty', 'pty'],
pty: { cols, rows, name: 'xterm-256color' },
});
or:
spawn(cmd, args, { pty: true, stdio: ['pipe', 'pipe', 'pipe'] });
Behavior:
interactive prompts work as in a real terminal).
fallback on older Windows.
This should be implemented on top of libuv PTY support (libuv#2640,
libuv PR#4802) and exposed through child_process, similar to how pipe
and inherit stdio modes work today.
What alternatives have you considered?
No response