@aliaksei-raketski/pi-tmux-bash
Pi extension that executes model-facing shell commands in managed tmux windows.
Package details
Install @aliaksei-raketski/pi-tmux-bash from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@aliaksei-raketski/pi-tmux-bash- Package
@aliaksei-raketski/pi-tmux-bash- Version
0.3.18- Published
- Jul 27, 2026
- Downloads
- 1,468/mo · 578/wk
- Author
- aliaksei-raketski
- License
- MIT
- Types
- extension
- Size
- 424.7 KB
- Dependencies
- 4 dependencies · 4 peers
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@aliaksei-raketski/pi-tmux-bash
A Pi extension that replaces model-facing bash with owned tmux windows. It preserves normal foreground behavior while adding durable background runs, stable @<windowId> control, completion policies, optional interactive input and ! routing, safe attach UX, resource quotas, and opt-in non-Git scopes.
Prerequisite and installation
Install tmux on PATH (or configure an absolute tmuxBinary), then install:
pi install npm:@aliaksei-raketski/pi-tmux-bash
The package registers model-facing bash and tmux tools plus /tmux-attach, /tmux-cleanup-preview, and /tmux-cleanup where enabled.
Command lifecycle and waiting
{ "command": "pnpm nx test app" }
Explicit background process:
{
"command": "pnpm nx serve app",
"background": true,
"waitForCompletion": false
}
Example of required finite asynchronous work:
{
"command": "pnpm nx test app",
"background": true,
"waitForCompletion": true,
"completionDelivery": "model"
}
Use waitForCompletion: true for every required finite command, including tests, builds, and subagents, regardless of duration or concurrent productive work. Use false only for persistent servers, watchers, or REPLs that are intentionally expected to remain alive. Waiting and completion delivery are independent.
The lifecycle is persisted as reserved → starting → running → completed|failed|killed|orphaned. A foreground timeout defaults to background execution and is awaited by default; timeoutAction: "kill" kills the validated owned window instead. Cancellation kills foreground work. Non-zero foreground exits remain normal bash tool errors with the actual status.
Completion delivery policies
defaultCompletionDelivery is model; each bash call may override it with completionDelivery.
modelqueues one bounded follow-up custom message, commits its wake handoff, then releases an awaited gate withwake: "producer-message".displaypersists and shows one TUI/RPC notification without model context, then releases withwake: "none". Without UI, thetmuxtool'slistaction and durable entries retain the diagnostic.next-turnpersists a bounded pending completion and releases withwake: "none". The next natural model turn injects and consumes it once; it never creates a turn itself.
A display-only awaited command resumes an autonomous workflow only when a consumer explicitly enables the continuation protocol's single-winner wake: none auto-resume policy. Otherwise Pi remains idle until natural input.
Stable completionId values are persisted in manifests and session entries. Watcher, poller, adoption, and retry observations are idempotent. Before model redelivery after restart, the extension scans the active branch for an existing completion or consumed marker.
Restart adoption
Adoption is off by default. Enable it only with a durable absolute directory:
{
"adoptionPolicy": "same-pi-session",
"durableOutputDir": "/home/me/.pi/agent/tmux-bash",
"preserveOutputFiles": true,
"adoptionScanTimeoutMs": 5000,
"adoptPolling": true,
}
On session_start, the extension independently scans complete tmux ownership metadata and strict durable manifests before publishing its authoritative gate snapshot. It:
- adopts only the same
ctx.sessionManager.getSessionId()and canonical workspace scope; - restores only validated live awaited gates and optional pollers;
- delivers a completion written while Pi was offline without reconstructing a completed gate;
- marks a missing-window/no-sentinel run orphaned;
- ignores another session, changed ownership, malformed data, symlinks, unsafe permissions, and pre-enhancement windows missing the complete current metadata shape;
- treats tmux absence or scan timeout as a bounded startup diagnostic rather than a Pi startup failure.
Shutdown persists live intent, releases only in-process gates, stops timers, and does not kill preserved windows. Adoption cannot recover work whose tmux metadata or manifest was never fully committed, and cannot guarantee delivery if both the Pi session entry and durable completion marker are lost in the same storage failure.
There are no schema/protocol version fields or compatibility negotiation. Older windows carrying only gitRoot/v1 options remain externally alive but unmanaged.
tmux actions
Targets use stable IDs such as @123; numeric indexes and arbitrary sessions are rejected.
list/peek: list in-scope runs or read a bounded tail.listincludes resource usage diagnostics.kill: revalidate ownership, kill the run, and release its gate.poll/unpoll/list-polls: manage progress delivery independently of waiting.await/unawait: idempotently acquire or release a finite run's continuation gate without changing the process.attach: return safe structured argv and a shell-quoted display command; never take TUI control.send-input/send-key: opt-in literal UTF-8 input or one fixed control key.cleanup-preview/cleanup: preview or delete only validated eligible non-running artifacts.
Interactive input
Enable input explicitly and include the actions:
{
"interactiveInputEnabled": true,
"maxInputBytes": 16384,
"enabledTmuxActions": ["list", "peek", "kill", "await", "unawait", "send-input", "send-key"],
}
send-input uses tmux buffer/paste semantics and argv, never shell composition or tmux key syntax. submit defaults to true. send-key accepts only enter, escape, ctrl-c, or ctrl-d. Every call revalidates a live, in-scope, metadata-owned stable window. Input does not poll, await, unawait, or alter completion policy.
Never put passwords, API keys, or other secrets in model tool arguments. Tool arguments remain session-visible. Input text is not logged, displayed, or persisted in the manifest.
Attach UX
The model action only presents { binary, args, display }. /tmux-attach [@id] is the explicit user action. With no ID it selects among live in-scope runs, confirms how to return to Pi, revalidates ownership after confirmation, then temporarily stops/restarts the TUI around inherited-stdio tmux. RPC/non-TUI contexts only present the safe command.
Outside tmux the argv attaches the owned session and selects the stable window. Inside tmux it switches the active client to the owned session before selecting the stable window, without shell composition. Session names are data, not executable syntax.
Optional ! and !! routing
routeUserBash defaults to false. When true, the extension handles Pi's user_bash event with a BashOperations adapter backed by the same tmux supervisor.
event.cwdis honored for each command;- stdout/stderr bytes are forwarded in order;
- normal non-zero status is returned as the actual exit code;
- timeout and cancellation kill the foreground owned window when termination can be confirmed; if ownership or termination cannot be revalidated, the command remains monitored in the background and the result reports that uncertainty;
- no continuation gate, poller, background completion, or synthetic message is created;
PI_SESSION_ID, session file, provider/model, and reasoning variables are removed;- Pi still owns
!!context exclusion because the extension returns standard operations/results.
No background syntax is added. Extension event ordering matters: only one user_bash router should own a command. Place tmux-bash deliberately relative to SSH, interactive-shell, sandbox, or other routers; leave routeUserBash off when another router should win.
Resource limits and bounded output
Defaults are nonzero and apply per canonical workspace durable root:
{
"maxConcurrentRuns": 16,
"maxArtifactBytesPerRun": 10485760,
"maxArtifactBytesTotal": 1073741824,
"maxCompletedRuns": 100,
"completedArtifactRetentionSeconds": 86400,
"resourceScanIntervalSeconds": 60,
"quotaPolicy": "reject-new",
}
A filesystem lock and reservation files serialize cross-process slot checks. Startup failures release reservations. Existing running work is reconciled and counted before new work; running work is never killed to satisfy quota.
The generated Node bounded-tee helper duplicates exact combined bytes to the tmux pane while compacting the private tail artifact in place. It never renames an open tee inode, preserves PIPESTATUS[0], marks rotation, and keeps the file at or below maxArtifactBytesPerRun. Small command, wrapper, manifest, marker, and sentinel files count toward total usage.
cleanup-preview reports bounded oldest-first candidates and reclaimable bytes. With quotaPolicy: "reject-new", launches reserve capacity for their eventual completed records and are rejected before maxCompletedRuns can be exceeded. Model cleanup observes configured retention. /tmux-cleanup can include retained completed runs only after explicit user confirmation. Cleanup rechecks that no owned live window exists and refuses symlinks, paths outside the durable root, live runs, and unowned resources.
Workspace scope and non-Git fallback
Git always takes precedence. Scope identity is { kind, root, hash }, where root is canonicalized through realpath; directories with the same basename never collide.
The default remains secure/erroring outside Git:
{ "nonGitScope": "error" }
Opt in to a canonical cwd scope:
{
"nonGitScope": "cwd",
"cwdTmuxSessionNameTemplate": "pi-cwd-{scopeHash}",
}
tmuxSessionScope now uses "workspace" or "global"; tmuxWindowScope uses "pi-session", "workspace", or "all". The old window-filter value "git-root" is explicitly normalized to "workspace" for configuration migration, but old tmux metadata is not adopted.
Continuation gates, Goal, and status
The package depends only on the generic continuation-gate protocol, not Pi Goal. An awaited model completion queues its producer message before releasing its gate with the committed handoff. Display/next-turn persist first and release with wake: "none". Gates use the protocol's default continuation domain.
Statusline publication remains load-order-independent through @aliaksei-raketski/pi-statusline-protocol: N bg job(s) is running; N bg jobs · M awaited is awaiting. Zero live jobs publishes a muted 0 bg jobs status so layouts that reference tmux-bash remain stable. Expanded list diagnostics can show adoption, completion policy, output rotation, pending completions, and quota usage without putting raw unbounded output into status.
Public compiled helpers
Read-only consumers can install @aliaksei-raketski/pi-tmux-bash-core:
import {
listManagedTmuxWindows,
parseManagedRunManifest,
resolveTmuxWorkspaceScope,
TMUX_BASH_METADATA_KEYS,
} from '@aliaksei-raketski/pi-tmux-bash-core';
It ships compiled ESM and declarations and has no Pi extension resource or ExtensionAPI dependency. Its public boundary is intentionally read-only: canonical scope resolution with injected hosts, strict manifest/metadata parsing, naming, ownership comparison, bounded discovery with an injected executor, structured attach construction, and stable constants. It does not export launching, gates, completion mutation, input, deletion, or runtime maps.
Configuration reference
Configuration is JSONC from $PI_TMUX_BASH_CONFIG, or tmux-bash.jsonc under Pi's global agent directory. Invalid values fail registration rather than weakening safety.
{
"defaultTimeoutSeconds": 120,
"maxTimeoutSeconds": 86400,
"defaultTimeoutAction": "background",
"defaultWaitForBackgroundCompletion": false,
"defaultWaitAfterForegroundTimeout": true,
"defaultCompletionDelivery": "model",
"tmuxBinary": "tmux",
"tmuxSessionScope": "workspace",
"globalTmuxSessionName": "pi-tmux-bash",
"gitRootTmuxSessionNameTemplate": "pi-{gitHash}",
"cwdTmuxSessionNameTemplate": "pi-cwd-{scopeHash}",
"tmuxWindowScope": "pi-session",
"tmuxWindowNameTemplate": "{name}-{runId}",
"nonGitScope": "error",
"adoptionPolicy": "off",
"adoptionScanTimeoutMs": 5000,
"adoptPolling": true,
"durableOutputDir": "/absolute/path/under/pi-agent/tmux-bash",
"interactiveInputEnabled": false,
"maxInputBytes": 16384,
"routeUserBash": false,
"pollDelivery": "display",
"defaultPollIntervalSeconds": 30,
"minimumModelPollIntervalSeconds": 15,
"maxOutputBytes": 51200,
"maxSpoolBytes": 10485760,
"maxArtifactBytesPerRun": 10485760,
"maxArtifactBytesTotal": 1073741824,
"maxConcurrentRuns": 16,
"maxCompletedRuns": 100,
"completedArtifactRetentionSeconds": 86400,
"resourceScanIntervalSeconds": 60,
"quotaPolicy": "reject-new",
"autoCloseWindowsOnCompletion": true,
"preserveOutputFiles": false,
"statusbarEnabled": true,
}
maxSpoolBytes is accepted as the direct migration alias for maxArtifactBytesPerRun; internal execution uses the latter. enabledTmuxActions narrows the public enum, and disabled interactive actions are omitted even if mistakenly listed. Omit enabledTmuxActions to inherit newly added safe actions; an explicit array remains pinned until you update it. environmentDenylist defaults to TMUX, TMUX_PANE, PWD, OLDPWD, SHLVL, and _.
Commands and input run with the user's permissions. Tmux is always invoked with argument arrays. Wrapper paths/environment values are quoted only where Bash syntax is required. Command text and output are bounded and sanitized before model/UI rendering.