Skip to content

Proposal: CustomPaneBackend protocol — decouple agent teams from tmux CLI to unblock Ghostty, WezTerm, Zellij, KILD, and remote deployments #26572

@Wirasm

Description

@Wirasm

Who I Am and Why I'm Writing This

I'm the creator of KILD, a tool for running parallel AI agents in isolated git worktrees. KILD ships a daemon that manages PTYs natively — it is, functionally, a terminal multiplexer purpose-built for agentic workflows. I have agent teams working inside KILD daemon sessions already.

I'm writing this because I've done the work to make agent teams work inside a non-tmux environment, which means I've reverse-engineered exactly what Claude Code needs from a pane backend. What I found is that Claude Code's dependency on tmux is causing compounding problems — for users, for the codebase, and for every terminal/multiplexer that wants to offer a first-class experience.

KILD itself needs this. So does Ghostty, WezTerm, Zellij, and any other tool that manages terminals and wants agent teams to work natively inside them. This isn't a "please add KILD support" request. It's a proposal to fix the underlying architecture in a way that benefits Anthropic, fixes open bugs, and unblocks an entire class of consumers simultaneously.


The Problem: tmux Has Leaked Into Claude Code's API Surface

Agent teams currently treat tmux as an interface contract. Claude Code calls approximately 20 distinct tmux subcommands to manage the lifetime of teammates. This means tmux's CLI is effectively Claude Code's pane management API — and it was never designed to be one.

The consequences are real and already showing up in your issue tracker:

These are not five separate requests. They are the same architectural gap filing in from different directions.


What It Looks Like to Work Around This Today

KILD ships a binary called kild-tmux-shim — a drop-in replacement for tmux that intercepts every command Claude Code issues and routes it to KILD's daemon via IPC. When a KILD session starts, we prepend ~/.kild/bin/ to $PATH and symlink the shim there as tmux. Claude Code sees $TMUX in the environment, finds tmux on the path, and proceeds normally.

The shim handles all ~20 tmux subcommands Claude Code issues. Most of them are no-ops (select-layout, resize-pane). The real work is:

split-window    → daemon: CreateSession (new PTY)
send-keys       → daemon: WriteStdin
kill-pane       → daemon: DestroySession
capture-pane    → daemon: ReadScrollback
display-message → local state lookup (pane ID, session name)
list-panes      → local pane registry

This works. Teammates run in daemon PTYs, output is captured, the race condition doesn't exist because our pane registry uses file-based locking. But it's fragile: any new tmux command Claude Code adds is a silent breakage until we notice. And it's an elaborate impersonation of something that should just be a clean interface.

The shim is proof that the real requirements are far simpler than tmux's surface area. It also demonstrates that a clean protocol is implementable — KILD has been running it in production.


What Claude Code Actually Needs

The shim exercise revealed that the real requirements are much simpler than tmux's surface area. Claude Code needs seven operations:

Operation Purpose
spawn_agent(argv[], cwd, env, metadata) Start a teammate process
write(context_id, data) Send to stdin
capture(context_id, lines?) Read scrollback
kill(context_id) Terminate
list() Enumerate live contexts
get_self_id() (via initialize) "What context am I running in?"
push: context_exited(context_id, code) Notification when context exits

That's it. Everything else — has-session, new-session, new-window, select-layout, resize-pane, break-pane, join-pane, border styling — is tmux-specific plumbing that has no business being in Claude Code's model.

Two other things worth noting:

spawn_agent should take argv[], not a shell string. Currently Claude Code constructs a shell command and sends it via send-keys into a running shell. A proper protocol can take the exact argv — no shell interpolation, no quoting edge cases, deterministic behavior.

The backend should own rendering. Claude Code currently calls select-pane -P "bg=...", set-option pane-border-format, etc. This is wrong. The coordinator should pass intent (metadata: {name, color, role}), and the backend should decide how to present it. Ghostty renders this differently than tmux. That's fine — it's their terminal.


The Proposal: CustomPaneBackend Protocol

Define a minimal JSON-RPC 2.0 protocol over NDJSON. Backends register via environment variable or config:

