Feature request: PTY support in `child_process.spawn()`
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 122k
- Forks
- 37.3k
- Avg merge
- 4d 2h
- Merged PRs (30d)
- 283
Description
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.isTTYis 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 -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.- 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
dataevents; 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
- libuv feature request: https://github.com/libuv/libuv/issues/2640
- libuv PTY implementation PR: https://github.com/libuv/libuv/pull/4802
- Node issue (closed, blocked on libuv): https://github.com/nodejs/node/issues/46105
- Related: https://github.com/nodejs/node/issues/31409
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the child_process.spawn() entry point and review test/pseudo-tty/pty_helper.py, then check the referenced libuv issue and PR because the request is blocked on libuv PTY support. Done would require an agreed API with cross-platform POSIX and ConPTY behavior, including stream, resize, fallback, and spawnSync handling.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 32/100