Profpatsch/users/Profpatsch/pty-prompt

A PTY broker for driving interactive, prompt-based programs from an agent (LLM) or any caller that cannot hold a persistent connection.

Tools like git add -p, git rebase -i, REPLs, or installer wizards expect a real terminal on the other end and talk in a back-and-forth of prompt → answer → new prompt. An LLM tool-call, however, is a one-shot process invocation: it starts, does a thing, and exits. There is nothing to keep the interactive program — or its pseudo-terminal — alive between calls.

pty-prompt solves this by splitting into two parts:

So the agent just shells out to pty-prompt step ... repeatedly; the daemon holds the session open and remembers the latest output in between.

# Start a session. Everything after `--` is the command to run.
# Prints the program's initial output once it settles.
pty-prompt start -- git add -p

# Iterate: write TEXT + Enter, wait for the output to settle, and print
# everything produced since the input — i.e. the next prompt.
pty-prompt step y

# With no TEXT, step just re-polls (e.g. while a program is still working).
pty-prompt step

# Block until the child exits, then print everything since the call + exit
# status. For non-interactive commands where you just want the result.
pty-prompt wait

# Graceful shutdown (SIGTERM, then SIGKILL after a grace period).
pty-prompt stop

# Force shutdown (SIGKILL now) — for a wedged child that ignores SIGTERM.
pty-prompt kill-session

step is the one verb you iterate with. It always appends Enter, so you never have to think about line submission. The reply contains everything the child produced since your input (including the echo of what you typed), so you see the freshly-rendered prompt in the same call — no separate "read" step.

A prompt-based program never tells you "I'm finished, your turn". So after sending input, step waits until the child has produced no new bytes for --timeout milliseconds (default 500), then returns whatever accumulated. This is a heuristic — bump --timeout for slow programs.

When you don't want a heuristic at all — e.g. a non-interactive build where the only thing you care about is the final result — use wait instead. It blocks until the child actually exits, then prints everything produced since the call plus the exit status. One call, no polling, no guessing a quiet window. --max-wait (default 60m) caps the block so a wedged child can't hang you:

pty-prompt start --session build -- nix build .#thing -L
pty-prompt wait  --session build          # blocks until the build finishes

When the child exits, step reports it in the status line (see below). The daemon stays up after the child exits so a final step can still retrieve the last output; stop/kill-session then tear it down.

Every invocation prints a single status line to stderr (program output stays on stdout, so the two never mix). It is always printed — including on an empty poll while a program is still working — because that is exactly when the timing is most useful:

[pty-prompt: 13:58:42 | running for 16s, quiet for 1.5s, still running]
[pty-prompt: 14:00:56 | running for 2m14s, quiet for 0.3s, exited: exit status 1]
[pty-prompt: 14:00:56 | ran for 2m14s, exited, session "build" stopped]

Errors use the same bracketed form, e.g. [pty-prompt: session "build" already running for 16s (stop it first)].

Output is ANSI-stripped by default (escape sequences removed, CR dropped), so you get plain, line-oriented text ready to read. Pass --raw to keep escape codes verbatim — useful for full-screen TUIs where cursor movement matters.

Multiple independent programs can run at once via --session NAME (default: default):

pty-prompt start --session rebase -- git rebase -i HEAD~5
pty-prompt step  --session rebase x       # mark a commit for edit
pty-prompt stop  --session rebase

Flag Applies to Meaning
--session NAME all Session name (default default).
--timeout MS start / step Quiet window that counts as "settled" (default 500 ms).
--max-wait MS wait Cap on how long to block for exit (default 60 min).
--raw start / step / wait Keep ANSI escape codes instead of stripping them.

pty-prompt start -- git add -p          # shows the first hunk + "Stage this hunk [y,n,...]?"
pty-prompt step y                       # stage it; shows the next hunk
pty-prompt step n                       # skip this one
pty-prompt step s                       # split a hunk
pty-prompt step q                       # quit
# git exits on its own; the last step's status line ends with "exited".
pty-prompt stop                         # clean up

nix build .#pty-prompt
nix profile install .#pty-prompt
# or for local dev:
go run ./users/Profpatsch/pty-prompt -- bash

Full reference documentation is in the manpage:

man pty-prompt
# or, without installing:
man ./users/Profpatsch/pty-prompt/pty-prompt.1

pty-prompt(1)

pty-prompt - PTY broker for driving interactive prompt-based programs

pty-prompt start [--session name] [--timeout ms] [--raw] -- cmd [args ...]
pty-prompt step [--session name] [--timeout ms] [--raw] [text]
pty-prompt stop [--session name]
pty-prompt kill-session [--session name]

pty-prompt drives interactive, prompt-based programs — such as git add -p, git rebase -i, REPLs, or installer wizards — from a caller that cannot hold a persistent connection, most notably a large language model (LLM) agent.

Such programs expect a real terminal and converse in a back-and-forth of prompt → answer → new prompt. An LLM tool-call, however, is a one-shot process invocation: it starts, does a thing, and exits, leaving nothing to keep the interactive program — or its pseudo-terminal — alive between calls.

pty-prompt solves this by splitting into two cooperating parts:

The agent therefore simply shells out to pty-prompt step repeatedly; the daemon holds the session open and remembers the latest output in between.