# Spawn-on-demand: Claude Code starts the backend process
CLAUDE_PANE_BACKEND=/path/to/binary

# Pre-running server: Claude Code connects to socket
CLAUDE_PANE_BACKEND_SOCKET=/path/to/server.sock

Both transports speak identical JSON-RPC.

Handshake

// Claude Code → Backend
{"id":"1","method":"initialize","params":{"protocol_version":"1","capabilities":["events"]}}

// Backend → Claude Code
{"id":"1","result":{
  "protocol_version": "1",
  "capabilities": ["events", "capture"],
  "self_context_id": "ctx_0"
}}

self_context_id replaces tmux display-message -p "#{pane_id}". The backend knows which context Claude Code is running in and declares it at initialization.

Operations

// Spawn a teammate — argv[], never a shell string
{"id":"2","method":"spawn_agent","params":{
  "command": ["claude","--agent-id","researcher@my-team","--parent-session-id","abc123"],
  "cwd": "/project",
  "env": {"CLAUDECODE":"1"},
  "metadata": {"name":"researcher","color":"blue","role":"teammate"}
}}
→ {"id":"2","result":{"context_id":"ctx_1"}}

// Write to stdin
{"id":"3","method":"write","params":{"context_id":"ctx_1","data":"<base64>"}}
→ {"id":"3","result":{}}

// Read scrollback (optional capability)
{"id":"4","method":"capture","params":{"context_id":"ctx_1","lines":200}}
→ {"id":"4","result":{"text":"..."}}

// Terminate
{"id":"5","method":"kill","params":{"context_id":"ctx_1"}}
→ {"id":"5","result":{}}

// List live contexts
{"id":"6","method":"list","params":{}}
→ {"id":"6","result":{"contexts":["ctx_0","ctx_1"]}}

Push Events (Backend → Claude Code, unsolicited)

// Essential — pushed when any context exits
{"method":"context_exited","params":{"context_id":"ctx_1","exit_code":0}}

// Optional capability — stream output if declared in initialize
{"method":"context_output","params":{"context_id":"ctx_1","data":"<base64>"}}

Why This Benefits Anthropic Directly

It fixes #23615. The race condition exists because split-window and send-keys are separate stateless subprocesses. A persistent backend process serializes all spawn_agent calls naturally. No sleep needed.

It fixes #23572. Silent fallback happens because backend detection is unreliable inference from env vars and PATH. Explicit registration via CLAUDE_PANE_BACKEND makes failure loud and diagnosable.

It unblocks KILD, #24189, #24122, and #23574 simultaneously. KILD, Ghostty, Zellij, and WezTerm each get a clean path to native integration by implementing this protocol. You write the interface once; the ecosystem implements it.

It makes your own codebase cleaner. The tmux integration in Claude Code is currently responsible for pane IDs, split geometry, session names, window indices, border styling, and layout management. None of that belongs in the coordinator. Claude Code's agent teams logic becomes purely about orchestration.

It enables remote deployments. The socket transport variant (CLAUDE_PANE_BACKEND_SOCKET) allows the backend to run on a different machine — running agents on a remote build server while monitoring from a laptop becomes trivially possible.


What I'm Offering

KILD's daemon already implements everything a backend needs on the server side. The shim already implements the client translation layer. Rewriting the shim to speak this protocol instead of impersonating tmux is a small, well-scoped change on our end the moment Claude Code supports it. KILD can serve as the reference implementation for this protocol — working, tested, and deployed.

I'm happy to:

  • Collaborate on the protocol spec
  • Implement the KILD backend and document the implementation as a reference
  • Test against Claude Code builds before release
  • Review the Claude Code-side integration if that's useful

The goal is a protocol stable enough that KILD, Ghostty, WezTerm, and Zellij can implement it independently and have it just work.


Summary

The five open issues linked above are not isolated requests. They share a root cause: tmux's CLI is being used as a pane management interface it was never designed to be. A small, well-designed protocol eliminates all of them and gives Claude Code's agent teams a proper extension point for any tool — terminal emulator, multiplexer, or purpose-built agent runner — that wants to participate natively.

I've done the work to understand what the minimal interface actually is. Happy to go deeper on any part of the design.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions