GeezerVison is showing github.com — feat: tmuxinator/teamocil feature parity by tony · Pull Request #1025 · tmux-python/tmuxp
Conversation
tony
commented
Mar 17, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
Summary
Bring tmuxp to feature parity with tmuxinator and teamocil. This adds the missing CLI commands, config keys, lifecycle hooks, config templating, and importer improvements that users migrating from those tools expect — plus a full feature comparison page and documentation for every new command, flag, and config key.
New CLI commands
tmuxp stop — kill a session with cleanup
Runs the on_project_stop lifecycle hook before killing the session, giving projects a chance to tear down background services, save state, etc.
tmuxp new — create a workspace config
Creates a new workspace config from a minimal template and opens it in $EDITOR.
tmuxp copy — copy a workspace config
$ tmuxp copy myproject myproject-backup
Copies an existing workspace config to a new name. Source is resolved using the same logic as tmuxp load.
tmuxp delete — delete workspace configs
$ tmuxp delete old-project
Deletes workspace config files. Prompts for confirmation unless -y is passed.
Lifecycle hooks
Workspace configs now support four hooks, matching tmuxinator's hook system:
session_name: myproject
on_project_start: docker compose up -d
on_project_exit: docker compose down
on_project_stop: docker compose down -v
on_project_restart: echo "Reattaching..."
windows:
- window_name: editor
panes:
- vim
| Hook |
When it runs |
on_project_start |
Before session build (new session creation only) |
on_project_restart |
When reattaching to an existing session (confirmed attach only) |
on_project_exit |
When the last client detaches (via tmux client-detached hook) |
on_project_stop |
Before tmuxp stop kills the session |
Config templating
Workspace configs now support {{ variable }} placeholders with values passed via --set. Given mytemplate.yaml:
session_name: "{{ project }}"
start_directory: "~/code/{{ project }}"
windows:
- window_name: editor
panes:
- vim
$ tmuxp load --set project=myapp mytemplate.yaml
New config keys
Pane titles
session_name: dashboard
enable_pane_titles: true
pane_title_position: top
pane_title_format: "#{pane_title}"
windows:
- window_name: main
panes:
- title: logs
shell_command: tail -f /var/log/syslog
- title: editor
shell_command: vim
synchronize shorthand
windows:
- window_name: multi-server
synchronize: before # or: after, true
panes:
- ssh server1
- ssh server2
Desugars synchronize: before → options: {synchronize-panes: on} and synchronize: after → options_after: {synchronize-panes: on}. true is equivalent to before.
shell_command_after and clear
windows:
- window_name: dev
shell_command_after:
- echo "Window ready"
clear: true
panes:
- vim
- npm run dev
New tmuxp load flags
| Flag |
Description |
--here |
Reuse the current tmux window instead of creating a new session |
--no-shell-command-before |
Skip all shell_command_before entries |
--debug |
Show tmux commands as they execute (disables progress spinner) |
--set KEY=VALUE |
Pass template variables for config templating |
Importer improvements
tmuxinator
pre → on_project_start, pre_window → shell_command_before
cli_args (-f, -S, -L) parsed into tmuxp equivalents
synchronize window key converted
startup_window / startup_pane → focus: true on the target
- Named panes (hash-key syntax) →
title on the pane
- Window names coerced to
str (fixes numeric/emoji YAML keys)
teamocil
- v1.x format support (
windows at top level, commands key in panes)
focus: true on windows and panes converted
- Window
options passed through
Design decisions
- Template syntax is plain
{{ var }} substitution, not Jinja2 — no
conditionals or loops. Keeps configs declarative and avoids a template-engine
dependency; --set values are validated against YAML-unsafe characters.
--here provisions the reused pane with respawn-pane -k rather than
typing cd/export into the running shell — no POSIX-shell assumption, no
keystrokes landing in foreground programs. tmuxp warns first if the pane has
running child processes, since -k kills them.
on_project_exit rides tmux's client-detached hook, so it fires on any
client detach — not only tmuxp-initiated ones.
Documentation
- Feature comparison page (
docs/comparison.md): Side-by-side of tmuxp vs tmuxinator vs teamocil — architecture, config keys, CLI flags, hooks
- Top-level config docs (
docs/configuration/top-level.md): New keys, lifecycle hooks, synchronize, pane titles
- Config examples (
docs/configuration/examples.md): Working examples for each new feature
- CLI docs: Pages for stop, new, copy, delete, plus updated load docs
- Import docs: Updated with notes on new importer capabilities
Related issues
Test plan
-
uv run py.test passes
- Tests for every new feature: stop, new, copy, delete, load flags, lifecycle hooks, config templating, builder, importers
- Importer fixtures for edge cases (numeric names, YAML aliases, named panes, v1.x teamocil format)
- Manual:
tmuxp load with lifecycle hooks, --here, --debug, --set
- Manual:
tmuxp stop, tmuxp new, tmuxp copy, tmuxp delete
- Manual: Import from tmuxinator/teamocil configs with new features
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
codecov
Bot
commented
Mar 17, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
❌ Patch coverage is 88.85542% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.34%. Comparing base (98af6d1) to head (f5f2530).
Additional details and impacted files
@@ Coverage Diff @@
## master #1025 +/- ##
==========================================
+ Coverage 82.55% 84.34% +1.79%
==========================================
Files 31 35 +4
Lines 2768 3341 +573
Branches 518 662 +144
==========================================
+ Hits 2285 2818 +533
- Misses 346 361 +15
- Partials 137 162 +25
☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.
🚀 New features to boost your workflow:
- ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Code review
Found 4 issues:
- Shell command injection via
start_directory in --here mode. The --here path constructs f'cd "{start_directory}"' and sends it as keystrokes via send_keys. A start_directory containing " or ; can inject arbitrary shell commands into the pane. The non---here path safely passes start_directory as a structured argument to new_window(). Consider using shlex.quote() or passing the directory through tmux's -c flag instead.
|
if start_directory: |
|
active_pane = window.active_pane |
|
if active_pane is not None: |
|
active_pane.send_keys( |
|
f'cd "{start_directory}"', |
|
enter=True, |
|
) |
- Missing doctests on
command_copy, command_delete, command_new, command_stop (CLAUDE.md says "All functions and methods MUST have working doctests.")
|
def command_copy( |
|
source: str, |
|
destination: str, |
|
parser: argparse.ArgumentParser | None = None, |
|
color: CLIColorModeLiteral | None = None, |
|
) -> None: |
|
"""Entrypoint for ``tmuxp copy``, copy a workspace config to a new name.""" |
|
def command_delete( |
|
workspace_names: list[str], |
|
answer_yes: bool = False, |
|
parser: argparse.ArgumentParser | None = None, |
|
color: CLIColorModeLiteral | None = None, |
|
) -> None: |
|
"""Entrypoint for ``tmuxp delete``, remove workspace config files.""" |
|
color_mode = get_color_mode(color) |
|
def command_new( |
|
workspace_name: str, |
|
parser: argparse.ArgumentParser | None = None, |
|
color: CLIColorModeLiteral | None = None, |
|
) -> None: |
|
"""Entrypoint for ``tmuxp new``, create a new workspace config from template.""" |
|
color_mode = get_color_mode(color) |
|
colors = Colors(color_mode) |
|
def command_stop( |
|
args: CLIStopNamespace, |
|
parser: argparse.ArgumentParser | None = None, |
|
) -> None: |
|
"""Entrypoint for ``tmuxp stop``, kill a tmux session.""" |
|
color_mode = get_color_mode(args.color) |
|
colors = Colors(color_mode) |
- Missing doctest on
_load_here_in_current_session (CLAUDE.md says "All functions and methods MUST have working doctests.")
|
def _load_here_in_current_session(builder: WorkspaceBuilder) -> None: |
|
"""Load workspace reusing current window for first window. |
|
|
|
Parameters |
|
---------- |
|
builder: :class:`workspace.builder.WorkspaceBuilder` |
|
""" |
|
current_attached_session = builder.find_current_attached_session() |
|
builder.build(current_attached_session, here=True) |
socket_name extracted from cli_args: "-L mysocket" is silently overwritten if the tmuxinator config also has an explicit socket_name key. The -L value is parsed at line 103, then unconditionally replaced at lines 105-106. Consider only overwriting if the explicit key exists and differs, or logging a warning about the conflict.
|
|
|
raw_args = workspace_dict.get("cli_args") or workspace_dict.get("tmux_options") |
|
if raw_args: |
|
tokens = shlex.split(raw_args) |
|
flag_map = {"-f": "config", "-L": "socket_name", "-S": "socket_path"} |
|
it = iter(tokens) |
|
for token in it: |
|
if token in flag_map: |
|
value = next(it, None) |
|
if value is not None: |
|
tmuxp_workspace[flag_map[token]] = value |
|
|
|
if "socket_name" in workspace_dict: |
|
tmuxp_workspace["socket_name"] = workspace_dict["socket_name"] |
|
|
🤖 Generated with Claude Code
- If this code review was useful, please react with 👍. Otherwise, react with 👎.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Code review
No issues found. Checked for bugs and CLAUDE.md compliance.
Review scope (5 parallel agents):
- CLAUDE.md compliance audit — 4 minor findings, all below confidence threshold after scoring
- Shallow bug scan — 3 candidates; 1 debunked (shlex.quote + replace is valid POSIX quoting), 1 confirmed as correct tmuxinator parity, 1 edge case with adequate logging
- Git history context — no regressions found vs. prior commits
- Prior PR comments — color hierarchy and prompt patterns from PRs CLI Colors #1006/feat(load): animated progress spinner for tmuxp load #1020 verified compliant
- Code comments compliance — 1 stale docstring (pre-existing, not introduced by this PR)
Prior review rounds: 3 rounds of 3-model (Claude/Gemini/GPT) loom reviews found 16 issues, all fixed in subsequent commits on this branch.
🤖 Generated with Claude Code
- If this code review was useful, please react with 👍. Otherwise, react with 👎.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Code review
Found 1 issue:
_load_here_in_current_session and _dispatch_build use callable() as a doctest placeholder, providing zero documentation or testing value (CLAUDE.md says "All functions and methods MUST have working doctests. Doctests serve as both documentation and tests." and "If you cannot create a working doctest, STOP and ask for help")
|
-------- |
|
>>> from tmuxp.cli.load import _load_here_in_current_session |
|
>>> callable(_load_here_in_current_session) |
|
True |
|
-------- |
|
>>> from tmuxp.cli.load import _dispatch_build |
|
>>> callable(_dispatch_build) |
|
True |
🤖 Generated with Claude Code
- If this code review was useful, please react with 👍. Otherwise, react with 👎.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Code review
Found 2 issues:
on_project_start fires unconditionally in --here mode even when the session already exists. The guard at line 793 (not here) causes the session-exists block to be skipped entirely, so execution always falls through to line 815 which runs on_project_start unconditionally. This contradicts commit 54afda7 which deliberately tightened on_project_start to fire on new session creation only.
|
# Session-exists check — outside spinner so prompt_yes_no is safe |
|
if builder.session_exists(session_name) and not append and not here: |
|
_confirmed = not detached and ( |
|
answer_yes |
|
or prompt_yes_no( |
|
f"{cli_colors.highlight(session_name)} is already running. Attach?", |
|
default=True, |
|
color_mode=cli_colors.mode, |
|
) |
|
) |
|
# Run on_project_restart hook — only when actually reattaching |
|
if _confirmed: |
|
if "on_project_restart" in expanded_workspace: |
|
_hook_cwd = expanded_workspace.get("start_directory") |
|
util.run_hook_commands( |
|
expanded_workspace["on_project_restart"], |
|
cwd=_hook_cwd, |
|
) |
|
_reattach(builder, cli_colors) |
|
_cleanup_debug() |
|
return None |
|
|
|
# Run on_project_start hook — fires before new session build |
|
if "on_project_start" in expanded_workspace: |
|
_hook_cwd = expanded_workspace.get("start_directory") |
|
util.run_hook_commands( |
|
expanded_workspace["on_project_start"], |
|
cwd=_hook_cwd, |
_validate_template_values doctest uses ... in the expected ValueError output without # doctest: +ELLIPSIS. It passes under pytest (global ELLIPSIS in pyproject.toml) but fails under python3 -m doctest. CLAUDE.md requires "Ellipsis for variable output: # doctest: +ELLIPSIS".
|
|
|
>>> _validate_template_values({"key": "foo: bar"}) |
|
Traceback (most recent call last): |
|
... |
|
ValueError: --set value for 'key' contains YAML-unsafe characters ... |
|
""" |
Generated with Claude Code
- If this code review was useful, please react with 👍. Otherwise, react with 👎.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Code review
No issues found. Checked for bugs and CLAUDE.md compliance.
🤖 Generated with Claude Code
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Code review
Found 1 issue:
_running_inside_pane compares the TMUX env socket (always str) against server.socket_path, which libtmux types as str | pathlib.Path | None. When a programmatic caller constructs Server(socket_path=pathlib.Path(...)), str != Path is always True, so the genuine self-pane is misclassified as not-self and the code falls through to respawn-pane -k — killing the tmuxp process mid-build, the exact failure this guard exists to prevent. Fix: compare against str(socket_path) (and/or normalize at the call site).
|
tmux = environ.get("TMUX") |
|
tmux_pane = environ.get("TMUX_PANE") |
|
if not tmux or not tmux_pane or pane_id is None: |
|
return False |
|
env_socket = tmux.split(",")[0] |
|
if socket_path is not None and env_socket != socket_path: |
|
return False |
|
return tmux_pane == pane_id |
Call site passing the un-normalized attribute:
|
if _here_pane is not None and _running_inside_pane( |
|
_here_pane.pane_id, |
|
socket_path=getattr(self.server, "socket_path", None), |
|
environ=os.environ, |
|
): |
🤖 Generated with Claude Code
- If this code review was useful, please react with 👍. Otherwise, react with 👎.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Code review
No issues found. Checked for bugs and CLAUDE.md compliance.
🤖 Generated with Claude Code
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
why: tmuxp lacked several tmuxinator and teamocil workflows, so configs
from those tools lost behavior on import and users missed the lifecycle
and session-management commands they relied on.
what:
- Add session lifecycle commands: tmuxp stop / new / copy / delete.
- Add project lifecycle hooks: on_project_start / restart / exit / stop.
- Add {{ variable }} config templating, filled via --set at load time.
- Add pane titles and window keys (synchronize, shell_command_after,
clear).
- Add tmuxp load flags: --here, --no-shell-command-before, --debug.
- Broaden tmuxinator/teamocil imports: hooks, named panes, startup
focus, synchronized windows, tmux CLI args, teamocil v1.x format.
- Extend the builder protocol build() with here= for current-window
loads so all builders share the contract.
This branch has not been deployed
No deployments
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|