pi-tmux-harness

Pi extension exposing tmux as native tools — drive other TUIs (pi, claude, copilot CLI, lazygit, etc.) for adversarial testing without fragile sleep+grep loops.

Package details

extension

Install pi-tmux-harness from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-tmux-harness
Package
pi-tmux-harness
Version
0.1.0
Published
May 4, 2026
Downloads
not available
Author
bagatelia
License
MIT
Types
extension
Size
33.1 KB
Dependencies
0 dependencies · 4 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-tmux-harness

npm version License

Tmux session/keystroke/capture exposed as native pi coding agent tools, so any agent in pi can drive other terminal programs (most importantly: another pi instance, claude code, copilot CLI, lazygit, htop, gh) for adversarial testing — without ad-hoc bash → tmux send-keys → sleep → grep chains.

Why

When testing pi extensions, you end up writing this kind of fragile shell:

tmux new-session -d -s pi-test 'pi'
sleep 8                                    # how long is enough?
tmux send-keys -t pi-test '/myextension' Enter
sleep 12                                   # guess again
tmux capture-pane -t pi-test -p | tail -25 | grep "expected output"

Every sleep is a guess. Wrong guesses produce flaky tests. The grep is eyeball-driven assertion.

This extension turns that into:

tmux_new({ command: "pi", cwd: "/path/to/repo" })
tmux_expect({ name, pattern: "\\[Extensions\\]", timeoutMs: 30000 })  // ready signal
tmux_send({ name, text: "/myextension foo" })
tmux_expect({ name, pattern: "expected output regex", timeoutMs: 15000 })
tmux_capture({ name })  // full snapshot for further assertions
tmux_kill({ name })

The killer feature is tmux_expect: poll capture-pane until a regex matches or timeout fires. Eliminates ~90% of the fragile sleep+grep we used to write.

Install

pi install npm:pi-tmux-harness

Then restart pi (or start a new session). The 7 tools below register automatically.

Tools

All sessions go to an isolated tmux socket (-L pi-harness) so they never appear in your normal tmux ls output, can't accidentally be attached to, and kill all is bounded.

Tool Purpose
tmux_new Spawn a detached tmux session running any command (default: shell). Returns the session name.
tmux_send Send literal text. Optional enter: true (default) appends Enter.
tmux_send_key Send a named special key (Escape, Tab, Up/Down, C-c, C-d, BSpace, etc.) — uses tmux's native key names.
tmux_capture Snapshot the pane. Configurable scrollback lines + trim.
tmux_expect Poll capture until a regex matches or timeout. Returns {matched, match?, elapsedMs, lastSnapshot}. The fragile-sleep killer.
tmux_kill Kill a session. Pass name: "all" for every managed session.
tmux_list List sessions managed by this extension (with elapsed time + cwd).

Hardening

This extension has been through ~10 rounds of multi-model code review. The defenses include:

  • Regex DoS protection in tmux_expect: AST-level rejection of obviously catastrophic patterns (nested quantifiers like (a+)+, (a{1,2})+, etc.) + 32KB input cap on the regex search window
  • Tmux flag injection blocked: strict allowlist /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/ on session names, validated at every tool call. Rejects names starting with - (which would be parsed as tmux flags) and reserved names like all
  • Tmux command-separator blocked: tmux_send_key rejects ;, {, }, and control bytes anywhere in key names (would otherwise escape the send-keys command context)
  • Exact-target syntax: every -t name invocation uses tmux's exact-match form (=name for session targets, =name: for pane targets) to defeat tmux's default prefix matching
  • AbortSignal honored at every poll boundary in tmux_expect
  • Bounded LRU on the managed-session map (hard cap 64; rejects new sessions when at cap)
  • Tracker bookkeeping for prune-on-list when sessions disappear

Per-agent reference (drive these via the harness)

The harness is agent-agnostic — it doesn't care what's in the tmux pane. Put the right binary in tmux_new's command and use the right ready-signal in tmux_expect.

pi (recursive testing)

tmux_new({ command: "pi", cwd: "/path/to/repo", width: 220, height: 50 })
tmux_expect({ name, pattern: "\\[Extensions\\]", timeoutMs: 30000 })  // pi finished startup
tmux_send({ name, text: "/t why does my parser leak memory" })
tmux_expect({ name, pattern: "\\[Triage\\]", timeoutMs: 15000 })
tmux_send_key({ name, keys: ["Escape"] })  // close any modal
tmux_send_key({ name, keys: ["C-d"] })     // quit pi

Claude Code

tmux_new({ command: "claude", cwd: "/path/to/repo" })
tmux_expect({ name, pattern: "\u2502\\s*>\\s*\u2502|Try .*claude", timeoutMs: 30000 })
tmux_send({ name, text: "explain the auth flow" })
tmux_send_key({ name, keys: ["Escape"] })  // interrupt running operation
tmux_send({ name, text: "/exit" })

GitHub Copilot CLI

tmux_new({ command: "copilot", cwd: "/path/to/repo" })
tmux_expect({ name, pattern: "GitHub Copilot|Welcome|Trust this folder|copilot>", timeoutMs: 30000 })
// If "Trust this folder?" appears, accept it:
tmux_send_key({ name, keys: ["Enter"] })
tmux_send({ name, text: "summarize this repo" })
tmux_send_key({ name, keys: ["Escape"] })  // cancel "Thinking..."
tmux_send({ name, text: "/exit" })

Generic shell (sanity check)

tmux_new({ command: "/bin/sh" })
tmux_send({ name, text: "echo READY_$(date +%s)" })
tmux_expect({ name, pattern: "READY_\\d+", timeoutMs: 5000 })

When to use harness vs -p print mode

Need Use
One-shot answer, parse stdout claude -p / copilot -p / pi -p directly. No harness needed.
Multi-step interactive flow with cancels / modals / slash commands Harness.
Drive a TUI you don't control (lazygit, htop, gh, etc.) Harness.
Stream structured events to a supervisor claude --output-format stream-json directly. No harness needed.
Test that a pi extension renders a specific message at a specific point Harness with tmux_expect.

Requirements

  • tmux (≥ 3.2 recommended for full key-modifier support)
  • Pi coding agent

License

MIT