@r3b1s/tmux-pilot
Spawn subagents as real coding agent processes in detached tmux windows — full TUI observability, guard lifecycle, settings UI, and debug log viewer.
Package details
Install @r3b1s/tmux-pilot from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@r3b1s/tmux-pilot- Package
@r3b1s/tmux-pilot- Version
1.0.0- Published
- Jul 21, 2026
- Downloads
- 290/mo · 290/wk
- Author
- r3b1s
- License
- MIT
- Types
- extension
- Size
- 9.3 MB
- Dependencies
- 4 dependencies · 5 peers
Pi manifest JSON
{
"extensions": [
"./dist/index.js"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
tmux-pilot
Spawn subagents as real coding agent processes in detached tmux windows — full TUI observability, external control, and session file result extraction.
Features
- Full TUI visibility — each subagent runs in its own tmux window with the complete pi TUI, tool calls, thinking indicators, and model responses visible in real-time
- One session per parent — all subagents for a parent pi session are grouped in a single tmux session (
pilot-<id>) with one window per subagent - Two tmux topologies —
current-session(default) puts subagent windows on your own tmux server, natively visible;isolateduses a dedicated per-parent socket/server - Party/raid frames widget — WoW-style unit frames showing live subagent status, context headroom, turn budget, and current tool
- External control — steer subagents by typing directly in their tmux window, or use the
steer_subagenttool - Result extraction — completion detected by a per-harness lifecycle-end sentinel; results extracted from the last assistant message
- Config-dir scope — point individual roles or harnesses at curated config directories (settings, skills, agents, auth, MCP servers)
- Guard lifecycle — turn limits, grace periods, steer thresholds, concurrency caps, and optional wall-clock/staleness timeouts
- Worktree isolation — run subagents in isolated git worktrees with auto-merge for ephemeral branches
- Opt-in OS sandbox — static filesystem, network, environment, and IPC containment for child harnesses on supported Linux x64 hosts
Installation
pi install npm:@r3b1s/tmux-pilot
Requirements
- tmux must be installed on your system:
- macOS:
brew install tmux - Ubuntu/Debian:
apt install tmux - Fedora:
dnf install tmux
- macOS:
- pi CLI (obviously)
Usage
Once installed, the extension registers:
subagent— spawn one or more subagents in tmux windows, with model routing fromtmux-pilot.config.yaml(pass a batch to fan out several at once)get_subagent_result— retrieve a subagent's resultsteer_subagent— send a message to a running subagent, or stop itresume_subagent— re-prompt an existing subagent (e.g. to continue an interrupted one)
Slash commands
/tmux-pilot:toggle-ui— show/hide the subagent widget (shortcut:alt+aby default)/tmux-pilot:settings— open the settings modal/tmux-pilot:webui— open the complete local browser configuration console/tmux-pilot:log— open the debug log viewer/tmux-pilot:collect-subagent-garbage— manually reap finished or stray subagents
Full configuration console
/tmux-pilot:webui opens the full configuration surface in your default
browser. It starts a temporary server bound only to 127.0.0.1, uses a
one-time URL token exchanged for a strict local session cookie, and never
opens a LAN port. If the browser launcher is unavailable, pi shows a local URL
you can copy into a browser yourself. /tmux-pilot:settings remains the
terminal quick-settings option.
The console edits only the entry tmux-pilot.config.yaml. Imports are visible
and contribute effective values, but are deliberately read-only; form edits
create entry-document overrides and preserve unrelated YAML comments and
structure. Save is explicit, validates first, and refuses to overwrite a file
changed by another editor. Routing, lifecycle, and role appearance changes
apply to future spawns; widget topology/presentation settings apply on the
next pi session. Existing agents are never restarted or mutated.
Each role detail page also exposes its effective agent-definition Markdown
document. This remains separately saved from YAML: imported role YAML is still
read-only, while an imported role's configured Markdown can be edited or
created through the authenticated local console. The server resolves the file
from the current role configuration (the browser never submits a path), reports
shared/global inheritance and unavailable documents, and detects external
edits before atomically saving. A definition's optional frontmatter
description enriches the role enum, while its Markdown body is delivered as
role instructions on the next new subagent spawn. Definitions are limited to
64 KiB and must be valid UTF-8 with a non-empty body; a configured missing,
invalid, or oversized document rejects that spawn before a tmux window is
created. Existing/running agents and resumed conversations are never changed.
Repair stale definitions in the console's Markdown editor, then start a new
subagent.
The console defaults to loopback port 41273, so a remote host can use a
consistent tunnel:
ssh -L 127.0.0.1:41273:127.0.0.1:41273 user@remote-host
Override it under web-configuration-console.port (1024–65535). Use the same
number on both sides of the tunnel because local writes are intentionally bound
to that exact Host and Origin.
web-configuration-console:
port: 41273
For local web UI work, run pnpm webui:dev. It uses the same loopback server
and default port as /tmux-pilot:webui, serves directly from src/web, and
loads a disposable showcase configuration with every implemented harness and
all packaged art/theme variants. Refreshing the browser picks up HTML, CSS,
and JavaScript edits without a rebuild. Use --port to match a configured
console port; --config-dir is available when intentionally testing against a
real configuration.
Attaching to the tmux session
In the default current-session topology, subagent windows live in a session on your own tmux server:
tmux attach -t pilot-<parentSessionId>
In isolated topology, each parent gets a dedicated socket. Use the attach hint shown in the spawn output (tmux -S <socket> attach -t <session>).
Use tmux list-sessions (or tmux -S <socket> list-sessions for isolated) to find active sessions.
Steer by typing
While attached to the tmux session, you can type directly into any subagent's window — it's received as user input by the pi process.
Kill subagents
- From tmux: press
Ctrl+Cin the subagent's window - From the
steer_subagenttool: setkill: true
How it works
- On the first subagent spawn, tmux-pilot creates a detached tmux session named
pilot-<parentSessionId>(on your own server by default, or on a dedicated socket ifsubagent-execution.tmux-server: isolated) - Each subagent gets its own tmux window running its harness (e.g.
pi --session-id <id> "<prompt>") - A per-spawn ephemeral config directory is synthesized under tmux-pilot's host storage dir (
~/.pi/agent/tmp/subagents/<parentId>/<agentId>/) when a harness-config-directory override is configured; otherwise the harness uses its default config source - The harness writes a lifecycle-end sentinel when its agent loop ends; the extension extracts the result from the session JSONL only after the sentinel fires
- When
subagent-execution.cleanup-enabledis true (default), the subagent's tmux window, TUI process, ephemeral config dir, temp files, and ephemeral worktree are removed after it reaches a terminal state (optionally delayed bycleanup-linger-ms). The parentpilot-<id>session is kept. Tracker records are kept forget_subagent_result
Configuration
Configuration lives in ~/.pi/agent/tmux-pilot.config.yaml (or $PI_CODING_AGENT_DIR/tmux-pilot.config.yaml if set). agent-model-routing.yaml is not loaded.
Compose files
Any configuration document may import local files without a prescribed folder layout:
# tmux-pilot.config.yaml
imports:
- shared/defaults.yaml
- teams/**/*.yaml
Imports resolve relative to the file that declares them (or may be absolute); glob matches are lexical. Imports are depth-first: maps deep-merge, lists replace, later values win, and entry-file values win last. Relative agent-definition and harness-config-directory values remain relative to the entry config directory. Duplicate role names are case-insensitive, warn once with both locations, and use the later declaration. Empty globs warn; missing files, malformed files, and cycles fail configuration loading. Imports do not support environment interpolation, URLs, commands, or cross-file YAML anchors.
# tmux-pilot.config.yaml
subagent-execution:
max-concurrent-agents: 5
cleanup-enabled: true
tmux-server: current-session
subagent-widget:
display-mode: auto
placement: above-editor
toggle-key: alt+a
worktree-settings:
keep-named-worktrees: false
harness-global-defaults:
pi:
stop-key-combo: "C-c"
# cleanup-linger-ms: 0
# harness-config-directory:
# path: ~/curated/pi
# mode: overlay
cmd:
stop-key-combo: "C-c"
claude-code:
stop-key-combo: "C-c"
# cleanup-linger-ms: 15000
# harness-config-directory:
# path: ~/.claude-work
# mode: replace
subagent-global-defaults:
thinking: medium
provider: opencode-go
models:
- deepseek-v4-flash
- mimo-v2.5
worktree-enabled: false
# cleanup-linger-ms: 0 # global default delay before window teardown
lifecycle-guards:
session-file-timeout-ms: 60000
max-turns: 50
grace-turns: 5
session-poll-interval-ms: 500
staleness-timeout-ms: 0
max-runtime-ms: 0
# Opt-in; see docs/os-sandbox.md for the threat model and exact semantics.
sandbox:
enabled: false
on-unavailable: refuse-spawn
env-allowlist-extra: []
filesystem:
read: []
write: []
deny-read: []
deny-write: []
network:
default: allow
allow: []
deny: []
subagent-roles:
implementer:
thinking: high
harness: pi
worktree-enabled: true
lifecycle-guards:
max-turns: 25
turn-thresholds:
- at-turns-remaining: 5
action:
type: steer
prompt: >
You have {{turnsRemaining}} turns remaining.
Wrap up and hand off results.
models:
- deepseek-v4-flash:
provider: opencode-go
thinking: xhigh
Field reference
| Field | Scope | Description |
|---|---|---|
subagent-execution.max-concurrent-agents |
Global | Maximum agents running simultaneously. 0 = unlimited. |
subagent-execution.cleanup-enabled |
Global | When true (default), tear down terminal agents automatically (after optional linger). |
cleanup-linger-ms |
Global / harness / role | Ms to keep the finished TUI visible before cleanup. 0 = immediate. Chain: role > harness-global-defaults > subagent-global-defaults > 0. |
worktree-settings.keep-named-worktrees |
Global | When false (default), all worktrees are ephemeral. |
harness-global-defaults.<id>.stop-key-combo |
Per-harness | tmux key combo sent when grace is exceeded. |
harness-global-defaults.<id>.harness-config-directory |
Per-harness | Default curated config dir for a harness. Object { path, mode } with mode: overlay (default) or replace. |
harness-global-defaults.<id>.cleanup-linger-ms |
Per-harness | Per-harness cleanup linger override. |
subagent-execution.tmux-server |
Global | current-session (default) or isolated. Applies on next session. |
subagent-widget.display-mode |
Global | Widget layout: auto, party, or raid. |
subagent-widget.placement |
Global | Widget placement: above-editor or below-editor. |
subagent-widget.toggle-key |
Global | pi KeyId shortcut for toggling the widget. |
subagent-widget.party-frame-min-width |
Global | Minimum column width a party frame may shrink to. Positive int. |
subagent-widget.party-frame-max-width |
Global | Maximum column width a party frame may grow to. Positive int. |
subagent-global-defaults.thinking |
Global | Default thinking level inherited by bare-string models. |
subagent-global-defaults.provider |
Global | Default provider inherited by bare-string models. |
subagent-global-defaults.models |
Global | Default model fallback chain (replaced wholesale by a role's models). |
agent-definition |
Role | Path to a Markdown file whose body is delivered as the role's instructions on the next spawn. Resolved against agent-definition-directory (or the config dir). Optional frontmatter description enriches the role enum. |
agent-definition-directory |
Global / Role | Base directory for resolving relative agent-definition paths. |
subagent-global-defaults.worktree-enabled |
Global | Default worktree isolation. |
subagent-global-defaults.lifecycle-guards.* |
Global | Default guard values. |
subagent-global-defaults.cleanup-linger-ms |
Global | Global default cleanup linger (ms). |
subagent-global-defaults.sandbox.* |
Global | Default opt-in OS sandbox policy. Arrays replace wholesale; on-unavailable defaults to refuse-spawn. |
subagent-roles.<role>.harness |
Role | Harness for the role: pi (default), cmd, claude-code, codex, or hermes. |
subagent-roles.<role>.worktree-enabled |
Role | Override worktree isolation for this role. |
subagent-roles.<role>.harness-config-directory |
Role | Override config dir for this role. Object { path, mode }. |
subagent-roles.<role>.cleanup-linger-ms |
Role | Per-role cleanup linger override. |
subagent-roles.<role>.sandbox.* |
Role | Per-leaf sandbox override for the role; applies on the next spawn. |
lifecycle-guards.session-file-timeout-ms |
Per-role | Max wait (ms) for a session file to appear. |
lifecycle-guards.max-turns |
Per-role | Target turn count before grace. 0 = no limit. |
lifecycle-guards.grace-turns |
Per-role | Extra turns after max-turns before forced stop. |
lifecycle-guards.session-poll-interval-ms |
Global | Polling interval for session analysis (ms). Minimum 100. |
lifecycle-guards.staleness-timeout-ms |
Per-role | Max ms with no session-file growth before failing as wedged. 0 = disabled. |
lifecycle-guards.max-runtime-ms |
Per-role | Overall wall-clock timeout (ms). 0 = disabled. |
lifecycle-guards.turn-thresholds |
Per-role | Ordered list of thresholds that fire steer prompts. Replaced wholesale at role level. |
See Subagent OS sandbox for Linux read semantics, default-allow network warnings, protected environment families, provider requirements, the Hermes real-state consequence, audit behavior, and accepted residual risks.
Threshold actions
Each turn threshold fires when max-turns - currentTurn === at-turns-remaining. Currently supported:
steer— Sends a prompt template to the agent via tmuxsend-keys. Supports{{turnsRemaining}}and{{agentId}}template variables. Eachat-turns-remainingfires only once (deduplicated).
Harness config-directory override
Point a role or harness at a curated directory with its own settings, skills, agents, auth, and MCP servers.
overlay(default): core deep-merges the harness's base config with your overlay, symlinks auth from base, and places the result in an ephemeral per-spawn dir that is torn down with the agent.replace: the harness reads your directory directly. For pi / claude-code the dir IS the agent dir (PI_CODING_AGENT_DIR/CLAUDE_CONFIG_DIR); for cmd the dir is HOME-shaped and must contain.commandcode/.
A harness without a configProfile warns and ignores any harness-config-directory configured for it.
Supported harnesses
| Harness | CLI | Models |
|---|---|---|
pi (default) |
pi |
Any pi-routed provider/model |
cmd |
cmd |
provider/model strings |
claude-code |
claude |
Anthropic models only (haiku / sonnet / opus / …). Routing provider is ignored (never mapped to ANTHROPIC_BASE_URL). Use ambient Claude/Anthropic auth; custom gateways must set ANTHROPIC_BASE_URL in the environment themselves. |
codex |
codex |
OpenAI models only (gpt-5.4-mini, …). Routing provider is ignored; uses your ambient codex login auth. Each spawn runs in a synthesized CODEX_HOME (so subagents don't see your codex memories/state). |
hermes |
hermes |
Model and provider pass through verbatim to -m / --provider. Runs against your real ~/.hermes (memories/skills/identity preserved). |
Harness-specific operator notes:
codex— the stop key isEscape, notCtrl+C(C-cquits the codex TUI). If you run on a ChatGPT plan, rate limits are account-global, so parallel codex spawns share one usage window — usemax-concurrent-agentsto throttle.hermes— the first spawn permanently installs a small env-gated telemetry plugin into~/.hermes/plugins/tmux-pilot/(inert for your own hermes sessions).harness-config-directoryis not supported for hermes (configured values are warned-and-ignored). If you setmax_concurrent_sessionsin hermes' ownconfig.yaml, parallel spawns can fail on its active-session lease.
See docs/harness-specific-notes/ for the full
per-harness reference (auth, config, session parsing, sentinels).
Worktree Isolation
Each subagent can optionally run in its own git worktree — a fully independent working directory sharing the repository's .git object store. This provides filesystem isolation for parallel subagents: each gets its own branch checkout without interfering with others or the parent repo.
Enabling worktree isolation
Pass worktree_branch to the subagent tool for explicit branch naming, or just provide a description to auto-generate a branch:
# Explicit branch name — persistence is gated by worktree-settings.keep-named-worktrees
- subagent:
prompt: "Implement OAuth2 login flow"
description: "OAuth2 implementation"
worktree_branch: "feat/oauth2"
# Auto-generated branch (ephemeral by default — merged and removed on completion)
- subagent:
prompt: "Fix the flaky test in auth.spec.ts"
description: "Fix flaky auth test"
Branch name resolution
worktree_branch |
description |
worktree-settings.keep-named-worktrees |
Result |
|---|---|---|---|
| Set | Any | true |
Uses worktree_branch as a persistent worktree |
| Set | Any | false |
Uses worktree_branch as a display label; branch is auto-generated and ephemeral |
| Empty/whitespace | Set | any | Auto-generates <description>-<hash> (ephemeral) |
| Empty/whitespace | Not set | any | Auto-generates agent-<hash> (ephemeral) |
The hash is a 6-character FNV-1a hash of the agent ID, providing collision-resistant uniqueness even for concurrent spawns.
Path convention
Worktrees are created at:
<repo-root>/.worktrees/<branch-name>/
The .worktrees/ directory is automatically added to .gitignore.
Merge behavior
- Ephemeral worktrees (auto-generated branch names): on agent completion, the worktree is squash-merged back to the base branch (
main/master). If conflicts arise, they are surfaced in the agent result and the worktree is preserved for inspection. - Explicit worktrees (
worktree_branchset withworktree-settings.keep-named-worktrees: true): no automatic merge — the branch and worktree persist for the user to manage.
Cache sharing configuration
Create .pi/worktree.yaml in your repo root to configure which gitignored directories are copied into new worktrees (avoiding cold npm install / cargo build):
# .pi/worktree.yaml
cache_sharing:
copy_ignored:
- node_modules/
- target/
- .venv/
- dist/
symlinks:
- .pi/
- .commandcode/
Project config directories (.pi/, .commandcode/) are symlinked into worktrees by default — no copy overhead.
Error handling
- Not a git repo: if the CWD is not inside a git repository, worktree creation fails with a clear error message mentioning the path and suggesting you run from a git repo.
- Invalid branch names: branches starting with
-, containing.., ending with.lock, or containing whitespace are rejected with descriptive errors before attemptinggit worktree add. - Branch already checked out: if the branch is checked out in another worktree, an error is raised suggesting a unique name.
Symlink staging prevention
When bootstrapping a worktree, symlinks for project config directories (.pi/, .commandcode/) are added to <repo>/.git/info/exclude so they do not get staged when the agent runs git add -A. This ensures the auto-merge path stays clean — symlinked config directories are already tracked as real directories in the main repo, and staging symlink artifacts would produce confusing diffs or break the squash merge.
The exclusion is idempotent: entries are only added once, and the same info/exclude file is shared by all worktrees (per git's GIT_COMMON_DIR semantics).
Security notes
- Branch name validation: all branch names are validated before use. Names containing
.., refspec characters (^~:@{}), leading.or-,.locksuffix, or whitespace are rejected with descriptive errors. - Symlink name validation: symlink target names are checked for path traversal (
..), absolute paths, and null bytes. The resolved symlink path is also verified to remain within the worktree directory. - No shell injection: all
gitcalls useexecFileSyncwith argv arrays — no shell interpolation occurs, preventing command injection from branch names, paths, or config values. - Safe YAML parsing:
.pi/worktree.yamlis parsed withjs-yaml'sload()(safe schema by default). Invalid or non-object YAML falls back to safe defaults with a warning.
Development
This project uses pnpm and mise:
mise install # install pnpm, node, etc.
mise run test # run the test suite
mise run ci # run check + lint + test
License
Copyright (c) 2026 r3b1s
Released under the MIT License.