Standard I/O and capture

I/O mode determines whether ProcessKit can observe the child's streams, whether the child sees a terminal, and whether the runner can capture or suppress output. Choose the mode from the child's actual contract rather than treating terminal inheritance as a cosmetic switch.

Mode matrix

ModeChild stdinChild stdout/stderrTTY preservedPump available
DefaultClosed / nullPipe → echoNoYes
--inherit-stdinRunner stdinPipe → echoOutput: noYes
--stdin-file FILEFile until EOFPipe → echoNoYes
--inherit-stdioRunner stdinDirect inherited handlesIf caller has oneNo
DetachedNullPump with echo discardedNoYes

The JSONL file is independent of every mode. Events always go to --jsonl, never to stdout.

Default: closed stdin, pipe and echo

processkit-cli run --jsonl run.jsonl -- cargo test

The child cannot wait forever for input the runner does not own. Its stdout and stderr are drained through separate pipes and retransmitted to the runner's stdout and stderr.

Because the child sees pipes rather than a terminal, it may disable color, progress bars, cursor movement, and interactive prompts. This is intentional and makes the default suitable for CI and automation.

Inherit all three handles

processkit-cli run --inherit-stdio --jsonl interactive.jsonl -- cargo watch

--inherit-stdio passes the caller's stdin, stdout, and stderr handles directly to the child. If the caller is attached to a terminal, the child sees that same terminal. The runner does not create a PTY.

Direct inheritance means there is no output pump. The following features are therefore unavailable and rejected at parse time:

  • --capture-dir and --capture-max-bytes;
  • --no-echo;
  • --idle-timeout;
  • --create-no-window;
  • --inherit-stdin and --stdin-file;
  • --detach.

That list is bounded by the pump, not by observability in general. --snapshot-interval composes with every I/O mode, --inherit-stdio included: a snapshot queries the container's own member list, so it needs no pipes to observe and conflicts with nothing here. --idle-timeout is the contrast that makes the boundary concrete — it is rejected above precisely because it can only re-arm on bytes the pump observes, while --snapshot-interval never looks at the child's output at all. --jsonl likewise keeps recording the full lifecycle under inheritance; only the child's bytes are outside the runner's view in this mode.

Terminal signal behavior is platform-dependent. On Unix a child in a separate foreground process group may receive Ctrl-C directly and report a signal exit; on Windows both child and runner can observe the console event. Use the local control-plane cancel command when an orchestrator needs one deterministic runner-owned cancellation outcome.

Inherit stdin only

processkit-cli run --inherit-stdin --jsonl input.jsonl -- sort

The child reads from the runner's input handle, while stdout/stderr remain on the default pump. The runner neither records nor mediates stdin bytes. This mode does not create a terminal; redirected caller input remains redirected input.

Feed a file

processkit-cli run \
  --stdin-file requests.ndjson \
  --jsonl importer.jsonl \
  -- importer --format jsonl

The checked file is streamed to the child and stdin closes at EOF. File bytes do not enter argv or lifecycle JSONL. A missing or unreadable file is a pre-spawn SETUP (111) failure.

--stdin-file and --inherit-stdin are mutually exclusive. For small literal input, create a file or explicitly invoke a shell/pipeline outside the runner; the CLI intentionally has no string-to-stdin flag that could encourage secrets inside process listings or shell history.

Suppress live echo

processkit-cli run \
  --no-echo \
  --capture-dir ./capture \
  --jsonl run.jsonl \
  -- noisy-worker

--no-echo removes only the runner's retransmission. Pipes stay open and the pump continues to:

  • drain the child so it cannot block on a full pipe;
  • write bounded capture files;
  • re-arm --idle-timeout on observed output;
  • preserve lifecycle events.

This is the normal embedding mode for an orchestrator that owns presentation and does not want the runner's live copy of child output interleaved with its own logs.

Bounded transcript capture

--capture-dir DIR creates two files:

DIR/
├── stdout.log
└── stderr.log

Streams remain separate and are still echoed unless --no-echo is present. Each file is capped independently at 8 MiB by default:

processkit-cli run \
  --capture-dir ./capture \
  --capture-max-bytes 32m \
  --jsonl run.jsonl \
  -- compiler

The terminal output_captured event reports, per stream:

FieldMeaning
pathCapture file location.
bytes_seenTotal bytes produced, including bytes beyond the file cap.
bytes_writtenBytes retained in the file.
sha256Digest of the retained bytes.
truncatedWhether output exceeded the configured cap.
write_errorCapture write failure, if one occurred.

Do not infer completeness from file size. Use truncated; a file whose length equals the cap may still be complete when the stream ended at exactly that boundary.

Two independent bounds

--capture-max-bytes limits retained disk bytes per stream. The ProcessKit pump also has a fixed 64 MiB in-flight line-assembly ceiling for a single unterminated line. They protect different resources and neither derives from the other.

Binary output

The live echo and capture path operate on bytes. Capture files preserve the bytes written up to the cap; they are not decoded or line-normalized. Runner diagnostics remain on stderr and are not written into stdout.log or stderr.log.

When stdout itself is a binary protocol, use --no-echo plus capture, or invoke the child directly if no lifecycle control is needed. The JSONL stream never shares stdout, so it cannot corrupt a binary child stream.

Secret hygiene

Argv is redacted in events by default, but child output is not. A command may echo credentials, environment values, file contents, or tokens into live output and capture files. Apply access controls and retention policies to:

  • the JSONL file when --argv-raw is used;
  • stdout.log and stderr.log;
  • any parent process that records runner stdout/stderr.

Diagnosing a degraded terminal UI

If a tool loses color, switches to plain progress messages, or refuses an interactive prompt under the default mode, confirm that it requires a TTY. Use --inherit-stdio only when the caller already owns a real console/terminal and does not need capture or idle detection.

ProcessKit CLI does not emulate a PTY. A program that specifically requires PTY semantics cannot obtain them from this runner today.

See also