Skip to content

Feature request: PTY support in child_process.spawn() #64019

@lygstate

Description

@lygstate

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:

  1. The terminal shows almost nothing for minutes ("stuck" UX).
  2. The log file updates in large bursts, not line-by-line.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestIssues that request new features to be added to Node.js.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    Awaiting Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions