A cross-platform Rust CLI and library for driving interactive terminal applications through PTYs.
ptywright is an early general-purpose PTY/TUI automation toolkit. It is designed to drive interactive terminal applications from code without coupling the core abstractions to any one program. A generic Extension trait sits above the PTY/session/screen/action/matcher primitives, with Claude Code shipped as the first plugin under that trait.
The library now includes target, session, rich screen snapshot, action, temporal and plugin-defined matchers, bounded transcripts with turn segmentation marks and optional raw file streaming, redaction, JSON-RPC with atomic adapter.turn / cooperative adapter.cancel_wait / introspection via plugin.describe, the generic Extension layer with in-process Session::events / ExtensionHandle::subscribe, a Lua-backed interactive Claude Code adapter with stable-screen turn evidence and structured metadata.* channels (permission, plan, error, usage, status, dialog correlation), plugin manifest/runtime primitives with per-method permission gating, a Session SIGTERM→SIGKILL signal ladder, and shell completion primitives backed by real PTYs. The CLI includes run for live stdin/stdout PTY debugging, serve --stdio for NDJSON or LSP-style JSON-RPC automation, multi-client local IPC via Unix sockets on macOS/Linux and named pipes on Windows, serve --plugin <manifest.toml> for trusted-local third-party plugins, logs --tail for following the rotated log file, repl for an interactive embedded-Lua client over a running server (auto-spawning one when none is listening), and completions for shell setup.
Docs: https://utensils.io/ptywright/
ptywright --help
ptywright --version
ptywright run -- /bin/sh -lc 'printf ready'
printf '{"jsonrpc":"2.0","id":1,"method":"server.capabilities"}\n' | ptywright serve --stdio
printf '{"jsonrpc":"2.0","id":1,"method":"adapter.list"}\n' | ptywright serve --stdio | jq '.result.plugins[].name'
printf '{"jsonrpc":"2.0","id":1,"method":"adapter.start","params":{"plugin":"claude-code","program":"/bin/sh","args":["-lc","cat"]}}\n' | ptywright serve --stdio | jq '.result | {adapter, plugin}'
ptywright serve --stdio --framing lsp
ptywright serve --socket /tmp/ptywright.sock
source <(ptywright completions zsh)The default build embeds Lua 5.4 for trusted adapter plugins, so source builds need a working C compiler in addition to Rust. The Nix dev shell provides the expected toolchain on macOS/Linux.
git clone https://github.com/utensils/ptywright
cd ptywright
nix develop
cargo build --release
./target/release/ptywright --helpOr run through Nix on macOS/Linux:
nix run github:utensils/ptywright -- --help- Provide Rust abstractions for spawning, attaching to, and driving PTY-backed terminal applications.
- Keep application-specific adapters separate from the core architecture.
- Support deterministic turn execution, transcript capture, prompt detection, and output parsing.
- Target macOS, Linux, and Windows.
- Keep a clean CLI surface while exposing reusable library primitives.
- Preserve a small, auditable, local-first implementation.
- Target configuration.
- PTY session lifecycle.
- Terminal screen observation.
- Input actions and key sequences.
- Matchers and waits.
- Bounded transcript capture with explicit raw transcript file streaming opt-in.
- JSON-RPC over stdio or multi-client local IPC for external automation clients, with NDJSON and LSP-style framing.
- Generic
Extensiontrait withExtensionHandlehost loop; Claude Code ships as the first plugin under that trait, driven through theadapter.*JSON-RPC surface. - Plugin manifests, permission declarations, and trusted embedded Lua runtime for adapter orchestration. Trusted-local third-party plugins load via the
ptywright serve --plugin <manifest.toml>CLI flag or theplugin.loadJSON-RPC method (gated by--allow-plugin-load). - Per-method permission gating at the JSON-RPC dispatcher — every
adapter.*method consults the bound plugin manifest's declared permissions and rejects calls with-32004 PermissionDeniedcarrying structureddata. - Per-user runtime directory under
~/.ptywright/with structured logging (daily rotation, redaction-aware writers). - Interactive REPL client (
ptywright repl, default-onreplCargo feature): embedded Lua 5.4 evaluator over the genericadapter.*surface, reedline line-editing with Lua-introspection completion / highlighting / multi-line continuation, auto-spawn of a background server when none is listening, tmux-style attach, and inline notification rendering. - Shell completion generation for bash, zsh, fish, elvish, and PowerShell.
- Rich screen snapshots with cell/style/mode metadata.
- Redaction helpers with built-in and caller-supplied patterns plus default RPC redaction for sensitive-looking output.
- In-process event subscription on
SessionandExtensionHandle; structured matcher outcomes onadapter.wait; cooperative cancellation viaCancellationToken/adapter.cancel_wait; plugin-definedMatcher::Luapredicates evaluated against a boundPluginRegistry. - Atomic
adapter.turn(send + wait under the per-adapter mutex),adapter.resumechaining across PTY restarts, andplugin.describeruntime introspection of intents, wait matchers, and classifier states. - Cross-platform PID + signal ladder (
Session::pid,Session::signal,Session::terminatewith SIGTERM→SIGKILL escalation) and plugin-mandated env (default_target.required_env) plusTarget::clear_envfor sandboxed spawns. - Bounded transcript turn segmentation (
Transcript::mark/marker/slice_between), classifier-drivenhost_marks, and theAction::MarkTranscriptplan primitive.
- More Claude Code real-world fixtures and transition tests as upstream Claude Code UI changes.
- A second built-in adapter (Codex / shell) to validate the plugin model against a non-Claude TUI.
- Optional WASM only if untrusted marketplace-style plugins become a concrete priority.
nix develop # auto via direnv + use_flake
ci-local # fmt-check → check → clippy → test → build
docs-dev # run the VitePress docs siteUseful direct commands:
cargo fmt --all -- --check
cargo check --locked
cargo clippy --locked -- -D warnings
cargo test --locked --features _test-fixtures
cargo run -- --helpptywright keeps configuration and rotated log files under ~/.ptywright/ (override with PTYWRIGHT_HOME=/some/path). Logs are written through tracing with daily rotation, 14-day retention by default, and built-in redaction of secret-shaped values. Override the filter at runtime with PTYWRIGHT_LOG:
PTYWRIGHT_LOG="info,ptywright::rpc=debug" ptywright serve --stdioSee config.example.toml for the full set of tunables and the Runtime directory docs for layout details. ptywright run writes only to file (it owns your terminal); serve --stdio and serve --socket write to file and stderr while keeping stdout reserved for JSON-RPC framing.
See AGENTS.md for repository guidance.
MIT — see LICENSE.