GoHome

Choose documentation 3

latest

Current version

latest
English

Early release libtmux is released, and its APIs may still change between releases. This site's structure and URLs may change too.

libtmux for Go

Go-based tmux client

tmux client library for Go. Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, watch for changes via control mode, capture pane contents

Product
Capability: Navigate tmux via a typed object graph
Example details
// Command quickstart lays out a tmux session, then walks it as objects.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/libtmux/libtmux-go/tmux"
)
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
// A private server: a socket and an empty config of its own, so this
// neither touches nor is changed by a tmux you already use.
// The process ID in the socket name keeps one run apart from the next.
server, err := tmux.NewServer(tmux.ServerOptions{
SocketName: fmt.Sprintf("libtmux-quickstart-%d", os.Getpid()),
ConfigFile: os.DevNull,
})
if err != nil {
return err
}
session, err := server.NewSession(ctx, tmux.NewSessionRequest{
Name: "demo",
})
if err != nil {
return err
}
// Lay out the session: name its window, split it, add a second window.
editor, err := session.ResolveActiveWindow(ctx)
if err != nil {
return err
}
editor, err = editor.Rename(ctx, "editor")
if err != nil {
return err
}
left, err := editor.ResolveActivePane(ctx)
if err != nil {
return err
}
right, err := left.Split(ctx, tmux.SplitPaneRequest{
Direction: tmux.PaneDirectionRight,
})
if err != nil {
return err
}
name := "logs"
logs, err := session.NewWindow(ctx, tmux.NewWindowRequest{
Name: &name,
})
if err != nil {
return err
}
// Type a command into the new pane and wait for what it prints.
command := "printf 'hello from %s\\n' tmux"
err = right.SendKeys(ctx, tmux.SendKeysRequest{Command: &command})
if err != nil {
return err
}
_, err = right.WaitForText(ctx, tmux.WaitForTextRequest{
Text: "hello from tmux",
})
if err != nil {
return err
}
// Move the focus an attached client sees: the pane, then the window.
_, err = right.Select(ctx, tmux.PaneSelectRequest{})
if err != nil {
return err
}
_, err = logs.Select(ctx)
if err != nil {
return err
}
// Walk the objects: down from the server, then up from one pane.
snapshot, err := server.Snapshot(ctx)
if err != nil {
return err
}
for _, each := range snapshot.Sessions() {
sessionName, _ := each.Name()
fmt.Println("session", sessionName)
windows, _ := each.Windows()
for _, window := range windows {
name, _ := window.Name()
fmt.Printf(" window %d:%s:", window.Index(), name)
panes, _ := window.Panes()
for _, pane := range panes {
fmt.Print(" ", pane.ID())
}
fmt.Println()
}
}
pane, _ := snapshot.PaneByID(right.ID())
window, _ := pane.Window()
owner, _ := window.Session()
windowName, _ := window.Name()
sessionName, _ := owner.Name()
fmt.Println(pane.ID(), "is in window", windowName,
"of session", sessionName)
return nil
}
// Command quickstart lays out a tmux session, then walks it as objects.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/libtmux/libtmux-go/tmux"
)
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
// A private server: a socket and an empty config of its own, so this
// neither touches nor is changed by a tmux you already use.
// The process ID in the socket name keeps one run apart from the next.
server, err := tmux.NewServer(tmux.ServerOptions{
SocketName: fmt.Sprintf("libtmux-quickstart-%d", os.Getpid()),
ConfigFile: os.DevNull,
})
if err != nil {
return fmt.Errorf("configure tmux: %w", err)
}
session, err := server.NewSession(ctx, tmux.NewSessionRequest{
Name: "demo",
})
if err != nil {
return fmt.Errorf("create session: %w", err)
}
// Lay out the session: name its window, split it, add a second window.
editor, err := session.ResolveActiveWindow(ctx)
if err != nil {
return fmt.Errorf("find window: %w", err)
}
editor, err = editor.Rename(ctx, "editor")
if err != nil {
return fmt.Errorf("rename window: %w", err)
}
left, err := editor.ResolveActivePane(ctx)
if err != nil {
return fmt.Errorf("find pane: %w", err)
}
right, err := left.Split(ctx, tmux.SplitPaneRequest{
Direction: tmux.PaneDirectionRight,
})
if err != nil {
return fmt.Errorf("split pane: %w", err)
}
name := "logs"
logs, err := session.NewWindow(ctx, tmux.NewWindowRequest{
Name: &name,
})
if err != nil {
return fmt.Errorf("add window: %w", err)
}
// Type a command into the new pane and wait for what it prints.
command := "printf 'hello from %s\\n' tmux"
err = right.SendKeys(ctx, tmux.SendKeysRequest{Command: &command})
if err != nil {
return fmt.Errorf("send keys: %w", err)
}
_, err = right.WaitForText(ctx, tmux.WaitForTextRequest{
Text: "hello from tmux",
})
if err != nil {
return fmt.Errorf("wait for output: %w", err)
}
// Move the focus an attached client sees: the pane, then the window.
_, err = right.Select(ctx, tmux.PaneSelectRequest{})
if err != nil {
return fmt.Errorf("select pane: %w", err)
}
_, err = logs.Select(ctx)
if err != nil {
return fmt.Errorf("select window: %w", err)
}
// Walk the objects: down from the server, then up from one pane.
snapshot, err := server.Snapshot(ctx)
if err != nil {
return fmt.Errorf("read tmux: %w", err)
}
for _, each := range snapshot.Sessions() {
sessionName, _ := each.Name()
fmt.Println("session", sessionName)
windows, _ := each.Windows()
for _, window := range windows {
name, _ := window.Name()
fmt.Printf(" window %d:%s:", window.Index(), name)
panes, _ := window.Panes()
for _, pane := range panes {
fmt.Print(" ", pane.ID())
}
fmt.Println()
}
}
pane, _ := snapshot.PaneByID(right.ID())
window, _ := pane.Window()
owner, _ := window.Session()
windowName, _ := window.Name()
sessionName, _ := owner.Name()
fmt.Println(pane.ID(), "is in window", windowName,
"of session", sessionName)
return nil
}
// Command quickstart lays out a tmux session, then walks it as objects.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"github.com/libtmux/libtmux-go/tmux"
)
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) (err error) {
// A private server: a socket and an empty config of its own, so this
// neither touches nor is changed by a tmux you already use.
// The process ID in the socket name keeps one run apart from the next.
server, err := tmux.NewServer(tmux.ServerOptions{
SocketName: fmt.Sprintf("libtmux-quickstart-%d", os.Getpid()),
ConfigFile: os.DevNull,
})
if err != nil {
return err
}
session, err := server.NewSession(ctx, tmux.NewSessionRequest{
Name: "demo",
})
if err != nil {
return err
}
// Cleanup runs even when ctx is the reason the program is leaving.
defer func() {
err = errors.Join(err, server.Kill(context.WithoutCancel(ctx)))
}()
// Lay out the session: name its window, split it, add a second window.
editor, err := session.ResolveActiveWindow(ctx)
if err != nil {
return err
}
editor, err = editor.Rename(ctx, "editor")
if err != nil {
return err
}
left, err := editor.ResolveActivePane(ctx)
if err != nil {
return err
}
right, err := left.Split(ctx, tmux.SplitPaneRequest{
Direction: tmux.PaneDirectionRight,
})
if err != nil {
return err
}
name := "logs"
logs, err := session.NewWindow(ctx, tmux.NewWindowRequest{
Name: &name,
})
if err != nil {
return err
}
// Type a command into the new pane and wait for what it prints.
command := "printf 'hello from %s\\n' tmux"
err = right.SendKeys(ctx, tmux.SendKeysRequest{Command: &command})
if err != nil {
return err
}
_, err = right.WaitForText(ctx, tmux.WaitForTextRequest{
Text: "hello from tmux",
})
if err != nil {
return err
}
// Move the focus an attached client sees: the pane, then the window.
_, err = right.Select(ctx, tmux.PaneSelectRequest{})
if err != nil {
return err
}
_, err = logs.Select(ctx)
if err != nil {
return err
}
// Walk the objects: down from the server, then up from one pane.
snapshot, err := server.Snapshot(ctx)
if err != nil {
return err
}
for _, each := range snapshot.Sessions() {
sessionName, _ := each.Name()
fmt.Println("session", sessionName)
windows, _ := each.Windows()
for _, window := range windows {
name, _ := window.Name()
fmt.Printf(" window %d:%s:", window.Index(), name)
panes, _ := window.Panes()
for _, pane := range panes {
fmt.Print(" ", pane.ID())
}
fmt.Println()
}
}
pane, _ := snapshot.PaneByID(right.ID())
window, _ := pane.Window()
owner, _ := window.Session()
windowName, _ := window.Name()
sessionName, _ := owner.Name()
fmt.Println(pane.ID(), "is in window", windowName,
"of session", sessionName)
return nil
}
// Command quickstart lays out a tmux session, then walks it as objects.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"github.com/libtmux/libtmux-go/tmux"
)
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) (err error) {
// A private server: a socket and an empty config of its own, so this
// neither touches nor is changed by a tmux you already use.
// The process ID in the socket name keeps one run apart from the next.
server, err := tmux.NewServer(tmux.ServerOptions{
SocketName: fmt.Sprintf("libtmux-quickstart-%d", os.Getpid()),
ConfigFile: os.DevNull,
})
if err != nil {
return fmt.Errorf("configure tmux: %w", err)
}
session, err := server.NewSession(ctx, tmux.NewSessionRequest{
Name: "demo",
})
if err != nil {
return fmt.Errorf("create session: %w", err)
}
// Cleanup runs even when ctx is the reason the program is leaving.
defer func() {
err = errors.Join(err, server.Kill(context.WithoutCancel(ctx)))
}()
// Lay out the session: name its window, split it, add a second window.
editor, err := session.ResolveActiveWindow(ctx)
if err != nil {
return fmt.Errorf("find window: %w", err)
}
editor, err = editor.Rename(ctx, "editor")
if err != nil {
return fmt.Errorf("rename window: %w", err)
}
left, err := editor.ResolveActivePane(ctx)
if err != nil {
return fmt.Errorf("find pane: %w", err)
}
right, err := left.Split(ctx, tmux.SplitPaneRequest{
Direction: tmux.PaneDirectionRight,
})
if err != nil {
return fmt.Errorf("split pane: %w", err)
}
name := "logs"
logs, err := session.NewWindow(ctx, tmux.NewWindowRequest{
Name: &name,
})
if err != nil {
return fmt.Errorf("add window: %w", err)
}
// Type a command into the new pane and wait for what it prints.
command := "printf 'hello from %s\\n' tmux"
err = right.SendKeys(ctx, tmux.SendKeysRequest{Command: &command})
if err != nil {
return fmt.Errorf("send keys: %w", err)
}
_, err = right.WaitForText(ctx, tmux.WaitForTextRequest{
Text: "hello from tmux",
})
if err != nil {
return fmt.Errorf("wait for output: %w", err)
}
// Move the focus an attached client sees: the pane, then the window.
_, err = right.Select(ctx, tmux.PaneSelectRequest{})
if err != nil {
return fmt.Errorf("select pane: %w", err)
}
_, err = logs.Select(ctx)
if err != nil {
return fmt.Errorf("select window: %w", err)
}
// Walk the objects: down from the server, then up from one pane.
snapshot, err := server.Snapshot(ctx)
if err != nil {
return fmt.Errorf("read tmux: %w", err)
}
for _, each := range snapshot.Sessions() {
sessionName, _ := each.Name()
fmt.Println("session", sessionName)
windows, _ := each.Windows()
for _, window := range windows {
name, _ := window.Name()
fmt.Printf(" window %d:%s:", window.Index(), name)
panes, _ := window.Panes()
for _, pane := range panes {
fmt.Print(" ", pane.ID())
}
fmt.Println()
}
}
pane, _ := snapshot.PaneByID(right.ID())
window, _ := pane.Window()
owner, _ := window.Session()
windowName, _ := window.Name()
sessionName, _ := owner.Name()
fmt.Println(pane.ID(), "is in window", windowName,
"of session", sessionName)
return nil
}
No tmux server yet

tmux before the first call: no server is running.

No tmux server yet

tmux after tmux.NewServer: no server is running; the call sent no command.

%0 sh
0:sh*
[demo]

tmux after Server.NewSession: added session demo.

%0 sh
0:sh*
[demo]

tmux after Session.ResolveActiveWindow: unchanged.

%0 sh
0:editor*
[demo]

tmux after Window.Rename: renamed window 0:sh to 0:editor.

%0 sh
0:editor*
[demo]

tmux after Window.ResolveActivePane: unchanged.

%0 sh
%1 sh
0:editor*
[demo]

tmux after Pane.Split: added pane %1 to window 0:editor.

%0 sh
%1 sh
0:editor*
%2 sh
1:logs
[demo]

tmux after Session.NewWindow: added window 1:logs.

%0 sh
%1 sh
0:editor*
%2 sh
1:logs
[demo]

tmux after Pane.SendKeys: sent keys to pane %1.

%0 sh
%1 sh
$ printf 'hello from %s\n' tmux
hello from tmux
$
0:editor*
%2 sh
1:logs
[demo]

tmux after Pane.WaitForText: read pane %1.

%0 sh
%1 sh
$ printf 'hello from %s\n' tmux
hello from tmux
$
0:editor*
%2 sh
1:logs
[demo]

tmux after Pane.Select: selected pane %1 in window 0:editor.

%0 sh
%1 sh
$ printf 'hello from %s\n' tmux
hello from tmux
$
0:editor
%2 sh
1:logs*
[demo]

tmux after Window.Select: selected window 1:logs.

%0 sh
%1 sh
$ printf 'hello from %s\n' tmux
hello from tmux
$
0:editor
%2 sh
1:logs*
[demo]

tmux after Server.Snapshot: unchanged.

%0 sh
%1 sh
$ printf 'hello from %s\n' tmux
hello from tmux
$
0:editor
%2 sh
1:logs*
[demo]

tmux after Snapshot.PaneByID: unchanged, the call sent no command.

%0 sh
%1 sh
$ printf 'hello from %s\n' tmux
hello from tmux
$
0:editor
%2 sh
1:logs*
[demo]

tmux after Pane.Window: unchanged, the call sent no command.

%0 sh
%1 sh
$ printf 'hello from %s\n' tmux
hello from tmux
$
0:editor
%2 sh
1:logs*
[demo]

tmux after Window.Session: unchanged, the call sent no command.

No sessions

tmux after Server.Kill: the server has exited.

Before the first call

No tmux server is running yet.

tmux.NewServer returned tmux.Server

{
"configFile": "/dev/null",
"executable": "/usr/bin/tmux",
"socketPath": "/tmp/tmux-1000/libtmux-quickstart-12345"
}

The call sent no tmux command.

Server.NewSession returned tmux.Session

{
"id": "$0",
"name": "demo"
}
3 tmux commands, 1 that changes tmux
  1. list-sessions -F '#{session_id} #{session_name}' error connecting to /tmp/tmux-1000/libtmux-quickstart-12345 (No such file or directory)
  2. new-session -P -F#{session_id} -sdemo -d $0
  3. list-sessions -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|0|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2…

Before these, the library asked tmux about itself 7 times (its version, its daemon, how a control client reads escapes).

Session.ResolveActiveWindow returned tmux.Window

{
"id": "@0",
"index": 0,
"name": "sh"
}
4 tmux commands
  1. list-sessions -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|0|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2…
  2. list-windows -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_coll… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|1|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2…
  3. list-panes -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|1|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|1|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2…
  4. list-clients -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:client_activity}|#{q:client_cell_height}|#{q:client_cell_width}|#{q:client_control_mode}|#{q:client_created}|#{q:client_discarded}|#{q:client_flags}|#{q:client_height}|#{q:client_key_table}|#{q:client_last_session}|#{q:client_mode_format}|#{q:c… no output

Window.Rename returned tmux.Window

{
"id": "@0",
"index": 0,
"name": "editor"
}
2 tmux commands, 1 that changes tmux
  1. rename-window -t @0 -- editor no output
  2. list-windows -t @0 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_c… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|1|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2…

Window.ResolveActivePane returned tmux.Pane

{
"id": "%0"
}
1 tmux command
  1. list-panes -t @0 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_col… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|0|1|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|1|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/14|0|2…

Pane.Split returned tmux.Pane

{
"id": "%1"
}
2 tmux commands, 1 that changes tmux
  1. split-window -t $0:.%0 -h -P -F#{pane_id} -d %1
  2. list-panes -t $0:.%1 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|0|2|0|0|0|0|0|0|\$1|0|1|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|1|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0…

Session.NewWindow returned tmux.Window

{
"id": "@1",
"index": 1,
"name": "logs"
}
5 tmux commands, 1 that changes tmux
  1. new-window -t $0 -d -P -F#{window_id} -n logs @1
  2. list-sessions -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|0|0|0|0|0|0|0|\$1|0|1|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0…
  3. list-windows -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_coll… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|1|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0…
  4. list-panes -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|1|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|1|24|\%0|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0…
  5. list-clients -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:client_activity}|#{q:client_cell_height}|#{q:client_cell_width}|#{q:client_control_mode}|#{q:client_created}|#{q:client_discarded}|#{q:client_flags}|#{q:client_height}|#{q:client_key_table}|#{q:client_last_session}|#{q:client_mode_format}|#{q:c… no output

Pane.SendKeys returned error

null
2 tmux commands, 2 that change tmux
  1. send-keys -t $0:.%1 -- 'printf '\''hello from %s\n'\'' tmux' no output
  2. send-keys -t $0:.%1 -- Enter no output

Pane.WaitForText returned error

null
1 tmux command, 1 that changes tmux
  1. capture-pane -p -J -t %1 ; run-shell -C -t %1 libtmux-text-wait-fcb5574e00cc8b43-#{q:pane_id}-#{q:pane_dead}| $ printf 'hello from %s\n' tmux hello from tmux $

Pane.Select returned error

null
2 tmux commands, 1 that changes tmux
  1. select-pane -t $0:.%1 no output
  2. list-panes -t $0:.%1 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history… 0|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|0|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|-|0|1|24|\%0|0|0|0|VT10x|1|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0|…

Window.Select returned error

null
2 tmux commands, 1 that changes tmux
  1. select-window -t @1 no output
  2. list-windows -t @1 -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_c… 1|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|2|0|24,960,67,335,0,0|1295|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|1|1|0|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%1|0|1|0|VT10x|0|41|0|0|0||4||0|hidden|2848987|0||7,25|79||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/16|0|24|39|39|41|0|1…

Server.Snapshot returned tmux.Snapshot