A prompt-based program never announces that it has finished and is awaiting input. After sending input, pty-prompt waits until the child has produced no new bytes for --timeout milliseconds (default 500), then returns whatever accumulated. This is a heuristic; raise --timeout for slow programs.

When the child exits, the status line (see Status line) reports it. The daemon stays up after the child exits so a final step can still retrieve the last output; stop or kill-session then tears it down.

Output is ANSI-stripped by default: escape sequences are removed and carriage returns dropped, yielding plain, line-oriented text. Pass --raw to keep escape codes verbatim, e.g. for full-screen TUIs where cursor movement matters.

Every invocation prints a single status line to standard error (program output stays on standard output, so the two never mix). It is always printed, including on an empty poll while a program is still working, since that is when the timing is most useful:

[pty-prompt: 13:58:42 | running for 16s, quiet for 1.5s, still running]
[pty-prompt: 14:00:56 | running for 2m14s, quiet for 0.3s, exited: exit status 1]
[pty-prompt: 14:00:56 | ran for 2m14s, exited, session "build" stopped]

The leading HH:MM:SS is the wall-clock time the line was printed — an absolute reference across polls, since relative durations alone do not convey how much real time elapsed between separate invocations. running for is the time since the session started; quiet for is the time since the child last produced output (a large, growing quiet for distinguishes a slow-but-busy program from a hung one). The final clause is the child's state: still running, exited, or exited: <error>. Errors use the same bracketed form, e.g. "[pty-prompt: session "build" already running for 16s (stop it first)]".

start [options] -- cmd [args ...]

Spawn cmd under a PTY held by a freshly forked, detached daemon, then print the program's initial output once it settles. Everything after -- is the command to run. Fails if a session of the same name is already running.

step [options] [text]

The verb you iterate with. Write text followed by Enter, wait for output to settle, and print everything produced since the input — that is, the freshly rendered next prompt (including the echo of what you typed). With no text, step only re-polls, which is useful while a program is still working.

wait [--session name] [--max-wait ms] [--raw]

Block until the child exits, then print everything produced since the call plus the exit status. Unlike step, there is no quiescence heuristic: the only stopping condition is real completion. Use this for non-interactive commands (e.g. a build) where you just want the result. --max-wait caps the block (default 60 minutes) so a wedged child cannot hang the caller.

stop [--session name]

Gracefully terminate the child (SIGTERM, then SIGKILL after a grace period) and shut the daemon down, removing its socket and log file.

kill-session [--session name]

Force-terminate the child (SIGKILL immediately) and shut the daemon down. Use when a child is wedged and ignores SIGTERM.

--session name

Operate on the named session. Multiple independent programs may run at once. Defaults to default, so simple use needs no name.

--timeout ms

The quiescence window, in milliseconds: how long the child must stay quiet before its output is considered settled. Applies to start and step. Default 500.

--max-wait ms

The hard cap, in milliseconds, on how long wait will block for the child to exit. Default 3600000 (60 minutes).

--raw

Return output with ANSI escape codes intact instead of stripping them. Applies to start, step and wait.

$ pty-prompt start -- git add -p   # shows first hunk + prompt
$ pty-prompt step y                # stage this hunk, show the next
$ pty-prompt step n                # skip the next one
$ pty-prompt step s                # split a hunk
$ pty-prompt step q                # quit
$ pty-prompt stop                  # clean up

$ pty-prompt start --session build -- nix build .#thing -L
$ pty-prompt wait  --session build   # blocks until the build exits
$ pty-prompt stop  --session build

$ pty-prompt start --session rebase -- git rebase -i HEAD~5
$ pty-prompt step  --session rebase x
$ pty-prompt stop  --session rebase

$ pty-prompt start --timeout 1500 -- python3 -q
$ pty-prompt step  --timeout 1000 "1 + 41"
$ pty-prompt step  "exit()"
$ pty-prompt stop

$XDG_RUNTIME_DIR/pty-prompt/<session>.sock

Unix-domain socket on which a session daemon listens. Falls back to a directory under TMPDIR when XDG_RUNTIME_DIR is unset.

$XDG_RUNTIME_DIR/pty-prompt/<session>.sock.log

Standard error of the detached daemon for that session.

XDG_RUNTIME_DIR

Base directory for session sockets. When unset, TMPDIR (or the system temporary directory) is used instead.

The pty-prompt utility exits 0 on success, and >0 if an error occurs.

A client command exits non-zero when it cannot reach the named session (for example, no daemon is running) or when the daemon reports an error.

Build and install using Nix flakes:

$ nix profile install git+https://codeberg.org/Profpatsch/Profpatsch?ref=canon#pty-prompt

This installs pty-prompt and this man page to your Nix profile.

The daemon's lifecycle hangs off a context.Context cancelled by a stop or kill-session request or by SIGTERM/SIGINT, which deterministically tears down the accept loop, the PTY read loop, and the child process. After the child exits, the daemon stays up so a final step can still retrieve the last output and the exit result; a subsequent stop then removes the session. The daemon's standard error is redirected to a per-session log file, so a caller that pipes start does not block waiting for the long-lived daemon to close the inherited stream. The output buffer is capped at the last 1 MiB to bound memory on chatty programs.

git-add(1), pty(7), tmux(1)

Profpatsch

pty-prompt was created with
Claude Code.