Skip to main content
Version: latest

sdk vs tmux

both backends implement the session.ExecutionSession interface. this page documents where their behavior diverges.

interface method comparison

methodtmuxsdk
Start(workDir string) errorcreates a tmux session and runs the program inside itselects a transport, spawns the agent subprocess via exec.Cmd, performs the app-server handshake
Restore() errorreconnects to an existing detached tmux sessionno-op — always returns nil; a fresh Session after restart fails DoesSessionExist()
Close() errorkills the tmux sessioncancels the transport context and kills the child process; safe to call multiple times
DoesSessionExist() boolcalls tmux has-sessionreturns s.alive; flips to false when the events channel closes (subprocess exited)
CapturePaneContent() (string, error)runs tmux capture-panecalls renderer.Capture() — returns all accumulated structured events joined by newlines
CapturePaneContentWithOptions(start, end string) (string, error)captures a scrollback rangecalls renderer.CaptureRange(start, end) — supports the same tmux -S/-E range semantics
HasUpdated() (bool, bool)checks content hash; detects tmux promptscompares renderer snapshot against lastContent; hasPrompt set from EventPermission / EventTurnStarted events
HasUpdatedWithContent() (bool, bool, string, bool)full poll with content and prompt detectionsame as tmux; captured is always true
GetPanePID() (int, error)returns the tmux pane PIDreturns transport.PID()
GetSanitizedName() stringreturns sanitized tmux session namereturns sanitized log file stem
SetAgentType(string)stored and used for tmux session titlestored; forwarded to LaunchConfig.AgentType at Start
SetInitialPrompt(string)sent to the pane after startdelivered via transport.SendPrompt after startup handshake
SetTaskEnv(task, wave, peers int)env vars injected at Startenv vars injected at Start via buildEnv (identical behavior)
SetSessionTitle(string)updates tmux window titlestored; used for log file path construction
SetTitleFunc(fn)callback invoked on session startstored; not called (no title lifecycle events in sdk mode)

interactive operations

the following operations require a live PTY. sdk sessions return ErrInteractiveOnly for all three:

func (s *Session) Attach() (chan struct{}, error) { return nil, ErrInteractiveOnly }
func (s *Session) DetachSafely() error { return ErrInteractiveOnly }
func (s *Session) SetDetachedSize(_, _ int) error { return ErrInteractiveOnly }
operationtmux behaviorsdk behavior
Attach()attaches the caller's terminal to the tmux sessionErrInteractiveOnly
DetachSafely()detaches gracefullyErrInteractiveOnly
SetDetachedSize(width, height int)resizes the detached paneErrInteractiveOnly

text input is supported

unlike the removed session/headless backend, sdk sessions do support text input:

operationtmux behaviorsdk behavior
SendKeys(keys string)sends key sequence to the panebuffers text; "\x03" (ctrl-C) triggers transport.Interrupt instead
TapEnter()sends a carriage returndrains the buffer and calls transport.SendPrompt(ctx, bufferedText)
SendPermissionResponse(choice)sends the permission dialog response keycalls transport.RespondPermission(ctx, choice) with a 10-second timeout

text entry is buffered and submitted. SendKeys accumulates text in promptBuf; TapEnter flushes it as a single SendPrompt call. this matches the orchestration layer's existing call pattern without any changes.

capture range semantics

CapturePaneContentWithOptions(start, end string) behaves differently from the removed headless backend:

tmuxsdkold headless (removed)
start/end paramsused as -S/-E scrollback rangeforwarded to Renderer.CaptureRange — same semanticsignored — always returned full buffer

Renderer.CaptureRange resolves range strings exactly as tmux does:

  • "" or "-" → beginning (start) or end (end) of buffer
  • non-negative integer → 0-based line index
  • negative integer → offset from the last line

permission prompt detection

tmux sessions detect permission prompts by scanning pane output for known patterns. sdk sessions receive structured EventPermission notifications from the transport instead. hasPrompt is set when EventPermission arrives and cleared when EventTurnStarted arrives.

consequence: sdk sessions handle permission responses more reliably than tmux — the transport delivers a typed permission event with a stable permission_id rather than requiring pattern matching on terminal output.

embedded terminal

the TUI's embedded terminal feature requires a tmux PTY:

// session/instance_session.go
func (i *Instance) NewEmbeddedTerminalForInstance(cols, rows int) (*EmbeddedTerminal, error) {
if i.ExecutionMode == ExecutionModeSDK {
return nil, ErrInteractiveOnly
}
// ...
}

sdk instances display rendered structured output in the preview pane. focus mode (the embedded interactive terminal) is only available for tmux-backed instances.

choosing the right backend

program is claude or codex? → sdk is available
program is opencode, gemini, amp? → tmux only (sdk silently falls back anyway)
need to attach interactively? → tmux
running in a CI pipeline? → sdk (claude/codex) or tmux (everything else)
running parallel wave agents? → sdk preferred for claude/codex (lower overhead)
need kas monitor live attach? → tmux

you can mix modes within a single plan:

[agents.coder]
enabled = true
program = "claude"
flags = ["--permission-mode bypassPermissions"]
execution_mode = "sdk"

[agents.reviewer]
enabled = true
program = "claude"
execution_mode = "tmux"