Interactive PTY sessions
Plain shell-exec runs commands through pipes: anything that needs a real
terminal — REPLs (python, node), debuggers (gdb, dlv), ssh,
password prompts, watch-mode dev servers, TUI programs — either hangs until
the tool timeout or misbehaves (no line editing, isatty false paths). PTY
sessions give the agent a real pseudo-terminal it can type into.
Tools
Three model-facing tools (granted by default to every agent that already has
shell-exec):
| Tool | Description |
|---|---|
pty-start {command, cols?, rows?} |
Allocates a pseudo-terminal (default 120×32), runs command through bash -lc under it, and returns a session ID (pty-N) plus the initial screen. |
pty-write {id, input?, submit?, wait_for?, wait_ms?} |
Sends input and returns output produced since the last read. submit: true appends \r (line submit). wait_for polls until a literal string (e.g. the prompt ">>> ") appears in new output, up to 10 s by default — prefer it over guessing wait_ms (single wait, default 700 ms, up to 30 s). Empty input just polls. |
pty-kill {id} |
Terminates the session's process group (SIGTERM, then SIGKILL after a 2 s grace) and frees the terminal. |
Input encoding — the canonical rule: backslash escape sequences in
input are decoded server-side into real bytes: \r \n \t \e (ESC),
\xHH, \uHHHH, and \\ for a literal backslash. It does not matter
whether a real control byte or the literal text \x03 lands in the JSON —
both arrive at the terminal as Ctrl-C, so the break character always works.
Unknown escapes pass through unchanged.
A typical session:
pty-start {"command": "python3"} → pty-1 + the >>> banner
pty-write {"id": "pty-1", "input": "2**64", "submit": true, "wait_for": ">>> "}
→ 18446744073709551616
pty-write {"id": "pty-1", "input": "\x04"} → Ctrl-D, interpreter exits
pty-kill {"id": "pty-1"} (or rely on session cleanup)
Security and approval
pty-startgoes through the exact same approval path asshell-exec: the same blocked-command list, permission rules, allowlist, and permission hooks apply.- Approval policy for input: approving a
pty-startcommand covers all subsequentpty-writeinput into that session. Free-form input into an approved interactive process is equivalent to running follow-up commands, so underask-firsttreat a pty-start approval as "this program may be driven unattended". Deny the start if that is not acceptable. - The child runs under the same OS sandbox policy as
shell-exec— the PTY is not a sandbox escape; kernel-level confinement wraps the process identically.
Output handling
- Each session keeps a 256 KiB scrollback ring buffer; older bytes are dropped. The model receives settled text: ANSI escapes are stripped and carriage returns / backspaces are applied as a terminal would (so readline's per-keystroke line redraws collapse into the final line instead of partial-frame soup). The raw bytes stay in the buffer.
- Large reads are spooled through the standard
tool-output spooling path, so huge
scrollback never floods the context (page the rest with
job-outputand the returnedspool:NID).
Lifecycle
- Sessions are session state: they survive between agent turns and are all killed when spettro exits (TUI quit or headless run end).
- The status bar shows
▣ N ptywhile sessions are live. - While a pty tool call is running, its transcript entry shows a live tail of the session's settled scrollback (last few lines); the ctrl+g full-output toggle lifts the cap to the whole scrollback.
Platform support
Unix only (Linux/macOS, via creack/pty).
On Windows the tools return "unsupported on this platform". Windows does have
the necessary primitive in ConPTY, but creack/pty does not implement it and
os/exec cannot pass the process attribute list a pseudoconsole is attached
through — so supporting it means driving CreateProcess directly and
re-implementing stdio plumbing and sandbox token handling by hand. Everything
else on Windows works normally; agents fall back to shell-exec with
run_in_background for long-running commands, which covers dev servers and
watch builds but not programs that must be driven interactively.
