Skip to main content
Version: 2.5.0

headless vs tmux

Both backends implement the session.ExecutionSession interface. This page documents where their behavior diverges.

interface method comparison

methodtmuxheadless
Start(workDir string) errorcreates a tmux session and runs the program inside itspawns the program directly via exec.Cmd; creates .kasmos/logs/
Restore() errorreconnects to an existing detached tmux sessionno-op — always returns nil
Close() errorkills the tmux sessionkills the child process; idempotent, safe to call multiple times
DoesSessionExist() boolcalls tmux has-sessionreads s.done channel; false once the process exits
CapturePaneContent() (string, error)runs tmux capture-panereads in-memory bytes.Buffer
CapturePaneContentWithOptions(start, end string) (string, error)captures a scrollback rangeignores start/end; returns full buffer
HasUpdated() (bool, bool)checks content hash; detects tmux promptscompares buffer snapshot; hasPrompt always false
HasUpdatedWithContent() (bool, bool, string, bool)full poll with content and prompt detectionfull poll; hasPrompt always false, captured always true
GetPanePID() (int, error)returns the tmux pane PIDreturns cmd.Process.Pid
GetSanitizedName() stringreturns sanitized tmux session namereturns sanitized log file stem
SetAgentType(string)stored and used for tmux session titlestored only; not used
SetInitialPrompt(string)sent to the pane after startstored only; not forwarded to the program
SetTaskEnv(task, wave, peers int)sets env vars injected at Startsets env vars injected at Start (identical behavior)
SetSessionTitle(string)updates tmux window titlestored but no-op
SetTitleFunc(fn)callback invoked on session startstored but no-op

interactive operations

The following operations require a live PTY. Headless sessions return ErrInteractiveOnly for all of them:

// ErrInteractiveOnly = errors.New("interactive operation requires tmux execution")

func (s *Session) Attach() (chan struct{}, error) {
return nil, ErrInteractiveOnly
}

func (s *Session) SendKeys(_ string) error { return ErrInteractiveOnly }

func (s *Session) TapEnter() error { return ErrInteractiveOnly }

func (s *Session) SendPermissionResponse(_ tmux.PermissionChoice) error {
return ErrInteractiveOnly
}

func (s *Session) SetDetachedSize(_, _ int) error { return ErrInteractiveOnly }
operationtmux behaviorheadless behavior
Attach()attaches the caller's terminal to the tmux session; returns a channel that closes on detachErrInteractiveOnly
SendKeys(keys string)sends key sequence to the paneErrInteractiveOnly
TapEnter()sends a carriage return to the paneErrInteractiveOnly
SendPermissionResponse(choice)sends the permission dialog response keyErrInteractiveOnly
SetDetachedSize(width, height int)resizes the detached paneErrInteractiveOnly

no-ops vs errors

Not all headless limitations are errors. Some operations are silently accepted as no-ops because they have no meaningful equivalent but do not indicate a broken state:

operationheadless behaviorrationale
Restore()returns nilno persistent session to reconnect to
DetachSafely()returns nilnothing to detach from; the process keeps running
SetSessionTitle(string)stores value; no effectno tmux window to rename
SetTitleFunc(fn)stores callback; never calledno title lifecycle events

permission prompts

tmux sessions detect permission prompts by scanning pane output for known patterns. headless sessions cannot do this — hasPrompt is always false in both HasUpdated() and HasUpdatedWithContent(). The ParsePermissionPrompt path in CollectMetadata still runs (it checks the content string), but SendPermissionResponse cannot be called to respond.

consequence: if your agent program pauses waiting for a permission confirmation, a headless session will stall indefinitely. Use --permission-mode bypassPermissions (or equivalent flags) for programs that would otherwise prompt, or use tmux mode so kasmos can detect and respond to prompts automatically.

embedded terminal

The TUI's embedded terminal feature (NewEmbeddedTerminalForInstance) requires a tmux PTY:

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

Headless instances display as read-only log output in the preview pane. Focus mode (the embedded interactive terminal) is only available for tmux-backed instances.

choosing the right backend

need to attach interactively? → tmux
agent may request permissions? → tmux (unless --permission-mode bypassPermissions)
running in a headless CI pipeline? → headless
running parallel wave agents? → headless (lower overhead)
need kas monitor live attach? → tmux

You can mix modes within a single plan — for example, run coders headlessly and the reviewer in tmux:

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

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