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
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
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 likeall - Tmux command-separator blocked:
tmux_send_keyrejects;,{,}, and control bytes anywhere in key names (would otherwise escape thesend-keyscommand context) - Exact-target syntax: every
-t nameinvocation uses tmux's exact-match form (=namefor 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