{
"panes": 3,
"sessions": 1,
"windows": 2
}
4 tmux commands
  1. list-sessions -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 1|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|0|2000|0|host|host|0|0|0|1|0|0|0|0|0|0|0|\$1|0|1|1|1|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%2|0|0|0|VT10x|0|0|0|0|0||1||0|hidden|2849087|0||7,25|79||||/tmp/marked|0|8,16,24,32,40,48,56,64,72|host|0|/dev/pts/17|0|2…
  2. list-windows -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_coll… 1|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|2|0|24,960,67,335,0,0|1295|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|1|1|0|1|1|default|23|0|sh|/tmp/marked|0||||default|\*|0|0|24|\%1|0|1|0|VT10x|0|41|0|0|0||4||0|hidden|2848987|0||7,25|79||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/16|0|24|39|39|41|0|1…
  3. list-panes -a -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:config_files}|#{q:cursor_blinking}|#{q:cursor_character}|#{q:cursor_colour}|#{q:cursor_flag}|#{q:cursor_shape}|#{q:cursor_very_visible}|#{q:cursor_x}|#{q:cursor_y}|#{q:history_added}|#{q:history_all_bytes}|#{q:history_bytes}|#{q:history_collec… 1|0|4294967295|4294967295|0|\#\{t/p:buffer_created\}:\ \#\{buffer_sample\}|/dev/null|0|\ |none|1|default|0|2|0|0|24,960,20,100,0,0|1060|0|1|2000|0|host|host|0|0|0|1|2|0|0|0|0|0|0|\$1|0|0|1|1|0|1|default|23|0|sh|/tmp/marked|0||||default|-|0|1|24|\%0|0|0|0|VT10x|1|0|0|0|0||1||0|hidden|2848733|0||7,25|39||||/tmp/marked|0|8,16,24,32|host|0|/dev/pts/14|0|24|40|40|0|0|1|0|…
  4. list-clients -F#{q:active_window_index}|#{q:alternate_on}|#{q:alternate_saved_x}|#{q:alternate_saved_y}|#{q:bracket_paste_flag}|#{q:buffer_mode_format}|#{q:client_activity}|#{q:client_cell_height}|#{q:client_cell_width}|#{q:client_control_mode}|#{q:client_created}|#{q:client_discarded}|#{q:client_flags}|#{q:client_height}|#{q:client_key_table}|#{q:client_last_session}|#{q:client_mode_format}|#{q:c… no output

Before these, the library asked tmux about itself 2 times (its version, its daemon, how a control client reads escapes).

Snapshot.PaneByID returned tmux.Pane

{
"id": "%1"
}

The call sent no tmux command.

Pane.Window returned tmux.Window

{
"id": "@0",
"index": 0,
"name": "editor"
}

The call sent no tmux command.

Window.Session returned tmux.Session

{
"id": "$0",
"name": "demo"
}

The call sent no tmux command.

Server.Kill returned error

null
1 tmux command, 1 that changes tmux
  1. kill-server no output

Before these, the library asked tmux about itself 1 time (its version, its daemon, how a control client reads escapes).

Run this example

POSIX shell, tmux 3.3a or newer and Go 1.26.5.

Save the program above as quickstart.go in a new directory, along with the files below.

Cleanup is omitted; the private tmux server and its temporary files may remain. Select Cleanup to remove them when the program finishes. The program stops its private tmux server and removes any temporary files it created when it finishes. Error handling adds diagnostics; every version checks required results and deadlines.

go.mod
module example.com/libtmux-home
go 1.26.0
require github.com/libtmux/libtmux-go v0.1.2

Setup and run

go mod tidy
go build -p 2 -o quickstart quickstart.go
unset TMUX TMUX_PANE
export ENV=/dev/null BASH_ENV=/dev/null SHELL=/bin/sh
./quickstart

Expected output

session demo
window 0:editor: %0 %1
window 1:logs: %2
%1 is in window editor of session demo

Library revision: 449267f7cd1d.

