Summary
Adopting libtmux 0.56.0's new wrappers across tmux-python/libtmux-mcp (15 raw cmd() call sites → typed wrappers) surfaced 9 spots where the wrapper either doesn't exist or doesn't quite fit a tmux invocation that the underlying command supports. A follow-up sweep against tmux 3.6a's full cmd_entry flag schemas and format_table[] registry surfaced 4 more gap categories (gaps #10–#13) plus an 8-token forward-looking set from tmux master post-3.6a (gap #14). tmux source citations below reference local copies at ~/study/c/tmux-3.6a/ (matching the upstream tmux/tmux@3.6a tag — the latest tmux release as of 2026-05-12) and link to the same files on GitHub at tmux/tmux/tree/3.6a.
Each gap is independently fixable; they cluster into five shapes (see end of issue).
1. Server.display_message doesn't exist
Site: Only Pane.display_message is wrapped (pane.py:625-774). Server has show_messages (message log) but no format-string evaluator.
Why tmux supports it: cmd-display-message.c:38-50 (local: ~/study/c/tmux-3.6a/cmd-display-message.c:38-50) — the entry's target is CMD_FIND_PANE with CMD_FIND_CANFAIL, so omitting -t falls back to the current/default pane and the format engine still resolves server-scoped variables like #{version} and #{socket_path}.
Where it bites callers: libtmux-mcp's get_server_info and _effective_socket_path need #{version} / #{socket_path} reads with no specific pane handle; they have to drop down to server.cmd("display-message", "-p", "#{…}"). Three sites in the MCP carry this workaround.
Proposed: add Server.display_message(format_string, *, get_text=True, target_client=…) returning list[str]. Same shape as Pane.display_message but no -t injection.
2. Window.display_message doesn't exist
Site: No wrapper at the Window level either.
Why tmux supports it: Same CMD_FIND_PANE | CMD_FIND_CANFAIL target as gap #1 (cmd-display-message.c:46); callers commonly pass -t @<window> for window-scoped reads like #{window_zoomed_flag}.
Where it bites callers: libtmux-mcp's resize_pane(zoom=…) needs the zoom state before deciding to toggle; with no Window.display_message, it must call window.cmd("display-message", "-p", "#{window_zoomed_flag}") directly.
Proposed: add Window.display_message (same signature as Pane.display_message), or generalize the Pane method to accept arbitrary targets.
3. Window.window_zoomed_flag is not a typed attribute on the dataclass
Site: window.py:53 — the Window dataclass declares many window_* fields but not window_zoomed_flag. mypy rejects window.window_zoomed_flag even after window.refresh().
Why tmux supports it: the variable is a first-class format token with a dedicated callback at format.c:2854-2864 (local: ~/study/c/tmux-3.6a/format.c:2854), formally registered in the format table at format.c:3557.
Where it bites callers: the umbrella issue libtmux-mcp#46 suggested swapping a display-message call for the ORM read — turned out it doesn't type-check, so the MCP reverted to cmd() with a comment.
Proposed: add window_zoomed_flag: str | None (and other common window_* format vars) as declared fields populated via refresh(). See gap #10 for the full undeclared token set across all object scopes.
4. Pane.reset() is still broken in 0.56.0 (open as #650)
Site: pane.py:2150-2153:
def reset(self) -> Pane:
self.cmd("send-keys", r"-R \; clear-history")
return self
The \; is tmux's interactive command separator. subprocess.run argv doesn't expand it — tmux sees a single argv "-R \; clear-history" and interprets the trailing \; clear-history as literal keys to send, never running clear-history. Pre-existing tracking issue: #650.
Why tmux supports both individually: send-keys -R is handled at cmd-send-keys.c:217-220 (local: ~/study/c/tmux-3.6a/cmd-send-keys.c:217); clear-history is its own entry in cmd-capture-pane.c.
Where it bites callers: libtmux-mcp's clear_pane (see tools/pane_tools/io.py) keeps a two-call workaround. Comment in the MCP cites this issue.
Proposed: split into two cmd() calls inside Pane.reset (the same workaround the MCP carries), or use tmux's \; separator via shell=True.
5. Pane.send_keys has no flag-only invocation
Site: pane.py:499-622. The wrapper always appends prefix + cmd (or copy_mode_cmd) as a trailing argv:
else:
self.cmd("send-keys", *tmux_args, prefix + cmd)
There's no path that emits just tmux send-keys -R (reset flag, no key argument).
Why tmux supports it: cmd-send-keys.c:223-227 (local: ~/study/c/tmux-3.6a/cmd-send-keys.c:223) — when count == 0 and either -N or -R is set, the command returns CMD_RETURN_NORMAL without sending any keys. tmux's authors explicitly handle the flag-only case.
Where it bites callers: libtmux-mcp's clear_pane needs tmux send-keys -R (no keys). pane.send_keys("", reset=True, enter=False) produces tmux send-keys -R "" — likely a no-op in practice, but the trailing empty arg isn't what the underlying tmux command expects for a flag-only invocation. The MCP keeps pane.cmd("send-keys", "-R") for that reason.
Proposed: make the cmd parameter Optional[str] with default None; when None and a flag like reset or repeat is set, emit tmux send-keys <flags> with no trailing arg. Backward-compatible because cmd is currently positional-required.
6. Server.list_buffers() returns default-formatted lines, not raw names
Site: server.py:1643-1663 — calls tmux list-buffers with no -F, so the output is tmux's default template (buf_name: N bytes: …). No format= kwarg on the wrapper.
Why tmux supports it: cmd-list-buffers.c:35-44 (local: ~/study/c/tmux-3.6a/cmd-list-buffers.c:35) — entry usage is [-F format] [-f filter]; the -F template is read at line 56 with a builtin fallback, the -f filter at line 58.
Where it bites callers: libtmux-mcp's buffer GC needs just the names matching libtmux_mcp_* prefix. The wrapper returns lines like libtmux_mcp_abc: 5 bytes: "…"; parsing requires regex. The MCP keeps server.cmd("list-buffers", "-F", "#{buffer_name}") instead.
Proposed: add format: str | None = None and filter: str | None = None kwargs to Server.list_buffers. When format is set, pass -F <fmt>; otherwise keep the default tmux output for backward compat.
7. server.panes / QueryList doesn't expose tmux's -f C-filter
Site: [Server.panes at server.py:2060-2076](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/server.py#L2060) returns a QueryList[Pane] that filters in Python. Callers who want the **C-level filter** with format-expression predicates have no wrapper path; [fetch_objsin neo.py](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/neo.py#L248) always emits libtmux's own-F{format_string}` template.
Why tmux supports it: cmd-list-panes.c:37-42 (local: ~/study/c/tmux-3.6a/cmd-list-panes.c:37) — entry usage is [-as] [-F format] [-f filter]; the filter is read at line 125 and evaluated by format_true(expanded) at line 134, gating which panes appear in the output.
Where it bites callers: libtmux-mcp's search_panes fast path uses tmux list-panes -a -f '#{C/i:pattern}' -F '#{pane_id}' — pushes a regex match into tmux's C code, returning only matching pane IDs without capturing every pane in Python. With QueryList, the only option is "capture all panes in Python and filter post-hoc" — orders of magnitude slower for the common case. The MCP keeps server.cmd("list-panes", ...) for this site.
Proposed: add a filter: str | None = None kwarg to Server.panes (and the analogous Session.panes, Window.panes) that round-trips a tmux format expression through -f. Alternatively, a Server.list_panes(filter=…, format=…) low-level method.
8. LibTmuxException translation loses subcommand context
Site: wrappers like Session.last_window raise exc.LibTmuxException(proc.stderr) (session.py:298-318). The exception (exc.py:18-19 — a bare Exception subclass with no payload fields) carries the raw stderr list but not the subcommand name that produced it.
Why this matters: downstream wrappers (libtmux-mcp's handle_tool_errors translates LibTmuxException → ToolError(f"tmux error: {e}")) and any agent-facing error string lose the "which tmux command failed" context. Pre-0.56.0, the MCP built f"tmux {subcommand} failed: {stderr}" manually; the wrapper version surfaces only "tmux error: ['no last window']". One MCP test had to be updated to match the new shape.
Proposed: extend LibTmuxException (and friends) with an optional subcommand: str | None attribute. Format __str__ as f"{subcommand}: {stderr_text}" when present. Wrappers populate it implicitly (e.g., a _run_cmd("last-window", *args) helper that catches and re-raises with the subcommand).
9. Pane.cmd always injects -t <pane_id>, no warning on duplicate
Site: Pane.cmd at pane.py:178-212](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/pane.py#L178) always sets target = self.pane_idbefore delegating toServer.cmd. [Server.cmd at server.py:326 then emits -t <target> at the front of cmd_args regardless of what the caller passes in *args. If the caller also adds -t %X, tmux sees tmux <subcmd> -t %1 -t %1 and silently accepts the duplicate.
Why tmux tolerates it: tmux's args.c args_get() returns the last value of a repeated flag — so -t %1 -t %1 is functionally -t %1. Safe but masks caller bugs.
Where it bites callers: libtmux-mcp had two sites doing this (buffer_tools.py:275 and copy_mode.py:60-68) before the 0.56.0 wrappers were adopted; both were quietly accepted by tmux despite being incorrect Python. Discovered only because the new wrappers (which inject -t correctly) made the duplicate impossible to keep.
Proposed: when Pane.cmd (and Session.cmd, Window.cmd, Server.cmd) detect a -t already in *args, either (a) raise a clear TypeError, or (b) emit warnings.warn("Pane.cmd auto-injects -t; redundant -t in args") once per call site. Option (a) is safer; option (b) is gentler if the existing wild ecosystem relies on the no-op.
10. ~37 first-class format tokens aren't typed on Pane/Window/Session dataclasses
Site: neo.py:139-159 declares a hand-curated allowlist of format tokens as str | None = None fields on Obj. Sweeping tmux's format_table[] array at format.c:3010-3563 against neo.py's declared fields shows 37 scope-relevant tokens that ship in tmux 3.6a but are not declared. Gap #3 is one instance; the full set:
Pane (13) — registered in format.c between line 3254 and 3341:
Window (13) — registered in format.c between line 3464 and 3557:
| Token |
format.c line |
window_active_clients_list |
3464 |
window_active_sessions_list |
3470 |
window_activity_flag |
3476 |
window_bell_flag |
3479 |
window_bigger |
3482 |
window_end_flag |
3491 |
window_flags |
3494 |
window_format |
3497 |
window_last_flag |
3509 |
window_silence_flag |
3542 |
window_start_flag |
3548 |
window_visible_layout |
3551 |
window_zoomed_flag |
3557 — already in gap #3 |
Session (11) — registered in format.c between line 3359 and 3428:
| Token |
format.c line |
session_active |
3359 |
session_activity_flag |
3365 |
session_alert |
3368 |
session_bell_flag |
3380 |
session_format |
3386 |
session_group_attached_list |
3395 |
session_group_many_attached |
3401 |
session_grouped |
3407 |
session_many_attached |
3416 |
session_marked |
3419 |
session_silence_flag |
3428 |
Why tmux supports them: every entry above appears verbatim in the format_table[] array — they're registered first-class format tokens with FORMAT_TABLE_STRING callbacks. tmux's format_create_defaults path populates them whenever the relevant scope is bound (pane→format_defaults_pane, window→format_defaults_winlink, session→format_defaults_session).
Where it bites callers: every undeclared token forces typed-Python callers back to pane.cmd("display-message", "-p", "#{…}"). libtmux-mcp's resize_pane(zoom=…) (window_zoomed_flag), copy-mode detection (pane_in_mode, pane_mode), and pane-death checks (pane_dead) all carry these workarounds.
Proposed: declare the full scope-relevant set on Obj (or split into per-scope mixins). Better: generate the field declarations from format.c parsing at build time so future tmux releases don't drift the typed surface — that closes gap #14 (HEAD tokens) for free.
11. No Client class — client_* format tokens have nowhere to land
Site: Server.list_clients at server.py:1392 returns raw output lines. No Client dataclass exists in src/libtmux/. Compare with Session/Window/Pane, which each have an Obj-backed dataclass and a QueryList-returning collection accessor.
Why tmux supports it: twelve client_* format tokens are registered in format.c:
| Token |
format.c line |
client_activity |
3041 |
client_control_mode |
3050 |
client_created |
3053 |
client_last_session |
3068 |
client_mode_format |
3071 |
client_prefix |
3080 |
client_readonly |
3083 |
client_session |
3086 |
client_termfeatures |
3089 |
client_termtype |
3095 |
client_theme |
3098 |
client_utf8 |
3110 |
The matching tmux subcommand is cmd-list-clients.c, which accepts [-F format] [-t target-session] for arbitrary projection.
Where it bites callers: anything inspecting attached clients (multi-client coordination, "is this client read-only?", per-client status drawing) must call server.cmd("list-clients", "-F", "#{…}") and hand-parse output. The typed-ORM ergonomics that exist for sessions/windows/panes simply aren't available for clients.
Proposed: add src/libtmux/client.py with a Client(Obj) dataclass declaring all twelve client_* fields, plus Server.clients -> QueryList[Client] mirroring Server.sessions. Reuse fetch_objs with list_cmd="list-clients".
12. Server.run_shell drops -c (cwd) and -E (show_stderr) flags
Site: server.py:427-486 exposes background (-b), delay (-d), as_tmux_command (-C), and target_pane (-t). The full flag schema in tmux is "bd:Ct:Es:c:".
Why tmux supports them: cmd-run-shell.c:47 declares the args; cmd-run-shell.c:156-159 reads -c to override the working directory (cdata->cwd = xstrdup(args_get(args, 'c'))), and cmd-run-shell.c:161-162 reads -E to enable JOB_SHOWSTDERR — the latter is the difference between "stdout only" and "stdout + stderr merged into the captured output."
Where it bites callers: any caller that needs the shell command to run in a specific directory (independent of any target pane's cwd) or wants to surface stderr in the result must drop to server.cmd("run-shell", …). libtmux-mcp's diagnostic shell-outs (e.g., probing helper binaries) fall into this bucket.
Proposed: add cwd: StrPath | None = None and show_stderr: bool | None = None kwargs, emitting -c <cwd> and -E respectively.
13. Pane.capture_pane drops -P (pending input) flag
Site: pane.py:354-498 exposes 12 of the 13 flags from tmux's "ab:CeE:JMNpPqS:Tt:" schema. -P is the missing one.
Why tmux supports it: cmd-capture-pane.c:42 declares the args; cmd-capture-pane.c:231-232 branches: when -P is set, cmd_capture_pane_pending(args, wp, &len) returns bytes tmux has buffered as input for the pane but the program hasn't consumed yet — distinct from -p (lowercase, "print to stdout") and from the default history capture.
Where it bites callers: diagnostic tooling that needs to inspect "what input is queued but unprocessed?" (debugging hung programs, copy-mode race conditions, paste-buffer drains) has no typed access; callers must do pane.cmd("capture-pane", "-P", "-p").
Proposed: add pending: bool = False kwarg. When set, emit -P (and keep -p for stdout return). Document that this returns pending input bytes, not history.
14. Forward-looking: 8 new format tokens in tmux master post-3.6a
Site: None of these are declared on libtmux dataclasses (same shape as gap #10), and they don't yet exist in tmux 3.6a — they were added between the 3.6a tag and tmux master. Tracking them now means libtmux is ready when the next tmux tag lands.
Why tmux is adding them: present in tmux master (tmux/tmux/blob/master/format.c), absent from the 3.6a tag. The full diff:
| Token |
Scope |
Purpose |
pane_zoomed_flag |
pane |
Pane-level zoom indicator (companion to window_zoomed_flag) |
pane_floating_flag |
pane |
Pane is a floating/popup pane |
pane_flags |
pane |
Combined pane flag string (analogous to window_flags) |
pane_pb_state |
pane |
Paste-buffer transfer state |
pane_pb_progress |
pane |
Paste-buffer transfer progress |
pane_pipe_pid |
pane |
PID of the pipe-pane child process |
synchronized_output_flag |
terminal |
DEC mode 2026 synchronized-output state |
bracket_paste_flag |
terminal |
Bracketed-paste mode active |
Where it bites callers: none yet — these only become reachable when callers upgrade to the next tmux release. But the Obj allowlist will need to be re-walked when that happens. Treating gap #10's fix as "auto-generated from tmux's format.c" makes gap #14 free.
Proposed: roll into the same generator described under gap #10 — when tmux ships these in a tagged release, regenerate; no manual list to maintain.
Suggested groupings for fix PRs
These cluster into five shapes — each cluster is a coherent PR:
Cluster A — display_message parity (gaps #1, #2)
Add Server.display_message and Window.display_message with the same signature as Pane.display_message. Single-commit PR; mirrors the symmetry the rest of the 0.56.0 release set up.
Cluster B — typed format-token coverage (gaps #3, #10, #14)
Expand the declared field set on Pane/Window/Session to cover all scope-relevant tokens from format.c's format_table[]. Prefer a build-time generator that reads format.c so the typed surface tracks tmux automatically; that closes gap #14 (HEAD tokens) for free when the next tmux release tag lands.
Cluster C — flag-only / format-aware wrappers (gaps #5, #6, #7, #12, #13)
Make cmd optional on Pane.send_keys to enable flag-only invocations; add format= / filter= kwargs to Server.list_buffers and Server.panes (et al.) to expose tmux's C-level filtering; add cwd= / show_stderr= to Server.run_shell; add pending= to Pane.capture_pane. These all expose existing tmux capability that the wrapper currently hides.
Cluster D — new Client object hierarchy (gap #11)
Add Client(Obj) dataclass with the twelve client_* fields and Server.clients -> QueryList[Client]. Mirrors the Session/Window/Pane shape and brings list-clients into the typed API.
Cluster E — diagnostics and safety (gaps #4, #8, #9)
Fix Pane.reset per #650; thread subcommand context through LibTmuxException; add duplicate--t detection on the cmd methods. Quality-of-life cluster; each item is small but compounds across the wider ecosystem.
Evidence / Motivation
The MCP-side adoption PR is at tmux-python/libtmux-mcp#48. Gaps #1–#9 are documented inline in commit messages or code comments there — pointers in the PR description and the umbrella issue libtmux-mcp#46. Gaps #10–#14 surfaced from a follow-up sweep of tmux/tmux@3.6a's cmd_entry flag strings and format_table[] cross-referenced against src/libtmux/neo.py field declarations and src/libtmux/*.py wrapper signatures.
Summary
Adopting libtmux 0.56.0's new wrappers across
tmux-python/libtmux-mcp(15 rawcmd()call sites → typed wrappers) surfaced 9 spots where the wrapper either doesn't exist or doesn't quite fit a tmux invocation that the underlying command supports. A follow-up sweep against tmux 3.6a's fullcmd_entryflag schemas andformat_table[]registry surfaced 4 more gap categories (gaps#10–#13) plus an 8-token forward-looking set from tmux master post-3.6a (gap#14). tmux source citations below reference local copies at~/study/c/tmux-3.6a/(matching the upstreamtmux/tmux@3.6atag — the latest tmux release as of 2026-05-12) and link to the same files on GitHub attmux/tmux/tree/3.6a.Each gap is independently fixable; they cluster into five shapes (see end of issue).
1.
Server.display_messagedoesn't existSite: Only
Pane.display_messageis wrapped (pane.py:625-774).Serverhasshow_messages(message log) but no format-string evaluator.Why tmux supports it:
cmd-display-message.c:38-50(local:~/study/c/tmux-3.6a/cmd-display-message.c:38-50) — the entry's target isCMD_FIND_PANEwithCMD_FIND_CANFAIL, so omitting-tfalls back to the current/default pane and the format engine still resolves server-scoped variables like#{version}and#{socket_path}.Where it bites callers: libtmux-mcp's
get_server_infoand_effective_socket_pathneed#{version}/#{socket_path}reads with no specific pane handle; they have to drop down toserver.cmd("display-message", "-p", "#{…}"). Three sites in the MCP carry this workaround.Proposed: add
Server.display_message(format_string, *, get_text=True, target_client=…)returninglist[str]. Same shape asPane.display_messagebut no-tinjection.2.
Window.display_messagedoesn't existSite: No wrapper at the Window level either.
Why tmux supports it: Same
CMD_FIND_PANE | CMD_FIND_CANFAILtarget as gap#1(cmd-display-message.c:46); callers commonly pass-t @<window>for window-scoped reads like#{window_zoomed_flag}.Where it bites callers: libtmux-mcp's
resize_pane(zoom=…)needs the zoom state before deciding to toggle; with noWindow.display_message, it must callwindow.cmd("display-message", "-p", "#{window_zoomed_flag}")directly.Proposed: add
Window.display_message(same signature asPane.display_message), or generalize the Pane method to accept arbitrary targets.3.
Window.window_zoomed_flagis not a typed attribute on the dataclassSite:
window.py:53— theWindowdataclass declares manywindow_*fields but notwindow_zoomed_flag. mypy rejectswindow.window_zoomed_flageven afterwindow.refresh().Why tmux supports it: the variable is a first-class format token with a dedicated callback at
format.c:2854-2864(local:~/study/c/tmux-3.6a/format.c:2854), formally registered in the format table atformat.c:3557.Where it bites callers: the umbrella issue libtmux-mcp#46 suggested swapping a
display-messagecall for the ORM read — turned out it doesn't type-check, so the MCP reverted tocmd()with a comment.Proposed: add
window_zoomed_flag: str | None(and other commonwindow_*format vars) as declared fields populated viarefresh(). See gap#10for the full undeclared token set across all object scopes.4.
Pane.reset()is still broken in 0.56.0 (open as #650)Site:
pane.py:2150-2153:The
\;is tmux's interactive command separator.subprocess.runargv doesn't expand it — tmux sees a single argv"-R \; clear-history"and interprets the trailing\; clear-historyas literal keys to send, never runningclear-history. Pre-existing tracking issue: #650.Why tmux supports both individually:
send-keys -Ris handled atcmd-send-keys.c:217-220(local:~/study/c/tmux-3.6a/cmd-send-keys.c:217);clear-historyis its own entry incmd-capture-pane.c.Where it bites callers: libtmux-mcp's
clear_pane(seetools/pane_tools/io.py) keeps a two-call workaround. Comment in the MCP cites this issue.Proposed: split into two
cmd()calls insidePane.reset(the same workaround the MCP carries), or use tmux's\;separator viashell=True.5.
Pane.send_keyshas no flag-only invocationSite:
pane.py:499-622. The wrapper always appendsprefix + cmd(orcopy_mode_cmd) as a trailing argv:There's no path that emits just
tmux send-keys -R(reset flag, no key argument).Why tmux supports it:
cmd-send-keys.c:223-227(local:~/study/c/tmux-3.6a/cmd-send-keys.c:223) — whencount == 0and either-Nor-Ris set, the command returnsCMD_RETURN_NORMALwithout sending any keys. tmux's authors explicitly handle the flag-only case.Where it bites callers: libtmux-mcp's
clear_paneneedstmux send-keys -R(no keys).pane.send_keys("", reset=True, enter=False)producestmux send-keys -R ""— likely a no-op in practice, but the trailing empty arg isn't what the underlying tmux command expects for a flag-only invocation. The MCP keepspane.cmd("send-keys", "-R")for that reason.Proposed: make the
cmdparameterOptional[str]with defaultNone; whenNoneand a flag likeresetorrepeatis set, emittmux send-keys <flags>with no trailing arg. Backward-compatible becausecmdis currently positional-required.6.
Server.list_buffers()returns default-formatted lines, not raw namesSite:
server.py:1643-1663— callstmux list-bufferswith no-F, so the output is tmux's default template (buf_name: N bytes: …). Noformat=kwarg on the wrapper.Why tmux supports it:
cmd-list-buffers.c:35-44(local:~/study/c/tmux-3.6a/cmd-list-buffers.c:35) — entry usage is[-F format] [-f filter]; the-Ftemplate is read at line 56 with a builtin fallback, the-ffilter at line 58.Where it bites callers: libtmux-mcp's buffer GC needs just the names matching
libtmux_mcp_*prefix. The wrapper returns lines likelibtmux_mcp_abc: 5 bytes: "…"; parsing requires regex. The MCP keepsserver.cmd("list-buffers", "-F", "#{buffer_name}")instead.Proposed: add
format: str | None = Noneandfilter: str | None = Nonekwargs toServer.list_buffers. Whenformatis set, pass-F <fmt>; otherwise keep the default tmux output for backward compat.7.
server.panes/QueryListdoesn't expose tmux's-fC-filterSite: [
Server.panesat server.py:2060-2076](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/server.py#L2060) returns aQueryList[Pane]that filters in Python. Callers who want the **C-level filter** with format-expression predicates have no wrapper path; [fetch_objsin neo.py](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/neo.py#L248) always emits libtmux's own-F{format_string}` template.Why tmux supports it:
cmd-list-panes.c:37-42(local:~/study/c/tmux-3.6a/cmd-list-panes.c:37) — entry usage is[-as] [-F format] [-f filter]; the filter is read at line 125 and evaluated byformat_true(expanded)at line 134, gating which panes appear in the output.Where it bites callers: libtmux-mcp's
search_panesfast path usestmux list-panes -a -f '#{C/i:pattern}' -F '#{pane_id}'— pushes a regex match into tmux's C code, returning only matching pane IDs without capturing every pane in Python. With QueryList, the only option is "capture all panes in Python and filter post-hoc" — orders of magnitude slower for the common case. The MCP keepsserver.cmd("list-panes", ...)for this site.Proposed: add a
filter: str | None = Nonekwarg toServer.panes(and the analogousSession.panes,Window.panes) that round-trips a tmux format expression through-f. Alternatively, aServer.list_panes(filter=…, format=…)low-level method.8.
LibTmuxExceptiontranslation loses subcommand contextSite: wrappers like
Session.last_windowraiseexc.LibTmuxException(proc.stderr)(session.py:298-318). The exception (exc.py:18-19— a bareExceptionsubclass with no payload fields) carries the raw stderr list but not the subcommand name that produced it.Why this matters: downstream wrappers (libtmux-mcp's
handle_tool_errorstranslatesLibTmuxException → ToolError(f"tmux error: {e}")) and any agent-facing error string lose the "which tmux command failed" context. Pre-0.56.0, the MCP builtf"tmux {subcommand} failed: {stderr}"manually; the wrapper version surfaces only"tmux error: ['no last window']". One MCP test had to be updated to match the new shape.Proposed: extend
LibTmuxException(and friends) with an optionalsubcommand: str | Noneattribute. Format__str__asf"{subcommand}: {stderr_text}"when present. Wrappers populate it implicitly (e.g., a_run_cmd("last-window", *args)helper that catches and re-raises with the subcommand).9.
Pane.cmdalways injects-t <pane_id>, no warning on duplicateSite:
Pane.cmdat pane.py:178-212](https://github.com/tmux-python/libtmux/blob/v0.56.0/src/libtmux/pane.py#L178) always setstarget = self.pane_idbefore delegating toServer.cmd. [Server.cmdat server.py:326then emits-t <target>at the front ofcmd_argsregardless of what the caller passes in*args. If the caller also adds-t %X, tmux seestmux <subcmd> -t %1 -t %1and silently accepts the duplicate.Why tmux tolerates it: tmux's
args.cargs_get()returns the last value of a repeated flag — so-t %1 -t %1is functionally-t %1. Safe but masks caller bugs.Where it bites callers: libtmux-mcp had two sites doing this (
buffer_tools.py:275andcopy_mode.py:60-68) before the 0.56.0 wrappers were adopted; both were quietly accepted by tmux despite being incorrect Python. Discovered only because the new wrappers (which inject-tcorrectly) made the duplicate impossible to keep.Proposed: when
Pane.cmd(andSession.cmd,Window.cmd,Server.cmd) detect a-talready in*args, either (a) raise a clearTypeError, or (b) emitwarnings.warn("Pane.cmd auto-injects -t; redundant -t in args")once per call site. Option (a) is safer; option (b) is gentler if the existing wild ecosystem relies on the no-op.10. ~37 first-class format tokens aren't typed on
Pane/Window/SessiondataclassesSite:
neo.py:139-159declares a hand-curated allowlist of format tokens asstr | None = Nonefields onObj. Sweeping tmux'sformat_table[]array at format.c:3010-3563 againstneo.py's declared fields shows 37 scope-relevant tokens that ship in tmux 3.6a but are not declared. Gap#3is one instance; the full set:Pane (13) — registered in
format.cbetween line 3254 and 3341:pane_deadpane_formatpane_in_modepane_input_offpane_key_modepane_lastpane_markedpane_marked_setpane_modepane_pathpane_pipepane_synchronizedpane_unseen_changesWindow (13) — registered in
format.cbetween line 3464 and 3557:window_active_clients_listwindow_active_sessions_listwindow_activity_flagwindow_bell_flagwindow_biggerwindow_end_flagwindow_flagswindow_formatwindow_last_flagwindow_silence_flagwindow_start_flagwindow_visible_layoutwindow_zoomed_flag#3Session (11) — registered in
format.cbetween line 3359 and 3428:session_activesession_activity_flagsession_alertsession_bell_flagsession_formatsession_group_attached_listsession_group_many_attachedsession_groupedsession_many_attachedsession_markedsession_silence_flagWhy tmux supports them: every entry above appears verbatim in the
format_table[]array — they're registered first-class format tokens withFORMAT_TABLE_STRINGcallbacks. tmux'sformat_create_defaultspath populates them whenever the relevant scope is bound (pane→format_defaults_pane, window→format_defaults_winlink, session→format_defaults_session).Where it bites callers: every undeclared token forces typed-Python callers back to
pane.cmd("display-message", "-p", "#{…}"). libtmux-mcp'sresize_pane(zoom=…)(window_zoomed_flag), copy-mode detection (pane_in_mode,pane_mode), and pane-death checks (pane_dead) all carry these workarounds.Proposed: declare the full scope-relevant set on
Obj(or split into per-scope mixins). Better: generate the field declarations fromformat.cparsing at build time so future tmux releases don't drift the typed surface — that closes gap#14(HEAD tokens) for free.11. No
Clientclass —client_*format tokens have nowhere to landSite:
Server.list_clientsat server.py:1392 returns raw output lines. NoClientdataclass exists insrc/libtmux/. Compare withSession/Window/Pane, which each have anObj-backed dataclass and aQueryList-returning collection accessor.Why tmux supports it: twelve
client_*format tokens are registered informat.c:client_activityclient_control_modeclient_createdclient_last_sessionclient_mode_formatclient_prefixclient_readonlyclient_sessionclient_termfeaturesclient_termtypeclient_themeclient_utf8The matching tmux subcommand is
cmd-list-clients.c, which accepts[-F format] [-t target-session]for arbitrary projection.Where it bites callers: anything inspecting attached clients (multi-client coordination, "is this client read-only?", per-client status drawing) must call
server.cmd("list-clients", "-F", "#{…}")and hand-parse output. The typed-ORM ergonomics that exist for sessions/windows/panes simply aren't available for clients.Proposed: add
src/libtmux/client.pywith aClient(Obj)dataclass declaring all twelveclient_*fields, plusServer.clients -> QueryList[Client]mirroringServer.sessions. Reusefetch_objswithlist_cmd="list-clients".12.
Server.run_shelldrops-c(cwd) and-E(show_stderr) flagsSite:
server.py:427-486exposesbackground(-b),delay(-d),as_tmux_command(-C), andtarget_pane(-t). The full flag schema in tmux is"bd:Ct:Es:c:".Why tmux supports them:
cmd-run-shell.c:47declares the args;cmd-run-shell.c:156-159reads-cto override the working directory (cdata->cwd = xstrdup(args_get(args, 'c'))), andcmd-run-shell.c:161-162reads-Eto enableJOB_SHOWSTDERR— the latter is the difference between "stdout only" and "stdout + stderr merged into the captured output."Where it bites callers: any caller that needs the shell command to run in a specific directory (independent of any target pane's cwd) or wants to surface stderr in the result must drop to
server.cmd("run-shell", …). libtmux-mcp's diagnostic shell-outs (e.g., probing helper binaries) fall into this bucket.Proposed: add
cwd: StrPath | None = Noneandshow_stderr: bool | None = Nonekwargs, emitting-c <cwd>and-Erespectively.13.
Pane.capture_panedrops-P(pending input) flagSite:
pane.py:354-498exposes 12 of the 13 flags from tmux's"ab:CeE:JMNpPqS:Tt:"schema.-Pis the missing one.Why tmux supports it:
cmd-capture-pane.c:42declares the args;cmd-capture-pane.c:231-232branches: when-Pis set,cmd_capture_pane_pending(args, wp, &len)returns bytes tmux has buffered as input for the pane but the program hasn't consumed yet — distinct from-p(lowercase, "print to stdout") and from the default history capture.Where it bites callers: diagnostic tooling that needs to inspect "what input is queued but unprocessed?" (debugging hung programs, copy-mode race conditions, paste-buffer drains) has no typed access; callers must do
pane.cmd("capture-pane", "-P", "-p").Proposed: add
pending: bool = Falsekwarg. When set, emit-P(and keep-pfor stdout return). Document that this returns pending input bytes, not history.14. Forward-looking: 8 new format tokens in tmux master post-3.6a
Site: None of these are declared on libtmux dataclasses (same shape as gap
#10), and they don't yet exist in tmux 3.6a — they were added between the 3.6a tag and tmux master. Tracking them now means libtmux is ready when the next tmux tag lands.Why tmux is adding them: present in tmux master (
tmux/tmux/blob/master/format.c), absent from the 3.6a tag. The full diff:pane_zoomed_flagwindow_zoomed_flag)pane_floating_flagpane_flagswindow_flags)pane_pb_statepane_pb_progresspane_pipe_pidpipe-panechild processsynchronized_output_flagbracket_paste_flagWhere it bites callers: none yet — these only become reachable when callers upgrade to the next tmux release. But the
Objallowlist will need to be re-walked when that happens. Treating gap#10's fix as "auto-generated from tmux'sformat.c" makes gap#14free.Proposed: roll into the same generator described under gap
#10— when tmux ships these in a tagged release, regenerate; no manual list to maintain.Suggested groupings for fix PRs
These cluster into five shapes — each cluster is a coherent PR:
Cluster A —
display_messageparity (gaps#1,#2)Add
Server.display_messageandWindow.display_messagewith the same signature asPane.display_message. Single-commit PR; mirrors the symmetry the rest of the 0.56.0 release set up.Cluster B — typed format-token coverage (gaps
#3,#10,#14)Expand the declared field set on
Pane/Window/Sessionto cover all scope-relevant tokens fromformat.c'sformat_table[]. Prefer a build-time generator that readsformat.cso the typed surface tracks tmux automatically; that closes gap#14(HEAD tokens) for free when the next tmux release tag lands.Cluster C — flag-only / format-aware wrappers (gaps
#5,#6,#7,#12,#13)Make
cmdoptional onPane.send_keysto enable flag-only invocations; addformat=/filter=kwargs toServer.list_buffersandServer.panes(et al.) to expose tmux's C-level filtering; addcwd=/show_stderr=toServer.run_shell; addpending=toPane.capture_pane. These all expose existing tmux capability that the wrapper currently hides.Cluster D — new
Clientobject hierarchy (gap#11)Add
Client(Obj)dataclass with the twelveclient_*fields andServer.clients -> QueryList[Client]. Mirrors theSession/Window/Paneshape and bringslist-clientsinto the typed API.Cluster E — diagnostics and safety (gaps
#4,#8,#9)Fix
Pane.resetper #650; thread subcommand context throughLibTmuxException; add duplicate--tdetection on thecmdmethods. Quality-of-life cluster; each item is small but compounds across the wider ecosystem.Evidence / Motivation
The MCP-side adoption PR is at tmux-python/libtmux-mcp#48. Gaps
#1–#9are documented inline in commit messages or code comments there — pointers in the PR description and the umbrella issue libtmux-mcp#46. Gaps#10–#14surfaced from a follow-up sweep oftmux/tmux@3.6a'scmd_entryflag strings andformat_table[]cross-referenced againstsrc/libtmux/neo.pyfield declarations andsrc/libtmux/*.pywrapper signatures.