tmux-agents 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tmux_agents-0.1.0/.github/workflows/ci.yml +23 -0
- tmux_agents-0.1.0/.gitignore +9 -0
- tmux_agents-0.1.0/CHANGELOG.md +10 -0
- tmux_agents-0.1.0/CONTRIBUTING.md +6 -0
- tmux_agents-0.1.0/LICENSE +21 -0
- tmux_agents-0.1.0/PKG-INFO +182 -0
- tmux_agents-0.1.0/README.md +153 -0
- tmux_agents-0.1.0/SECURITY.md +7 -0
- tmux_agents-0.1.0/examples/claude_desktop_config.json +12 -0
- tmux_agents-0.1.0/examples/orchestrate.md +15 -0
- tmux_agents-0.1.0/pyproject.toml +57 -0
- tmux_agents-0.1.0/src/tmux_agents/__init__.py +3 -0
- tmux_agents-0.1.0/src/tmux_agents/__main__.py +20 -0
- tmux_agents-0.1.0/src/tmux_agents/redact.py +42 -0
- tmux_agents-0.1.0/src/tmux_agents/server.py +192 -0
- tmux_agents-0.1.0/src/tmux_agents/settings.py +92 -0
- tmux_agents-0.1.0/src/tmux_agents/tmux.py +305 -0
- tmux_agents-0.1.0/tests/conftest.py +40 -0
- tmux_agents-0.1.0/tests/test_redact.py +23 -0
- tmux_agents-0.1.0/tests/test_server.py +71 -0
- tmux_agents-0.1.0/tests/test_tmux.py +116 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
runs-on: ${{ matrix.os }}
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
os: [ubuntu-latest, macos-latest]
|
|
12
|
+
python: ["3.10", "3.12"]
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: ${{ matrix.python }}
|
|
18
|
+
- name: install tmux
|
|
19
|
+
run: |
|
|
20
|
+
if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get update && sudo apt-get install -y tmux; else brew install tmux; fi
|
|
21
|
+
- run: uv sync --extra dev
|
|
22
|
+
- run: uv run ruff check src tests
|
|
23
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-09-22
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `agents_launch`: one session, one titled pane per agent, auto layout, optional terminal-app open.
|
|
8
|
+
- `pane_read`, `pane_wait` (idle / regex), `pane_send` (literal, multi-line safe), `pane_key`.
|
|
9
|
+
- `panes_list`, `tmux_status`, `pane_kill`, `session_kill`.
|
|
10
|
+
- Session-prefix scoping, secret redaction, no raw shell tool.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
- `uv sync --extra dev`, then `uv run pytest` and `uv run ruff check src tests` must pass.
|
|
4
|
+
- Tests run against a real tmux on a private socket; keep new tests that way (no mocks of tmux).
|
|
5
|
+
- No new tool that runs arbitrary commands. The scope is: launch agents, read panes, type into panes, wait, kill.
|
|
6
|
+
- Conventional commit messages (`feat:`, `fix:`, `docs:`).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Woonyong Choi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tmux-agents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server that lets any AI agent launch, watch and steer coding agents (Claude Code, Codex, Hermes, aider, ...) running in tmux panes.
|
|
5
|
+
Project-URL: Homepage, https://github.com/woonyong-choi/tmux-agents
|
|
6
|
+
Project-URL: Issues, https://github.com/woonyong-choi/tmux-agents/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/woonyong-choi/tmux-agents/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Woonyong Choi
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,aider,automation,claude-code,codex,hermes-agent,mcp,model-context-protocol,orchestration,terminal,tmux
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Topic :: Terminals
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: mcp>=1.2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# tmux-agents
|
|
31
|
+
|
|
32
|
+
**Let one AI agent run a room full of coding agents.**
|
|
33
|
+
|
|
34
|
+
`tmux-agents` is a Model Context Protocol server that turns tmux into a control surface for CLI agents. Point Claude, Cursor, Codex, Hermes or any MCP client at it and it can start Claude Code / Codex / Hermes / aider in split panes, watch each one, answer their questions, hand them the next task and tear the session down — while you watch the same panes in your own terminal.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
uvx tmux-agents
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That is the whole install. It needs `tmux` on the machine and nothing else.
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<code>
|
|
44
|
+
┌ WP1 Path ───────────────┬ WP2 Run ────────────────┐<br>
|
|
45
|
+
│ claude --model sonnet │ codex --model gpt-5 │<br>
|
|
46
|
+
│ ✓ 87 tests passed │ ? Overwrite main.ts? y/n│<br>
|
|
47
|
+
├ WP3 Timeline ───────────┼ WP4 Diagrams ───────────┤<br>
|
|
48
|
+
│ hermes chat -q "..." │ aider --yes │<br>
|
|
49
|
+
└─────────────────────────┴─────────────────────────┘
|
|
50
|
+
</code>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
## Why
|
|
54
|
+
|
|
55
|
+
Every coding agent ships as a terminal program, and every orchestrator wants to run several of them at once. The pieces that are missing are small but annoying:
|
|
56
|
+
|
|
57
|
+
- An agent hosted **somewhere else** (a cloud session, a desktop app, a phone client) has no terminal. It can plan the work but has to ask a human to type `run-batch.sh`.
|
|
58
|
+
- Four agents in four windows are four things to babysit. One stops to ask *"overwrite? (y/n)"* and everything waits until someone notices.
|
|
59
|
+
- Reading a pane, deciding it is done, and typing the follow-up is exactly the kind of loop a model is good at — if it can see the pane.
|
|
60
|
+
|
|
61
|
+
`tmux-agents` gives the orchestrating model seven tools that map to what you would do by hand, and nothing more. There is no generic `shell` tool: the model can type into agent panes, not into your machine.
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
| Tool | What it does |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `tmux_status` | Server config, tmux version, visible sessions |
|
|
68
|
+
| `agents_launch` | Create a session with one pane per agent, title each pane, run each command, pick a layout that fits (side-by-side for 2, tiled for 3+), optionally open it in your terminal app |
|
|
69
|
+
| `panes_list` | Panes with title, cwd, running command, size |
|
|
70
|
+
| `pane_read` | Last N lines of a pane, ANSI stripped, secrets redacted |
|
|
71
|
+
| `pane_wait` | Block until a pane is quiet for N seconds or a regex appears — returns `idle` / `matched` / `timeout` plus the tail |
|
|
72
|
+
| `pane_send` | Type text (literally, multi-line safe) and press Enter |
|
|
73
|
+
| `pane_key` | Send `C-c`, `Escape`, `Up`, `Tab` … |
|
|
74
|
+
| `pane_kill` / `session_kill` | Stop one agent or the whole batch (can be disabled) |
|
|
75
|
+
|
|
76
|
+
Panes are addressed by **title** (`"WP2"`, case-insensitive partial match), by id (`%3`) or by tmux target (`batch:0.1`). Titles are shown on the pane borders, so what the model calls a pane is what you see.
|
|
77
|
+
|
|
78
|
+
## Setup
|
|
79
|
+
|
|
80
|
+
### Claude Code
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
claude mcp add tmux-agents -- uvx tmux-agents
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Claude Desktop / Cursor / Windsurf / Codex
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"tmux-agents": {
|
|
92
|
+
"command": "uvx",
|
|
93
|
+
"args": ["tmux-agents"],
|
|
94
|
+
"env": {
|
|
95
|
+
"TMUX_AGENTS_SESSION_PREFIX": "agents-",
|
|
96
|
+
"TMUX_AGENTS_OPEN_COMMAND": "open -na Ghostty.app --args -e tmux attach -t {session}"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`TMUX_AGENTS_OPEN_COMMAND` is the one macOS-flavoured line: it pops the new session into a real terminal window so you can watch. Other terminals:
|
|
104
|
+
|
|
105
|
+
| Terminal | Command |
|
|
106
|
+
|---|---|
|
|
107
|
+
| Ghostty | `open -na Ghostty.app --args -e tmux attach -t {session}` |
|
|
108
|
+
| iTerm2 | `open -na iTerm.app --args tmux attach -t {session}` |
|
|
109
|
+
| kitty | `kitty tmux attach -t {session}` |
|
|
110
|
+
| WezTerm | `wezterm start -- tmux attach -t {session}` |
|
|
111
|
+
| Terminal.app | `osascript -e 'tell app "Terminal" to do script "tmux attach -t {session}"'` |
|
|
112
|
+
| Linux (any) | `x-terminal-emulator -e tmux attach -t {session}` |
|
|
113
|
+
|
|
114
|
+
Leave it unset and the session runs detached; `tmux attach -t <name>` from any terminal shows it.
|
|
115
|
+
|
|
116
|
+
## A full loop, as the model sees it
|
|
117
|
+
|
|
118
|
+
```jsonc
|
|
119
|
+
agents_launch({
|
|
120
|
+
"session": "agents-wp5",
|
|
121
|
+
"cwd": "/Users/me/repo",
|
|
122
|
+
"agents": [
|
|
123
|
+
{"title": "WP5 Review", "command": "claude --model claude-opus-5 \"$(cat prompts/wp5.md)\""},
|
|
124
|
+
{"title": "WP6a Lazy", "command": "claude --model claude-sonnet-5 \"$(cat prompts/wp6a.md)\""},
|
|
125
|
+
{"title": "WP6b Shell", "command": "codex --model gpt-5-codex \"$(cat prompts/wp6b.md)\""}
|
|
126
|
+
]
|
|
127
|
+
})
|
|
128
|
+
// → 3 tiled panes, Ghostty window opens
|
|
129
|
+
|
|
130
|
+
pane_wait({"pane": "WP6b", "timeout_seconds": 300, "idle_seconds": 8})
|
|
131
|
+
// → {"state": "idle", "tail": "... Overwrite tsconfig.json? (y/N)"}
|
|
132
|
+
|
|
133
|
+
pane_send({"pane": "WP6b", "text": "y"})
|
|
134
|
+
|
|
135
|
+
pane_wait({"pane": "WP5", "pattern": "handoff\\.md written"})
|
|
136
|
+
pane_read({"pane": "WP5", "lines": 80})
|
|
137
|
+
|
|
138
|
+
session_kill({"session": "agents-wp5"})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`pane_wait` is the heart of it. "Quiet for 8 seconds" is a surprisingly reliable definition of *an agent is either done or waiting for you*, and the tail tells the model which.
|
|
142
|
+
|
|
143
|
+
## Configuration
|
|
144
|
+
|
|
145
|
+
All via environment variables (set them in the `env` block of your MCP config):
|
|
146
|
+
|
|
147
|
+
| Variable | Default | Meaning |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `TMUX_AGENTS_SESSION_PREFIX` | *(empty = all)* | Only sessions whose name starts with this are visible or controllable. **Set it.** With `agents-` the model can never read or type into your personal tmux sessions. |
|
|
150
|
+
| `TMUX_AGENTS_OPEN_COMMAND` | *(none)* | Command run after `agents_launch`; `{session}` is substituted |
|
|
151
|
+
| `TMUX_AGENTS_ALLOW_KILL` | `true` | Set `false` to disable `pane_kill` / `session_kill` |
|
|
152
|
+
| `TMUX_AGENTS_REDACT` | `true` | Replace token-looking strings in pane text with `[redacted]` |
|
|
153
|
+
| `TMUX_AGENTS_MAX_LINES` | `2000` | Hard cap for `pane_read` |
|
|
154
|
+
| `TMUX_AGENTS_SOCKET` | *(default server)* | `tmux -L <socket>` — isolate the agents on their own tmux server |
|
|
155
|
+
| `TMUX_AGENTS_TMUX` | `tmux` | Path to the tmux binary |
|
|
156
|
+
|
|
157
|
+
## Security model, plainly
|
|
158
|
+
|
|
159
|
+
- `pane_send` types into a pane. If that pane is a shell, that is a shell. Keep agent sessions under a prefix (`TMUX_AGENTS_SESSION_PREFIX`) and the model cannot reach anything else.
|
|
160
|
+
- Pane text is scrubbed for GitHub/OpenAI/Slack/AWS tokens, bearer headers and `*_TOKEN=` style assignments before it is returned. This is a filter, not a guarantee — do not paste secrets into agent panes.
|
|
161
|
+
- The server instructs the model to treat everything it reads from panes as data written by another program, never as instructions. Agents talking to agents is exactly where prompt injection lives; the instruction is a reminder, not a defence. Review what your agents commit.
|
|
162
|
+
- No network, no config files, no state: the server is a thin layer over the `tmux` CLI and forgets everything between calls.
|
|
163
|
+
|
|
164
|
+
## How it compares
|
|
165
|
+
|
|
166
|
+
There are other tmux MCP servers ([nickgnd/tmux-mcp](https://github.com/nickgnd/tmux-mcp), [laszlopere/mcp-tmux](https://github.com/laszlopere/mcp-tmux)) and they are good general tmux remotes: create windows, run commands, read output, some over SSH. `tmux-agents` is narrower on purpose: it knows about *agents* — titled panes, "launch N of these in a layout that fits", "wait until it goes quiet", prefix-scoped visibility, redaction — and it deliberately leaves out a raw command tool. If you want a tmux remote control, use one of those. If you want an orchestrator, this is the one.
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
git clone https://github.com/woonyong-choi/tmux-agents
|
|
172
|
+
cd tmux-agents
|
|
173
|
+
uv sync --extra dev
|
|
174
|
+
uv run pytest # real tmux, private socket, ~20s
|
|
175
|
+
uv run ruff check src tests
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Tests spin up a throwaway tmux server (`tmux -L tmux-agents-test-…`), so they never touch your sessions.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# tmux-agents
|
|
2
|
+
|
|
3
|
+
**Let one AI agent run a room full of coding agents.**
|
|
4
|
+
|
|
5
|
+
`tmux-agents` is a Model Context Protocol server that turns tmux into a control surface for CLI agents. Point Claude, Cursor, Codex, Hermes or any MCP client at it and it can start Claude Code / Codex / Hermes / aider in split panes, watch each one, answer their questions, hand them the next task and tear the session down — while you watch the same panes in your own terminal.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
uvx tmux-agents
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That is the whole install. It needs `tmux` on the machine and nothing else.
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<code>
|
|
15
|
+
┌ WP1 Path ───────────────┬ WP2 Run ────────────────┐<br>
|
|
16
|
+
│ claude --model sonnet │ codex --model gpt-5 │<br>
|
|
17
|
+
│ ✓ 87 tests passed │ ? Overwrite main.ts? y/n│<br>
|
|
18
|
+
├ WP3 Timeline ───────────┼ WP4 Diagrams ───────────┤<br>
|
|
19
|
+
│ hermes chat -q "..." │ aider --yes │<br>
|
|
20
|
+
└─────────────────────────┴─────────────────────────┘
|
|
21
|
+
</code>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
Every coding agent ships as a terminal program, and every orchestrator wants to run several of them at once. The pieces that are missing are small but annoying:
|
|
27
|
+
|
|
28
|
+
- An agent hosted **somewhere else** (a cloud session, a desktop app, a phone client) has no terminal. It can plan the work but has to ask a human to type `run-batch.sh`.
|
|
29
|
+
- Four agents in four windows are four things to babysit. One stops to ask *"overwrite? (y/n)"* and everything waits until someone notices.
|
|
30
|
+
- Reading a pane, deciding it is done, and typing the follow-up is exactly the kind of loop a model is good at — if it can see the pane.
|
|
31
|
+
|
|
32
|
+
`tmux-agents` gives the orchestrating model seven tools that map to what you would do by hand, and nothing more. There is no generic `shell` tool: the model can type into agent panes, not into your machine.
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
| Tool | What it does |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `tmux_status` | Server config, tmux version, visible sessions |
|
|
39
|
+
| `agents_launch` | Create a session with one pane per agent, title each pane, run each command, pick a layout that fits (side-by-side for 2, tiled for 3+), optionally open it in your terminal app |
|
|
40
|
+
| `panes_list` | Panes with title, cwd, running command, size |
|
|
41
|
+
| `pane_read` | Last N lines of a pane, ANSI stripped, secrets redacted |
|
|
42
|
+
| `pane_wait` | Block until a pane is quiet for N seconds or a regex appears — returns `idle` / `matched` / `timeout` plus the tail |
|
|
43
|
+
| `pane_send` | Type text (literally, multi-line safe) and press Enter |
|
|
44
|
+
| `pane_key` | Send `C-c`, `Escape`, `Up`, `Tab` … |
|
|
45
|
+
| `pane_kill` / `session_kill` | Stop one agent or the whole batch (can be disabled) |
|
|
46
|
+
|
|
47
|
+
Panes are addressed by **title** (`"WP2"`, case-insensitive partial match), by id (`%3`) or by tmux target (`batch:0.1`). Titles are shown on the pane borders, so what the model calls a pane is what you see.
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
### Claude Code
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
claude mcp add tmux-agents -- uvx tmux-agents
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Claude Desktop / Cursor / Windsurf / Codex
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"tmux-agents": {
|
|
63
|
+
"command": "uvx",
|
|
64
|
+
"args": ["tmux-agents"],
|
|
65
|
+
"env": {
|
|
66
|
+
"TMUX_AGENTS_SESSION_PREFIX": "agents-",
|
|
67
|
+
"TMUX_AGENTS_OPEN_COMMAND": "open -na Ghostty.app --args -e tmux attach -t {session}"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`TMUX_AGENTS_OPEN_COMMAND` is the one macOS-flavoured line: it pops the new session into a real terminal window so you can watch. Other terminals:
|
|
75
|
+
|
|
76
|
+
| Terminal | Command |
|
|
77
|
+
|---|---|
|
|
78
|
+
| Ghostty | `open -na Ghostty.app --args -e tmux attach -t {session}` |
|
|
79
|
+
| iTerm2 | `open -na iTerm.app --args tmux attach -t {session}` |
|
|
80
|
+
| kitty | `kitty tmux attach -t {session}` |
|
|
81
|
+
| WezTerm | `wezterm start -- tmux attach -t {session}` |
|
|
82
|
+
| Terminal.app | `osascript -e 'tell app "Terminal" to do script "tmux attach -t {session}"'` |
|
|
83
|
+
| Linux (any) | `x-terminal-emulator -e tmux attach -t {session}` |
|
|
84
|
+
|
|
85
|
+
Leave it unset and the session runs detached; `tmux attach -t <name>` from any terminal shows it.
|
|
86
|
+
|
|
87
|
+
## A full loop, as the model sees it
|
|
88
|
+
|
|
89
|
+
```jsonc
|
|
90
|
+
agents_launch({
|
|
91
|
+
"session": "agents-wp5",
|
|
92
|
+
"cwd": "/Users/me/repo",
|
|
93
|
+
"agents": [
|
|
94
|
+
{"title": "WP5 Review", "command": "claude --model claude-opus-5 \"$(cat prompts/wp5.md)\""},
|
|
95
|
+
{"title": "WP6a Lazy", "command": "claude --model claude-sonnet-5 \"$(cat prompts/wp6a.md)\""},
|
|
96
|
+
{"title": "WP6b Shell", "command": "codex --model gpt-5-codex \"$(cat prompts/wp6b.md)\""}
|
|
97
|
+
]
|
|
98
|
+
})
|
|
99
|
+
// → 3 tiled panes, Ghostty window opens
|
|
100
|
+
|
|
101
|
+
pane_wait({"pane": "WP6b", "timeout_seconds": 300, "idle_seconds": 8})
|
|
102
|
+
// → {"state": "idle", "tail": "... Overwrite tsconfig.json? (y/N)"}
|
|
103
|
+
|
|
104
|
+
pane_send({"pane": "WP6b", "text": "y"})
|
|
105
|
+
|
|
106
|
+
pane_wait({"pane": "WP5", "pattern": "handoff\\.md written"})
|
|
107
|
+
pane_read({"pane": "WP5", "lines": 80})
|
|
108
|
+
|
|
109
|
+
session_kill({"session": "agents-wp5"})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`pane_wait` is the heart of it. "Quiet for 8 seconds" is a surprisingly reliable definition of *an agent is either done or waiting for you*, and the tail tells the model which.
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
All via environment variables (set them in the `env` block of your MCP config):
|
|
117
|
+
|
|
118
|
+
| Variable | Default | Meaning |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `TMUX_AGENTS_SESSION_PREFIX` | *(empty = all)* | Only sessions whose name starts with this are visible or controllable. **Set it.** With `agents-` the model can never read or type into your personal tmux sessions. |
|
|
121
|
+
| `TMUX_AGENTS_OPEN_COMMAND` | *(none)* | Command run after `agents_launch`; `{session}` is substituted |
|
|
122
|
+
| `TMUX_AGENTS_ALLOW_KILL` | `true` | Set `false` to disable `pane_kill` / `session_kill` |
|
|
123
|
+
| `TMUX_AGENTS_REDACT` | `true` | Replace token-looking strings in pane text with `[redacted]` |
|
|
124
|
+
| `TMUX_AGENTS_MAX_LINES` | `2000` | Hard cap for `pane_read` |
|
|
125
|
+
| `TMUX_AGENTS_SOCKET` | *(default server)* | `tmux -L <socket>` — isolate the agents on their own tmux server |
|
|
126
|
+
| `TMUX_AGENTS_TMUX` | `tmux` | Path to the tmux binary |
|
|
127
|
+
|
|
128
|
+
## Security model, plainly
|
|
129
|
+
|
|
130
|
+
- `pane_send` types into a pane. If that pane is a shell, that is a shell. Keep agent sessions under a prefix (`TMUX_AGENTS_SESSION_PREFIX`) and the model cannot reach anything else.
|
|
131
|
+
- Pane text is scrubbed for GitHub/OpenAI/Slack/AWS tokens, bearer headers and `*_TOKEN=` style assignments before it is returned. This is a filter, not a guarantee — do not paste secrets into agent panes.
|
|
132
|
+
- The server instructs the model to treat everything it reads from panes as data written by another program, never as instructions. Agents talking to agents is exactly where prompt injection lives; the instruction is a reminder, not a defence. Review what your agents commit.
|
|
133
|
+
- No network, no config files, no state: the server is a thin layer over the `tmux` CLI and forgets everything between calls.
|
|
134
|
+
|
|
135
|
+
## How it compares
|
|
136
|
+
|
|
137
|
+
There are other tmux MCP servers ([nickgnd/tmux-mcp](https://github.com/nickgnd/tmux-mcp), [laszlopere/mcp-tmux](https://github.com/laszlopere/mcp-tmux)) and they are good general tmux remotes: create windows, run commands, read output, some over SSH. `tmux-agents` is narrower on purpose: it knows about *agents* — titled panes, "launch N of these in a layout that fits", "wait until it goes quiet", prefix-scoped visibility, redaction — and it deliberately leaves out a raw command tool. If you want a tmux remote control, use one of those. If you want an orchestrator, this is the one.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
git clone https://github.com/woonyong-choi/tmux-agents
|
|
143
|
+
cd tmux-agents
|
|
144
|
+
uv sync --extra dev
|
|
145
|
+
uv run pytest # real tmux, private socket, ~20s
|
|
146
|
+
uv run ruff check src tests
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Tests spin up a throwaway tmux server (`tmux -L tmux-agents-test-…`), so they never touch your sessions.
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
`tmux-agents` lets an MCP client type into tmux panes. Read the "Security model, plainly"
|
|
4
|
+
section of the README before enabling it, and always set `TMUX_AGENTS_SESSION_PREFIX`.
|
|
5
|
+
|
|
6
|
+
To report a vulnerability, open a private security advisory on GitHub
|
|
7
|
+
(Security → Report a vulnerability) rather than a public issue.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Example: a batch of three agents, steered by one
|
|
2
|
+
|
|
3
|
+
A prompt you can give the orchestrating model once `tmux-agents` is connected:
|
|
4
|
+
|
|
5
|
+
> Launch session `agents-wp5` in `/path/to/repo` with three agents:
|
|
6
|
+
> "WP5 Review" → `claude --model claude-opus-5 "$(cat prompts/wp5.md)"`,
|
|
7
|
+
> "WP6a Lazy" → `claude --model claude-sonnet-5 "$(cat prompts/wp6a.md)"`,
|
|
8
|
+
> "WP6b Shell" → `codex --model gpt-5-codex "$(cat prompts/wp6b.md)"`.
|
|
9
|
+
> Then loop: `pane_wait` each pane (idle 8s, timeout 240s). If the tail ends in a
|
|
10
|
+
> question, answer it with `pane_send` following the rules in `docs/plan.md`. If it
|
|
11
|
+
> ends with "handoff.md written", read the last 80 lines and summarise. When all
|
|
12
|
+
> three are done, `session_kill`.
|
|
13
|
+
|
|
14
|
+
Everything the model reads from the panes is another agent's output; it should
|
|
15
|
+
verify claims (`npm test`, `git status`) rather than trust "done".
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tmux-agents"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server that lets any AI agent launch, watch and steer coding agents (Claude Code, Codex, Hermes, aider, ...) running in tmux panes."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Woonyong Choi" }]
|
|
9
|
+
keywords = [
|
|
10
|
+
"mcp", "model-context-protocol", "tmux", "agents", "claude-code", "codex",
|
|
11
|
+
"hermes-agent", "aider", "orchestration", "terminal", "automation",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: MacOS",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: Terminals",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"mcp>=1.2.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/woonyong-choi/tmux-agents"
|
|
32
|
+
Issues = "https://github.com/woonyong-choi/tmux-agents/issues"
|
|
33
|
+
Changelog = "https://github.com/woonyong-choi/tmux-agents/blob/main/CHANGELOG.md"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
tmux-agents = "tmux_agents.__main__:main"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/tmux_agents"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = "py310"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
pythonpath = ["src"]
|
|
57
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""`tmux-agents` entry point: run the MCP server over stdio."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
if "--version" in sys.argv[1:]:
|
|
10
|
+
from . import __version__
|
|
11
|
+
|
|
12
|
+
print(f"tmux-agents {__version__}")
|
|
13
|
+
return
|
|
14
|
+
from .server import mcp
|
|
15
|
+
|
|
16
|
+
mcp.run()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Scrub secrets out of pane text before it reaches the model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
_PATTERNS = [
|
|
8
|
+
# GitHub, OpenAI, Anthropic, Slack, AWS, generic bearer/API keys
|
|
9
|
+
re.compile(r"\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b"),
|
|
10
|
+
re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b"),
|
|
11
|
+
re.compile(r"\bsk-[A-Za-z0-9_-]{16,}\b"),
|
|
12
|
+
re.compile(r"\bxox[abprs]-[A-Za-z0-9-]{10,}\b"),
|
|
13
|
+
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
|
14
|
+
re.compile(r"(?i)\b(bearer)\s+[A-Za-z0-9._~+/=-]{16,}"),
|
|
15
|
+
re.compile(
|
|
16
|
+
r"(?i)\b([A-Z0-9_]*(?:token|secret|api[_-]?key|password|passwd))"
|
|
17
|
+
r"\s*[=:]\s*['\"]?[^\s'\"]{8,}"
|
|
18
|
+
),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
_ANSI = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07]*\x07|\x1b[()][A-Za-z0-9]")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def redact(text: str) -> str:
|
|
25
|
+
for pattern in _PATTERNS:
|
|
26
|
+
text = pattern.sub(_replace, text)
|
|
27
|
+
return text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _replace(match: re.Match[str]) -> str:
|
|
31
|
+
groups = match.groups()
|
|
32
|
+
if groups and groups[0]:
|
|
33
|
+
return (
|
|
34
|
+
f"{groups[0]}=[redacted]"
|
|
35
|
+
if "=" in match.group(0) or ":" in match.group(0)
|
|
36
|
+
else f"{groups[0]} [redacted]"
|
|
37
|
+
)
|
|
38
|
+
return "[redacted]"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def strip_ansi(text: str) -> str:
|
|
42
|
+
return _ANSI.sub("", text)
|