tmux

Capture and inspect content from tmux sessions, windows, and panes — locally or over SSH. Useful for sharing terminal output with LLMs, debugging across multiple terminals, or monitoring long-running processes.

SSH

All capture and list functions accept SSH kwargs to target a remote machine: use host for an SSH alias, or ip + user + optional keyfile.

pane(host='hack')
windows(ip='1.2.3.4', user='ubuntu', keyfile='~/.ssh/id_rsa')

source

shell_ret

def shell_ret(
    cmd:str, capture_output:bool=True, text:bool=True, ret:bool=True, *, host:str=None, # Optional SSH Host
    ip:str=None, # Optional SSH IP
    user:str=None, # Optional SSH user
    keyfile:str=None, # Optional SSH keyfile
):

Run shell command locally or over ssh (use host for alias, or ip/user/keyfile)

print(shell_ret('du -sh'))
416K    .
print(shell_ret('du -sh', host='hack'))
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[17], line 1
----> 1 print(shell_ret('du -sh', host='hack'))

Cell In[13], line 8, in shell_ret(cmd, capture_output, text, ret, **kwargs)
      4     "Run shell command locally or over ssh (use host for alias, or ip/user/keyfile)"
      5     host, ip, user, keyfile = kwargs.pop('host', None), kwargs.pop('ip', None), kwargs.pop('user', None), kwargs.pop('keyfile', None)
      6     if host: cmd = f"echo '{cmd}' | ssh -A {host} 'bash -ls'"
      7     elif ip: cmd = f"echo '{cmd}' | ssh {f'-i {keyfile} ' if keyfile else ''}-A {user}@{ip} 'bash -ls'"
----> 8     o = subprocess.run(cmd, shell=True, text=text, capture_output=capture_output, **kwargs)
      9     return (o.stdout or o.stderr) if ret else o

File ~/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/subprocess.py:556, in run(input, capture_output, timeout, check, *popenargs, **kwargs)
    554 with Popen(*popenargs, **kwargs) as process:
    555     try:
--> 556         stdout, stderr = process.communicate(input, timeout=timeout)
    557     except TimeoutExpired as exc:
    558         process.kill()

File ~/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/subprocess.py:1222, in Popen.communicate(self, input, timeout)
   1219     endtime = None
   1221 try:
-> 1222     stdout, stderr = self._communicate(input, endtime, timeout)
   1223 except KeyboardInterrupt:
   1224     # https://bugs.python.org/issue25942
   1225     # See the detailed comment in .wait().
   1226     if timeout is not None:

File ~/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/subprocess.py:2154, in Popen._communicate(self, input, endtime, orig_timeout)
   2147     self._check_timeout(endtime, orig_timeout,
   2148                         stdout, stderr,
   2149                         skip_check_and_raise=True)
   2150     raise RuntimeError(  # Impossible :)
   2151         '_check_timeout(..., skip_check_and_raise=True) '
   2152         'failed to raise TimeoutExpired.')
-> 2154 ready = selector.select(timeout)
   2155 self._check_timeout(endtime, orig_timeout, stdout, stderr)
   2157 # XXX Rewrite these to use non-blocking I/O on the file
   2158 # objects; they are no longer using C stdio!

File ~/.local/share/uv/python/cpython-3.13.15-macos-aarch64-none/lib/python3.13/selectors.py:398, in _PollLikeSelector.select(self, timeout)
    396 ready = []
    397 try:
--> 398     fd_event_list = self._selector.poll(timeout)
    399 except InterruptedError:
    400     return ready

File ~/aai-ws/kernmini/kernmini/kernel.py:821, in MiniKernel.handle_sigint(self, signum, frame)
    819 if parent.cancel_async_execution(wake=True): return
    820 if not parent.sync_executing.is_set(): return
--> 821 raise KeyboardInterrupt

KeyboardInterrupt: 

source

set_default_history

def set_default_history(
    n:int
):

Set the default number of lines to capture from tmux history

pane


source

pane

def pane(
    n:int=None, # Number of scrollback lines to capture, in addition to visible area (None uses default_tmux_lines, which is 500 if not otherwise set)
    pane:int | str=None, # Pane number to capture from (accepts both integers for window IDs and "%{int}" for global pane IDs)
    session:str=None, # Session name to target
    window:int=None, # Window number to target
    *, capture_output:bool=True, text:bool=True, ret:bool=True, host:str=None, # Optional SSH Host
    ip:str=None, # Optional SSH IP
    user:str=None, # Optional SSH user
    keyfile:str=None, # Optional SSH keyfile
):

