How shuck Works
shuck is built around three core mechanisms: PTY allocation, I/O bridging, and ANSI stripping. Understanding these helps you know exactly what shuck does and doesn’t do.
Why PTY Allocation Matters for Agents
The core problem is that interactive CLI tools check isatty(stdout) to decide how to behave. When an agent runs psql or brew doctor through a normal pipe, the tool detects it’s not in a terminal and changes behavior — often producing less output, dropping to a minimal mode, or refusing to run at all.
shuck solves this at the kernel level by allocating a real pseudo-terminal. The child process sees a real terminal and behaves exactly as it would for a human — but the output goes to the agent instead.
Architecture Overview
┌─────────┐ stdin ┌─────────────┐ PTY master ┌──────────────┐
│ Agent │ ──────────────>│ shuck │ ──────────────> │ Child Process│
│ Input │ │ (Rust) │ <────────────── │ (interactive)│
└─────────┘ │ │ PTY slave └──────────────┘
│ ┌────────┐ │
│ │ VTE │ │ stdout ┌─────────────┐
│ │Stripper│ │ ────────────> │ Clean Output│
│ └────────┘ │ └─────────────┘
└─────────────┘
Step 1: PTY Allocation
When you run shuck <command>, it allocates a pseudo-terminal pair — a master/slave file descriptor pair that mimics a real terminal:
- PTY slave is given to the child process as its controlling terminal
- PTY master is held by shuck — reading from it captures the child’s output, writing to it sends input
- The child process sees
isatty(stdout) == truebecause it IS connected to a real (pseudo) terminal
This is why tools like psql, mysql, redis-cli, and brew doctor behave interactively — they think they’re in a real terminal.
Step 2: Child Process Launch
shuck forks the child process with:
- PTY slave as
stdin,stdout, andstderr - The PTY slave set as the child’s controlling terminal (
TIOCSTTY) - Your environment variables inherited (plus any
--envadditions) - The PTY dimensions set to
--cols × --rows(default 80×24)
Step 3: I/O Bridging
shuck runs an event loop with two concurrent operations:
- stdin → PTY master: Any data on shuck’s stdin is written to the PTY master (forwarded to the child)
- PTY master → VTE stripper → stdout: Output from the child passes through the VTE ANSI stripper before being written to shuck’s stdout
The -- syntax lets you provide a string directly as stdin:
shuck python3 -- "print('hello')"
# Equivalent to: echo "print('hello')" | shuck python3
Step 4: ANSI Stripping
The VTE (Virtual Terminal Emulator) stripper processes raw terminal output and removes:
| Sequence Type | Example | Action |
|---|---|---|
| SGR colors | \x1b[31mred\x1b[0m | Strip escape codes, keep text |
| Screen clear | \x1b[2J\x1b[H | Discard entirely |
| OSC (window title) | \x1b]0;title\x07 | Discard entirely |
| CR+LF | \r\n | Normalize to \n |
| Backspaces | loading...\x08\x08done | Apply (simulate terminal) |
| Cursor movement | \x1b[A\x1b[B | Track internally, emit text only |
The stripper is a single-pass state machine — no regex, no allocations, O(n) in input length. It uses the same parsing logic as Alacritty’s VTE crate.
Step 5: Exit Code Propagation
When the child process exits, shuck:
- Waits for the child’s exit code
- Flushes any buffered output
- Exits with the same code as the child
Exit codes 124, 125, 126, 127 are reserved for shuck’s own errors. See Exit Codes for details.
Platform Backends
shuck has three PTY backends, selected at compile time or runtime:
POSIX (Linux, macOS, BSD, illumos)
Uses posix_openpt / grantpt / unlockpt / ptsname to allocate a PTY pair, then fork + setsid + TIOCSCTTY to launch the child. Implemented via the nix crate.
Windows (ConPTY)
Uses CreatePseudoConsole from the Win32 API (available on Windows 10 1809+). The ConPTY subsystem provides equivalent PTY functionality on Windows.
Pipe Fallback (Windows)
shuck always uses a real PTY. On platforms where PTY isn’t available (Windows, until ConPTY support lands), it falls back to pipes automatically. ANSI stripping still runs. The child process won’t see isatty(stdout) == true, which means some tools may behave differently (less output, or output without color).
What shuck Does NOT Do
- No process injection — shuck cannot control an already-running interactive program
- No screen scraping — shuck reads the raw PTY stream, not a rendered terminal buffer
- No expect scripting — shuck does not match patterns and send responses; for that, use
expect - No network access — shuck touches nothing outside its PTY and child process