examples/control-mode-subscribe/main.go
// Command control-mode-subscribe watches a persistent tmux control client
// receive changes without polling.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"time"
"github.com/libtmux/libtmux-go/tmux"
)
func main() {
if err := start(); err != nil {
log.Fatal(err)
}
}
// start owns cleanup because log.Fatal skips deferred calls in main.
func start() error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Choose an unused socket name: cleanup kills the server selected here.
// The empty config keeps personal settings out of a new server.
server, err := tmux.NewServer(tmux.ServerOptions{
SocketName: "libtmux-control-mode-subscribe",
ConfigFile: os.DevNull,
})
if err != nil {
return fmt.Errorf("configure tmux server: %w", err)
}
return run(ctx, server)
}
// run accepts injected server state so tests can isolate the example.
func run(ctx context.Context, server tmux.Server) (err error) {
session, err := server.NewSession(ctx, tmux.NewSessionRequest{Name: "libtmux-control"})
if err != nil {
return fmt.Errorf("create session: %w", err)
}
defer func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
defer cleanupCancel()
err = errors.Join(err, session.Kill(cleanupCtx))
}()
// docs:watching given:ctx context.Context; session tmux.Session
stream, err := session.OpenNotifications(ctx, tmux.NotificationOptions{})
if err != nil {
return fmt.Errorf("open notification stream: %w", err)
}
defer func() { err = errors.Join(err, stream.Close()) }()
// Rename after subscribing; notifications do not include earlier changes.
if _, err := session.Rename(ctx, "control-example"); err != nil {
return fmt.Errorf("rename session: %w", err)
}
for {
notification, err := stream.Next(ctx)
if err != nil {
return fmt.Errorf("read notification: %w", err)
}
fmt.Printf("notification: %s\n", notification.Kind())
if notification.Kind() == tmux.ControlNotificationSessionRenamed {
fmt.Println("heard the rename")
break
}
}
// docs:end
// docs:subscribing given:ctx context.Context; session tmux.Session; stream *tmux.NotificationStream
// A subscription is a format tmux evaluates for you: it reports the value
// when it first looks, about a second later, and then each time it changes.
if err := stream.Subscribe(ctx, tmux.SubscriptionRequest{
Name: "windows", Format: "#{session_windows}",
}); err != nil {
return fmt.Errorf("subscribe: %w", err)
}
if _, err := session.NewWindow(ctx, tmux.NewWindowRequest{}); err != nil {
return fmt.Errorf("open window: %w", err)
}
for {
notification, err := stream.Next(ctx)
if err != nil {
return fmt.Errorf("read notification: %w", err)
}
if change, ok := notification.Subscription(); ok && change.Value == "2" {
fmt.Println("session has", change.Value, "windows")
return nil
}
}
// docs:end
}

The whole program, examples/control-mode-subscribe/main.go. The port's CI runs it in .github/workflows/tests.yml from examples/: go test -race -count=1.

examples/run-to-completion/main.go
// Command run-to-completion runs a program in a tmux pane and reads its exit
// status and screen back, the way os/exec would if the program had a terminal.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"time"
"github.com/libtmux/libtmux-go/tmux"
)
func main() {
if err := start(); err != nil {
log.Fatal(err)
}
}
// start owns cleanup because log.Fatal skips deferred calls in main.
func start() error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Choose an unused socket name: cleanup kills the server selected here.
// The empty config keeps personal settings out of a new server.
server, err := tmux.NewServer(tmux.ServerOptions{
SocketName: "libtmux-run-to-completion",
ConfigFile: os.DevNull,
})
if err != nil {
return fmt.Errorf("configure tmux server: %w", err)
}
return run(ctx, server)
}
// run accepts injected server state so tests can isolate the example.
func run(ctx context.Context, server tmux.Server) (err error) {
session, err := server.NewSession(ctx, tmux.NewSessionRequest{Name: "libtmux-run"})
if err != nil {
return fmt.Errorf("create session: %w", err)
}
defer func() {
cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
defer cleanupCancel()
err = errors.Join(err, session.Kill(cleanupCtx))
}()
// docs:run-to-completion given:ctx context.Context; session tmux.Session
// The command runs with a terminal, so test -t holds where a pipe fails.
command := "test -t 1 && echo stdout is a terminal; exit 3"
result, err := session.Run(ctx, command, tmux.RunOptions{})
if err != nil {
return fmt.Errorf("run command: %w", err)
}
for _, line := range result.Lines {
fmt.Println("screen:", line)
}
fmt.Println("exited", result.Status)
// docs:end
// docs:run-streaming given:ctx context.Context; session tmux.Session
// Start returns while the command is still running, so its output can be
// followed and it can be stopped from another goroutine. The stream begins
// where StreamTo opens it, so this command waits before its first line;
// result.Lines holds the screen either way.
running, err := session.Start(ctx, "sleep 1; seq 1 3; sleep 30", tmux.RunOptions{})
if err != nil {
return fmt.Errorf("start command: %w", err)
}
stopped := make(chan error, 1)
go func() {
time.Sleep(2 * time.Second)
stopped <- running.Kill(ctx)
}()
streamed, err := running.StreamTo(ctx, os.Stdout)
if err != nil {
return fmt.Errorf("stream command: %w", err)
}
if err := <-stopped; err != nil {
return fmt.Errorf("stop command: %w", err)
}
fmt.Println("stopped:", streamed.Signal)
// docs:end
return nil
}

The whole program, examples/run-to-completion/main.go. The port's CI runs it in .github/workflows/tests.yml from examples/: go test -race -count=1.

Install github.com/libtmux/libtmux-go/tmux

$ go get github.com/libtmux/libtmux-go/tmux@latest

MCP

Worked example

No sessions

tmux after kill_session.

you

Create a tmux session called checks, split its window into two panes, run pytest -q in one and show checks.log in the other, show me the results, then remove the session.

agentCreate the session.

create_session Execute {"session_name":"checks"}
Result
{
"sessionId": "$0",
"sessionName": "checks"
}