Grab the tmux history in plain text

# print(pane(1))
# print(pane(1, host='hack'))

list_panes


source

list_panes

def list_panes(
    session:str=None, # Session name to list panes from
    window:int=None, # Window number to list panes from
    *, capture_output:bool=True, text:bool=True, ret:bool=True, host:str=None, # Optional SSH Host
    ip:str=None, # Optional SSH IP
    user:str=None, # Optional SSH user
    keyfile:str=None, # Optional SSH keyfile
):

List panes for a session/window (or current if none specified)

print(list_panes(window=0))
print(list_panes(window=0, host='hack'))

panes


source

panes

def panes(
    session:str=None, # Session name to target
    window:int=None, # Window number to target
    n:int=None, # Number of scrollback lines to capture
    *, capture_output:bool=True, text:bool=True, ret:bool=True, host:str=None, # Optional SSH Host
    ip:str=None, # Optional SSH IP
    user:str=None, # Optional SSH user
    keyfile:str=None, # Optional SSH keyfile
):

Grab history from all panes in a session/window

from pprint import pprint
# pprint(panes(window=0, n=10))
# pprint(panes(n=10, host='hack'))

list_windows


source

list_windows

def list_windows(
    session:str=None, # Session name to list windows from
    *, capture_output:bool=True, text:bool=True, ret:bool=True, host:str=None, # Optional SSH Host
    ip:str=None, # Optional SSH IP
    user:str=None, # Optional SSH user
    keyfile:str=None, # Optional SSH keyfile
):

List all windows in a session

print(list_windows())
print(list_windows(host='hack'))

windows


source

windows

def windows(
    session:str=None, # Session name to target
    n:int=None, # Number of scrollback lines to capture
    *, capture_output:bool=True, text:bool=True, ret:bool=True, host:str=None, # Optional SSH Host
    ip:str=None, # Optional SSH IP
    user:str=None, # Optional SSH user
    keyfile:str=None, # Optional SSH keyfile
):

Grab history from all panes in all windows of a session as {'win_num:name': {pane_num: content}}

# pprint(windows())
# pprint(windows(host='hack'))

list_sessions


source

list_sessions

def list_sessions(
    *, capture_output:bool=True, text:bool=True, ret:bool=True, host:str=None, # Optional SSH Host
    ip:str=None, # Optional SSH IP
    user:str=None, # Optional SSH user
    keyfile:str=None, # Optional SSH keyfile
):

List all tmux sessions

print(list_sessions())
print(list_sessions(host='hack'))

sessions


source

sessions

def sessions(
    n:int=None, # Number of scrollback lines to capture
    *, capture_output:bool=True, text:bool=True, ret:bool=True, host:str=None, # Optional SSH Host
    ip:str=None, # Optional SSH IP
    user:str=None, # Optional SSH user
    keyfile:str=None, # Optional SSH keyfile
):

Grab history from all panes in all windows of all sessions as {session: {'win_num:name': {pane_num: content}}}

# pprint(sessions())
# pprint(sessions(host='hack'))

Searching nested results

windows() and sessions() return nested dicts. Use flatten_dict to search them as (path, content) tuples, where path identifies the session/window/pane source. The path string uses // separators showing the hierarchy, e.g. "mysession//0:bash//1" means session "mysession", window 0 named "bash", pane 1.

For example, find error lines across all tmux history:

errors = [(path, line)
          for path, content in flatten_dict(sessions(n=2000))
          for line in content.splitlines()
          if 'error' in line.lower()]

Or find panes containing a command you remember:

found = {path: content for path, content in flatten_dict(windows(n=2000)) if 'curl' in content}

source

flatten_dict

def flatten_dict(
    d, parent_key:str='', sep:str='//'
):

Flatten nested dict into list of (key_path, value) tuples

nested = {
    'session1': {
        '0:bash': {0: 'some content', 1: 'more content'},
        '1:vim': {0: 'editing file'}
    }
}
for path, content in flatten_dict(nested): print(f"{path}: {content}")