Custom workspace builders

A workspace builder turns an expanded workspace dict into a live tmux session. tmuxp ships one builder, ClassicWorkspaceBuilder, and uses it by default. You can point tmuxp at a different builder, and packagers can distribute builders that you select by name.

This is an advanced, opt-in feature. Existing workspace files keep using the classic builder with no changes.

See also

If you only want to set these keys in a workspace file, see Workspace builders in the configuration reference.

How a workspace is built

  1. tmuxp load reads the YAML/JSON file and expands it (shorthand, environment variables, trickle-down defaults).

  2. tmuxp resolves which builder to use from the workspace_builder config key (default: the classic builder).

  3. tmuxp constructs the builder with the expanded workspace and a libtmux.Server, then calls build().

  4. The builder creates the session, windows, and panes, honoring plugin hooks and progress callbacks.

Selecting a builder

By dotted path

Point workspace_builder at an importable class. Both a module:attr object reference and a dotted module.attr path are accepted:

session_name: my-session
workspace_builder: my_tmuxp_builders.builders:CustomBuilder
windows:
  - window_name: editor
    panes:
      - vim

By entry-point name

Packaged builders register under the tmuxp.workspace_builders entry-point group, letting users select them by a short name instead of an internal module path:

workspace_builder: classic

The built-in classic builder is registered this way. A distribution registers its own builder in pyproject.toml:

[project.entry-points."tmuxp.workspace_builders"]
mybuilder = "my_tmuxp_builders.builders:CustomBuilder"

Trusted import paths

When a builder lives outside tmuxp’s runtime environment (for example, a script in your config directory), list trusted directories in workspace_builder_paths. tmuxp expands ~ and environment variables, resolves relative entries against the workspace file’s directory, requires each entry to be an existing directory, and temporarily prepends them to sys.path for the import and build:

workspace_builder: my_local_builder:CustomBuilder
workspace_builder_paths:
  - ~/.config/tmuxp/builders

Warning

A workspace file that names a builder runs that builder’s Python code. Only load workspace files you trust. tmuxp deliberately does not use site.addsitedir() for these paths — that would execute .pth startup files and is broader than making a module importable.

Writing a builder

The simplest custom builder subclasses the classic builder and overrides what it needs:

from tmuxp.workspace.builder.classic import ClassicWorkspaceBuilder


class CustomBuilder(ClassicWorkspaceBuilder):
    """A builder that renames the session after building."""

    def build(self, session=None, append=False):
        super().build(session=session, append=append)
        self.session.rename_session(f"{self.session.name}-custom")

A builder written from scratch must satisfy WorkspaceBuilderProtocol. The contract covers what tmuxp load drives:

The contract is synchronous today. It is shaped so an async builder can be added later as an additive extension without changing this surface.

Pane readiness

tmuxp waits for a pane’s shell prompt before dispatching layout and commands, which avoids a zsh prompt-redraw artifact. That wait is only needed for zsh, so it is configurable independently of which builder you use, through the workspace_builder_options catalog:

workspace_builder_options:
  pane_readiness: auto   # auto | always | never (+ truthy/falsy aliases)
  • auto (default) — wait only when the session’s interactive shell is zsh.

  • always (or true/on/yes/1) — always wait for default-shell panes.

  • never (or false/off/no/0) — never wait; fastest, but accepts the prompt/layout race for shells that need it.

Panes with a custom shell or window_shell never wait, regardless of policy — those run a command in place of an interactive shell, so there is no prompt to wait for.

See PaneReadiness and WorkspaceBuilderOptions for the parsing rules.

Testing a builder

Resolve, construct, and build against a real tmux server fixture:

from tmuxp.workspace import loader
from tmuxp.workspace.builder import registry


def test_custom_builder(server):
    config = loader.expand(
        {
            "session_name": "demo",
            "workspace_builder": "my_tmuxp_builders.builders:CustomBuilder",
            "windows": [{"window_name": "main", "panes": [{"shell_command": []}]}],
        },
    )
    builder_cls = registry.resolve_builder_class(config)
    builder = builder_cls(session_config=config, server=server)
    builder.build()
    assert builder.session.name == "demo"
    builder.session.kill()

For builders that live in a trusted directory, build the sys.path sandbox with resolve_builder_paths() and prepended_sys_path().

Choosing an approach

  • Classic builder — the default. Use it for any workspace that depends on strict, pane-by-pane side effects (start_directory, shell, window_shell, pane environment).

  • Readiness tuning — set pane_readiness to trade prompt-safety for speed without swapping builders.

  • A custom builder — when you need behavior the classic builder doesn’t provide. Keep dependency-sensitive setup in before_script or shell_command_before if your builder relaxes ordering guarantees.

Reference