agentList the panes, each with the session it belongs to.

list_panes Inspect {}
Result
{
"panes": [
{
"active": true,
"currentCommand": "sh",
"geometry": {
"height": 24,
"left": 0,
"top": 0,
"width": 80
},
"id": "%0",
"index": 0,
"isCaller": null,
"session": "checks",
"window": "sh",
"windowId": "@0"
}
],
"total": 1
}

agentSplit the pane the list returned (%0) in two.

split_window Execute {"pane_id":"%0","direction":"right"}
Result
{
"paneId": "%1"
}

agentList the panes again; each now carries its position and size.

list_panes Inspect {}
Result
{
"panes": [
{
"active": true,
"currentCommand": "sh",
"geometry": {
"height": 24,
"left": 0,
"top": 0,
"width": 40
},
"id": "%0",
"index": 0,
"isCaller": null,
"session": "checks",
"window": "sh",
"windowId": "@0"
},
{
"active": false,
"currentCommand": "sh",
"geometry": {
"height": 24,
"left": 41,
"top": 0,
"width": 39
},
"id": "%1",
"index": 1,
"isCaller": null,
"session": "checks",
"window": "sh",
"windowId": "@0"
}
],
"total": 2
}

agentRun the tests in the pane the list returned (%0).

run_shell_command Execute {"pane_id":"%0","command":"pytest -q"}
Result
{
"effective_timeout_seconds": 120,
"exit_status": 0,
"output": [
".. [100%]",
"2 passed in 0.01s"
],
"pane_id": "%0",
"resolved_pane_ids": [
"%0"
],
"timed_out": false
}

agentShow the log in the new pane the split returned (%1).

run_shell_command Execute {"pane_id":"%1","command":"cat checks.log"}
Result
{
"effective_timeout_seconds": 120,
"exit_status": 0,
"output": [
"2026-01-01 checks started",
"2026-01-01 checks finished"
],
"pane_id": "%1",
"resolved_pane_ids": [
"%1"
],
"timed_out": false
}

agentRemove the session by the id the create result returned ($0).

kill_session Teardown {"session_id":"$0"}
Result
{
"killed": "checks"
}

agent in %0 it printed 2 passed in 0.01s; in %1 it printed 2026-01-01 checks finished.

Calls and results are recorded against libtmux/libtmux-go at f59af95c2, on a private tmux 3.8a server, running pytest 9.2.0. The preview takes its layout and panes from the recording's own tmux server after each call, and a pane's output from the results of the calls. The agent's narration is illustrative. This page documents d8ea6f77e, a different revision. The calls have the same input and output schemas there, but the results at that revision may differ from the recorded ones. The sandbox's project directory is shown as /home/user/project.

Install github.com/libtmux/libtmux-go/mcp

Choose your client and configuration scope, then run the command or paste the configuration.

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

$ claude mcp add tmux -- libtmux-mcp

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: ~/.claude.json (this project)

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

$ claude mcp add tmux --scope user -- libtmux-mcp

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: ~/.claude.json (all projects)

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

$ claude mcp add tmux --scope project -- libtmux-mcp

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: .mcp.json (in repo, version-controlled)

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then use this config:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp"
        }
    }
}

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: claude_desktop_config.json

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

$ codex mcp add tmux -- libtmux-mcp

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: ~/.codex/config.toml

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.

[mcp_servers.tmux]
command = "libtmux-mcp"

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: .codex/config.toml (in repo)

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

$ gemini mcp add --scope user tmux libtmux-mcp

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: ~/.gemini/settings.json

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

$ gemini mcp add --scope project tmux libtmux-mcp

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: .gemini/settings.json (in repo)

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then use this config:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp"
        }
    }
}

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: .cursor/mcp.json (in repo)

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then use this config:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp"
        }
    }
}

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: ~/.cursor/mcp.json

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

$ grok mcp add --scope user tmux -- libtmux-mcp

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: ~/.grok/config.toml

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

$ grok mcp add --scope project tmux -- libtmux-mcp

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: ./.grok/config.toml (in repo)

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then use this config:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp"
        }
    }
}

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: ~/.gemini/config/mcp_config.json

Build or install the server first:

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest

Then register:

$ opencode mcp add tmux -- libtmux-mcp

That puts `libtmux-mcp` in $(go env GOPATH)/bin, which has to be on your PATH for the command below to resolve.

Config file: ~/.config/opencode/opencode.jsonc

Embed the server in a program
mcp/examples/agent-workflow/main.go
func connect(ctx context.Context, target tmux.Server) (*sdk.ClientSession, func(), error) {
clientTransport, serverTransport := sdk.NewInMemoryTransports()
instance, err := tmuxmcp.NewServer(target)
if err != nil {
return nil, nil, fmt.Errorf("construct the server: %w", err)
}
serverSession, err := instance.Connect(
ctx, tmuxmcp.AssumeResponseCommit(serverTransport), nil,
)
if err != nil {
_ = instance.Close()
return nil, nil, fmt.Errorf("start the server: %w", err)
}
client := sdk.NewClient(&sdk.Implementation{Name: "agent-workflow", Version: "1"}, nil)
session, err := client.Connect(ctx, clientTransport, nil)
if err != nil {
_ = serverSession.Close()
_ = instance.Close()
return nil, nil, fmt.Errorf("connect a client: %w", err)
}
return session, func() {
_ = session.Close()
_ = serverSession.Close()
_ = instance.Close()
}, nil
}

Excerpt of mcp/examples/agent-workflow/main.go, which go test -race -count=1 runs in the repository's CI (.github/workflows/tests.yml).

Workspace Manager

Environment

environment sets variables for a session, a window or a pane.

session_name: native
start_directory: .
environment: {SESSION_VALUE: session}
windows:
- window_name: development
window_index: 3
layout: even-horizontal
environment: {WINDOW_VALUE: window}
panes:
- start_directory: one
shell_command: 'printf "%s:%s:%s" "$SESSION_VALUE" "$WINDOW_VALUE" "$PANE_VALUE" > result'
- start_directory: two
environment: {PANE_VALUE: pane}
shell_command: 'printf "%s:%s:%s" "$SESSION_VALUE" "$WINDOW_VALUE" "$PANE_VALUE" > result'
- window_name: second
focus: true
panes: [blank]
{
"session_name": "native",
"start_directory": ".",
"environment": {
"SESSION_VALUE": "session"
},
"windows": [
{
"window_name": "development",
"window_index": 3,
"layout": "even-horizontal",
"environment": {
"WINDOW_VALUE": "window"
},
"panes": [
{
"start_directory": "one",
"shell_command": "printf \"%s:%s:%s\" \"$SESSION_VALUE\" \"$WINDOW_VALUE\" \"$PANE_VALUE\" > result"
},
{
"start_directory": "two",
"environment": {
"PANE_VALUE": "pane"
},
"shell_command": "printf \"%s:%s:%s\" \"$SESSION_VALUE\" \"$WINDOW_VALUE\" \"$PANE_VALUE\" > result"
}
]
},
{
"window_name": "second",
"focus": true,
"panes": [
"blank"
]
}
]
}

Converted from the YAML; the tests load the YAML.

0:second
Window 0, second: 1 pane, tiled
no layout set, so the port tiles the panes
3:development
Window 3, development: 2 panes, even-horizontal❯ printf "%s:%s:%s" "$SESSION_VALUE" "$WINDOW_VALUE" "$PANE_VALUE" > result❯ printf "%s:%s:%s" "$SESSION_VALUE" "$WINDOW_VALUE" "$PANE_VALUE" > result
layout even-horizontal

Save the file as workspace.yaml and load it with:

tmux-workspace load workspace.yaml

Workspace file: workspace/internal/integration/cli_test.go. Tested by TestLoadFreezeReloadAppendAndEnvironment (workspace/internal/integration/cli_test.go; a test runs the command).

Install github.com/libtmux/libtmux-go/workspace

The workspace package is separate from the core library.

$ go install github.com/libtmux/libtmux-go/workspace/cmd/tmux-workspace@latest

Needs Go 1.26 or newer. Puts tmux-workspace in $(go env GOBIN), or in $(go env GOPATH)/bin when GOBIN is unset; rerunning it upgrades.

Build the workspace from a program
workspace/examples/build-workspace/main.go
// document is a workspace file. A program of your own would read one from
// disk with os.ReadFile.
const document = `
session_name: project
windows:
- window_name: editor
layout: main-vertical
panes:
- echo editing
- echo testing
- window_name: logs
panes:
- echo api
- echo worker
- echo database
`
// Parse refuses a field it does not know rather than dropping it, so a
// workspace that loads is one that was understood.
parsed, err := workspace.Parse([]byte(document))
if err != nil {
return fmt.Errorf("parse workspace: %w", err)
}
// Choose an unused socket name: cleanup kills the server selected here.
// The empty config keeps personal settings out of a new server.
server, err := tmux.NewServer(tmux.ServerOptions{
SocketName: "libtmux-build-workspace",
ConfigFile: os.DevNull,
})
if err != nil {
return fmt.Errorf("configure tmux: %w", err)
}
// Cleanup runs even when ctx is the reason the program is leaving.
defer func() {
err = errors.Join(err, server.Kill(context.WithoutCancel(ctx)))
}()
session, err := workspace.Build(ctx, server, parsed)

Excerpt of workspace/examples/build-workspace/main.go, which go test -race -count=1 runs in the repository's CI (.github/workflows/tests.yml).

Packages and integrations

API references

All Go versions and install instructions

Esc

Type to search.

Reference previewOpen reference