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) == true because 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, and stderr
  • The PTY slave set as the child’s controlling terminal (TIOCSTTY)
  • Your environment variables inherited (plus any --env additions)
  • The PTY dimensions set to --cols × --rows (default 80×24)

Step 3: I/O Bridging

shuck runs an event loop with two concurrent operations:

  1. stdin → PTY master: Any data on shuck’s stdin is written to the PTY master (forwarded to the child)
  2. 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 TypeExampleAction
SGR colors\x1b[31mred\x1b[0mStrip escape codes, keep text
Screen clear\x1b[2J\x1b[HDiscard entirely
OSC (window title)\x1b]0;title\x07Discard entirely
CR+LF\r\nNormalize to \n
Backspacesloading...\x08\x08doneApply (simulate terminal)
Cursor movement\x1b[A\x1b[BTrack 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:

  1. Waits for the child’s exit code
  2. Flushes any buffered output
  3. 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