Introduction

shuck makes any interactive CLI non-interactive.

It wraps any program in a pseudo-terminal so the child process believes it’s running in a real terminal, captures all output, strips TUI artifacts (ANSI escape sequences, spinners, progress bars, cursor movement), and returns clean, pipeable UTF-8 text.

Think of it as expect for the AI agent era — zero config, zero scripting.

The Problem

Interactive CLIs are everywhere. Tools like python3, node, mysql, psql, and hundreds of others detect whether they’re running in a terminal and behave differently:

  • They render TUI interfaces with colors, spinners, and progress bars
  • They refuse to accept piped input
  • They emit raw ANSI escape sequences that corrupt your data
  • They can’t be easily composed with |, >, $(), or xargs

This creates a wall between interactive tools and any kind of automation — CI/CD, AI agents, scripts.

The Solution

# Before: interactive CLIs can't be piped
python3                          # launches REPL, blocks
mysql -u root                    # interactive prompt, requires TTY

# After: clean, pipeable output
echo "import sys; print(sys.version)" | shuck python3
shuck mysql -u root -- -e "SHOW DATABASES" > dbs.txt

shuck solves this by:

  1. Allocating a PTY — the child process sees isatty(stdout) == true
  2. Forwarding stdin — you can pipe input in, or provide it with --
  3. Capturing all output — everything written to the PTY master
  4. Stripping ANSI — single-pass VTE state machine, zero allocations
  5. Returning clean text — valid UTF-8, suitable for any downstream tool

Design Principles

Safety First

shuck never silently modifies, truncates, or reinterprets output. It propagates real exit codes from the child process. stdout is sacred — only child output appears there. Diagnostics go to stderr.

Speed by Design

ANSI stripping uses a single-pass state machine (not regex). The bottleneck is always the child process, never shuck. Written in Rust with zero unnecessary allocations.

Composability

Output is valid UTF-8, suitable for |, >, $(), xargs, and everything else in your shell. Deterministic: same input → same output (modulo child process).

Agent Ready

  • --json mode returns {stdout, stderr, exit_code, duration_ms, timed_out}
  • Exit codes follow conventions agents already know (124=timeout, 127=not-found)
  • MCP server available for direct LLM integration

Next Steps