@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.

Packages

Package details

extension

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 topologiescurrent-session (default) puts subagent windows on your own tmux server, natively visible; isolated uses 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_subagent tool
  • 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
  • pi CLI (obviously)

Usage

Once installed, the extension registers:

  • subagent — spawn one or more subagents in tmux windows, with model routing from tmux-pilot.config.yaml (pass a batch to fan out several at once)
  • get_subagent_result — retrieve a subagent's result
  • steer_subagent — send a message to a running subagent, or stop it
  • resume_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+a by 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+C in the subagent's window
  • From the steer_subagent tool: set kill: true

How it works

  1. 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 if subagent-execution.tmux-server: isolated)
  2. Each subagent gets its own tmux window running its harness (e.g. pi --session-id <id> "<prompt>")
  3. 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
  4. 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
  5. When subagent-execution.cleanup-enabled is 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 by cleanup-linger-ms). The parent pilot-<id> session is kept. Tracker records are kept for get_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 tmux send-keys. Supports {{turnsRemaining}} and {{agentId}} template variables. Each at-turns-remaining fires 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 is Escape, not Ctrl+C (C-c quits the codex TUI). If you run on a ChatGPT plan, rate limits are account-global, so parallel codex spawns share one usage window — use max-concurrent-agents to 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-directory is not supported for hermes (configured values are warned-and-ignored). If you set max_concurrent_sessions in hermes' own config.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_branch set with worktree-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 attempting git 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 -, .lock suffix, 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 git calls use execFileSync with argv arrays — no shell interpolation occurs, preventing command injection from branch names, paths, or config values.
  • Safe YAML parsing: .pi/worktree.yaml is parsed with js-yaml's load() (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.