Skip to content

Instantly share code, notes, and snippets.

@0xBigBoss
Created January 30, 2026 17:05
Show Gist options
  • Select an option

  • Save 0xBigBoss/625a297065c996820201f42ed9d1d076 to your computer and use it in GitHub Desktop.

Select an option

Save 0xBigBoss/625a297065c996820201f42ed9d1d076 to your computer and use it in GitHub Desktop.
RFC: Transparent tmux Process Manager for Claude Code

RFC: Transparent tmux Process Manager for Claude Code

Status: Draft Author: Allen Eubank Date: 2026-01-30

Problem

Long-running processes spawned by Claude Code sessions (dev servers, Tilt, watchers) routinely become orphaned — detached from any terminal (TTY: ??), re-parented to launchd (PPID 1), invisible unless you manually hunt with lsof/ps/pstree. This happens when:

  • A Claude Code session ends or is compacted
  • The Bash tool times out on a blocking command
  • A tool like Tilt forks child processes that outlive their parent

These orphans hold ports, consume resources, and require manual forensics to find and kill.

Goal

Every long-running process Claude Code spawns should be visible and recoverable via tmux while remaining transparent to Claude Code's existing Bash tool interface — stdout, stderr, exit codes, and timeouts all work unchanged.

Current State

We have a tmux-processes skill that teaches Claude best practices for tmux session management (naming conventions, send-keys patterns, capture-pane monitoring, idempotent starts). This is advisory only — Claude can and does ignore it, running commands directly via the Bash tool.

Proposed Approach

Architecture

┌─────────────────────────────────────────────────────┐
│ Claude Code                                         │
│                                                     │
│  Bash tool call                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │ command: "npm run dev --port 5174"             │  │
│  │ run_in_background: false                       │  │
│  └──────────────┬────────────────────────────────┘  │
│                 │                                    │
│  ┌──────────────▼────────────────────────────────┐  │
│  │ PreToolUse Hook (Bash matcher)                 │  │
│  │                                                │  │
│  │  1. Read tool_input.command                    │  │
│  │  2. Match against long-running patterns        │  │
│  │  3. If match: rewrite command to use wrapper   │  │
│  │  4. Return updatedInput + permissionDecision   │  │
│  └──────────────┬────────────────────────────────┘  │
│                 │                                    │
│  ┌──────────────▼────────────────────────────────┐  │
│  │ Bash executes (rewritten command)              │  │
│  │                                                │  │
│  │  claude-tmux-wrap --name dev-5174 \            │  │
│  │    -- npm run dev --port 5174                  │  │
│  └──────────────┬────────────────────────────────┘  │
│                 │                                    │
└─────────────────┼───────────────────────────────────┘
                  │
   ┌──────────────▼──────────────────────────────────┐
   │ claude-tmux-wrap (wrapper binary/script)         │
   │                                                  │
   │  1. Generate session name from project context   │
   │  2. Create tmux session + window                 │
   │  3. Start command inside tmux pane               │
   │  4. Proxy stdout/stderr via FIFOs → own stdout   │
   │  5. Block until inner command exits              │
   │  6. Return inner command's real exit code        │
   │                                                  │
   │  If wrapper is killed (session dies, timeout):   │
   │  → tmux session survives                         │
   │  → user can: tmux attach -t <session>            │
   └─────────────────────────────────────────────────┘

Components

1. claude-tmux-wrap — Wrapper Script

A standalone script (bash or Go) that:

  • Accepts a command and optional session/window name
  • Creates a tmux session (or adds a window to an existing project session)
  • Runs the command inside the tmux pane
  • Proxies stdout/stderr back to the wrapper's own file descriptors via FIFOs
  • Blocks until the inner command exits, then exits with its real exit code
  • On abnormal termination (SIGTERM, SIGKILL), the tmux session persists

Session naming follows the existing convention: derived from git rev-parse --show-toplevel or $PWD.

2. PreToolUse Hook — Command Interceptor

A hook script registered in Claude Code settings that:

  • Fires on every Bash tool invocation
  • Pattern-matches the command against a configurable list of long-running indicators (e.g., tilt up, npm run dev, vite, next dev, yarn .* dev, docker compose up, process on known ports)
  • If matched: rewrites tool_input.command to claude-tmux-wrap --name <derived> -- <original command>
  • If not matched: passes through unchanged (exit 0, no JSON output)

3. Existing tmux-processes Skill — Monitoring & Recovery

Already implemented. Teaches Claude how to:

  • tmux capture-pane for log inspection
  • tmux list-windows for process inventory
  • tmux send-keys C-c for stopping processes
  • tmux attach guidance for human takeover

No changes needed. This skill covers the "read" side; the hook + wrapper add the "write" side.

Transparency Matrix

Concern Behavior with wrapper Notes
stdout Proxied via FIFO to wrapper stdout → Claude sees it Use script -q or stdbuf -oL for line buffering
stderr Proxied via separate FIFO → Claude sees it Merged or separate, configurable
Exit code Inner command's code written to temp file, wrapper returns it Fully transparent
Bash timeout Wrapper killed, tmux survives Desired: process recoverable
Session end Wrapper killed, tmux survives Desired: process recoverable
stdin Not proxied (command reads from tmux PTY) Acceptable: Claude rarely pipes stdin
TTY dimensions Command sees tmux PTY, not Claude's Minor: may affect column-dependent output
Signal forwarding Wrapper traps SIGINT/SIGTERM, forwards to tmux pane Ctrl-C behavior preserved
run_in_background Same wrapping; Claude polls via capture-pane per skill Replaces TaskOutput polling

Pattern Matching Strategy

The hook uses an allowlist of patterns for commands that should be wrapped. Conservative by default — only wrap known long-running patterns:

# Long-running server/watcher patterns
tilt up|tilt ci
npm run (dev|start|serve|watch)
yarn (dev|start|serve|watch)
npx (vite|next|remix|astro) dev
docker compose up
python.* (manage\.py runserver|uvicorn|gunicorn|flask run)
go run .* (serve|server)

Commands NOT matched pass through to Bash unchanged. False negatives (missed patterns) are safe — the process just runs without tmux as it does today. False positives (unnecessary wrapping) add minor overhead but don't break behavior.

The pattern list is configurable — stored in a file alongside the hook, not hard-coded.

Session Layout

Processes from the same project share a tmux session with separate windows:

tmux session: "canton-monorepo"
  ├── window "dev-5174"    → vite dev server
  ├── window "tilt"        → tilt up
  └── window "test-watch"  → vitest --watch

This matches the convention already documented in the tmux-processes skill.

Alternatives Considered

Approach Why not (as primary)
MCP server with custom tools Not transparent — Claude must choose the MCP tool over Bash. Requires denying Bash access (too aggressive) or relying on Claude preference (unreliable).
Overmind (Procfile manager) Only handles predefined process sets. Ad-hoc commands from Claude don't have Procfiles. Good complement, not a replacement.
PostToolUse audit hook Detects orphans after the fact, doesn't prevent them. Educational but not a solution.
Advisory skill only Current state. Claude ignores it when it wants to.
run_in_background only No persistence across sessions. Cleaned up when Claude exits. No tmux recovery.

Open Questions

  1. Wrapper language — Bash script is simplest. Go binary is more robust (FIFO management, signal handling, cross-platform). Worth the build step?

  2. Output fidelity — FIFOs lose PTY features (colors, cursor control). Use script -q /dev/null to preserve PTY semantics? Or accept clean text output as a feature?

  3. Pattern list ownership — Should the hook hard-code patterns, read from a config file, or derive from the project (e.g., parse package.json scripts)?

  4. Cleanup policy — When should tmux sessions be cleaned up? Manual only? After N hours idle? On next Claude session start?

  5. Multiple Claude sessions — Two Claude sessions in the same project could collide on tmux session names. Namespace by session ID, or share the session and let windows accumulate?

  6. Interaction with Tilt — Tilt already manages its own process tree. Should tilt up be wrapped (making Tilt visible in tmux) or excluded (since Tilt has its own UI)?

Risks

  • FIFO lifecycle — If the wrapper crashes without cleaning up FIFOs, stale named pipes accumulate in /tmp. Mitigated by using mktemp and trap-based cleanup.
  • Hook performance — PreToolUse hook runs on every Bash invocation. Must be fast (< 100ms). Pattern matching is cheap; the risk is in the hook startup time if using a heavy runtime.
  • Double wrapping — If Claude explicitly uses tmux (following the skill) AND the hook wraps it again, you get nested tmux. Hook must detect and skip commands that already reference tmux.

Implementation Plan

  1. Write claude-tmux-wrap script with FIFO proxying + exit code forwarding
  2. Write PreToolUse hook with pattern matching and command rewriting
  3. Add hook to Claude Code settings (~/.claude/settings.json)
  4. Test: foreground commands (blocking, stdout, exit codes)
  5. Test: background commands (run_in_background: true)
  6. Test: timeout/kill behavior (wrapper dies, tmux survives)
  7. Test: double-wrap detection (skip if command already uses tmux)
  8. Add pattern config file, document how to extend
  9. Update tmux-processes skill to reference the new automatic wrapping
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment