Add tmux shim for native agent teams support#1102
Add tmux shim for native agent teams support#1102adisinghstudent wants to merge 4 commits intomanaflow-ai:mainfrom
Conversation
Claude Code agent teams detect tmux via the TMUX env var, then use tmux split-window/send-keys to spawn teammates in split panes. This adds two pieces to make that work natively in cmux: 1. Resources/bin/tmux — shim script that translates tmux commands to cmux equivalents (split-window → new-split, send-keys → send, capture-pane → capture-pane, kill-pane → close-surface, etc.) with a pane ID mapping layer (%N → surface:N). 2. GhosttyTerminalView.swift — exports TMUX env var in every cmux shell so Claude Code detects split-pane mode and routes through the shim (which is first on PATH via Resources/bin/). Resolves manaflow-ai#1080 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@adisinghstudent is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a Bash tmux-compatible shim at Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client / tmux-aware tool
participant Shim as Tmux Shim\n(Resources/bin/tmux)
participant Cmux as cmux CLI\n(socket)
participant Surface as Cmux Surface
Client->>Shim: issue tmux command
Shim->>Shim: resolve target / seq-id / store mapping
Shim->>Cmux: invoke cmux (via socket)
Cmux->>Surface: operate on surface (create/send/inspect)
Surface-->>Cmux: respond (ref / ack / data)
Cmux-->>Shim: return result
Shim-->>Client: emit tmux-compatible output
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Greptile SummaryThis PR adds a Several issues need to be addressed before merging:
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Agent as AI Agent
participant Shim as Resources/bin/tmux (shim)
participant Map as PANE_MAP file
participant Cmux as cmux CLI
Note over Agent: Detects TMUX env var set by GhosttyTerminalView.swift
Agent->>Shim: tmux split-window -h -c /path
Shim->>Cmux: cmux --json new-split right
Cmux-->>Shim: {"surface_ref": "surface:42"}
Shim->>Map: store %1 → surface:42
Shim-->>Agent: %1
Agent->>Shim: tmux send-keys -t %1 "ls" Enter
Shim->>Map: resolve_pane(%1)
Map-->>Shim: surface:42
Shim->>Cmux: cmux send --surface surface:42 "ls\n"
Agent->>Shim: tmux capture-pane -t %1 -p
Shim->>Map: resolve_pane(%1)
Map-->>Shim: surface:42
Shim->>Cmux: cmux capture-pane --surface surface:42
Cmux-->>Agent: pane content
Agent->>Shim: tmux kill-pane -t %1
Shim->>Map: resolve_pane(%1)
Map-->>Shim: surface:42
Shim->>Cmux: cmux close-surface --surface surface:42
Last reviewed commit: df2bc8c |
Resources/bin/tmux
Outdated
| echo "$panes" | python3 -c " | ||
| import sys, json | ||
| fmt = '''$format''' | ||
| try: | ||
| data = json.load(sys.stdin) | ||
| except: | ||
| sys.exit(0) | ||
| # Output basic pane info | ||
| panes = data if isinstance(data, list) else [] | ||
| for i, p in enumerate(panes): | ||
| line = fmt | ||
| pane_id = '%' + str(i) | ||
| line = line.replace('#{pane_id}', pane_id) | ||
| line = line.replace('#{pane_index}', str(i)) | ||
| line = line.replace('#{pane_width}', str(p.get('width', 80))) | ||
| line = line.replace('#{pane_height}', str(p.get('height', 24))) | ||
| line = line.replace('#{pane_active}', '1' if p.get('focused', False) else '0') | ||
| line = line.replace('#{pane_current_command}', p.get('command', 'bash')) | ||
| line = line.replace('#{pane_pid}', str(p.get('pid', 0))) | ||
| print(line) | ||
| " 2>/dev/null |
There was a problem hiding this comment.
Python code injection via unescaped $format
The $format variable is interpolated verbatim into a Python source string delimited by triple-quotes ('''...'). An agent calling tmux list-panes -F with a format string containing ''' will terminate the Python string early, and everything after that point will be parsed as executable Python code.
For example:
tmux list-panes -F "'''; import os; os.system('malicious'); '''"
would break out of the fmt = '''...''' assignment and execute arbitrary Python in the context of the shim's process.
Use sys.argv or pass the format string as a JSON-encoded environment variable instead of string-interpolating it into Python source:
echo "$panes" | TMUX_FORMAT="$format" python3 -c "
import sys, json, os
fmt = os.environ.get('TMUX_FORMAT', '')
try:
data = json.load(sys.stdin)
except:
sys.exit(0)
panes = data if isinstance(data, list) else []
for i, p in enumerate(panes):
line = fmt
pane_id = '%' + str(i)
line = line.replace('#{pane_id}', pane_id)
line = line.replace('#{pane_index}', str(i))
line = line.replace('#{pane_width}', str(p.get('width', 80)))
line = line.replace('#{pane_height}', str(p.get('height', 24)))
line = line.replace('#{pane_active}', '1' if p.get('focused', False) else '0')
line = line.replace('#{pane_current_command}', p.get('command', 'bash'))
line = line.replace('#{pane_pid}', str(p.get('pid', 0)))
print(line)
" 2>/dev/null
Resources/bin/tmux
Outdated
| result=$(cmux_cmd --json new-workspace 2>/dev/null) | ||
| # After creating workspace, get the new surface | ||
| sleep 0.3 | ||
|
|
||
| if [[ -n "$cwd" ]]; then | ||
| cmux_cmd send "cd $(printf '%q' "$cwd")"$'\n' 2>/dev/null | ||
| sleep 0.2 | ||
| fi | ||
| if [[ -n "$shell_cmd" ]]; then | ||
| cmux_cmd send "${shell_cmd}"$'\n' 2>/dev/null | ||
| fi | ||
|
|
||
| tmux_id=$(next_pane_id) | ||
| echo "$tmux_id" |
There was a problem hiding this comment.
new-window never stores the pane mapping
result captures the JSON output from cmux --json new-workspace, but surface_ref is never extracted from it, and store_pane is never called. The returned $tmux_id is allocated but not registered in PANE_MAP, so any subsequent send-keys -t, capture-pane -t, kill-pane -t, etc. targeting that ID will fail to find a mapping in resolve_pane and will pass the raw %N string as a surface reference to cmux (which will error silently).
Compare to split-window, which correctly does:
surface_ref=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('surface_ref',''))" 2>/dev/null || echo "")
# …
store_pane "$tmux_id" "$surface_ref"The same extraction and store_pane call is needed here. Also, the send commands in new-window (lines 333/337) are missing --surface "$surface_ref", so they may target the wrong pane even once the mapping is fixed.
Resources/bin/tmux
Outdated
| # Allocate a tmux-style %N pane ID. | ||
| next_pane_id() { | ||
| local counter_file="${PANE_MAP}.counter" | ||
| local n=0 | ||
| if [[ -f "$counter_file" ]]; then | ||
| n=$(cat "$counter_file" 2>/dev/null || echo 0) | ||
| fi | ||
| n=$((n + 1)) | ||
| echo "$n" > "$counter_file" | ||
| echo "%${n}" | ||
| } |
There was a problem hiding this comment.
Race condition in next_pane_id counter
The read → increment → write sequence on the counter file is not atomic. Two concurrent shim invocations (e.g. an agent spawning several splits rapidly) can both read the same n, both increment to the same value, and both write it back — producing duplicate %N IDs. A duplicate ID causes resolve_pane to tail -1 the last entry for that ID, silently redirecting one pane's commands to another surface.
A simple mitigation is to use flock for mutual exclusion:
next_pane_id() {
local counter_file="${PANE_MAP}.counter"
local n
n=$(flock -x "$counter_file" bash -c '
n=$(cat "$1" 2>/dev/null) || n=0
n=$((n + 1))
echo "$n" > "$1"
echo "$n"
' _ "$counter_file")
echo "%${n}"
}
Resources/bin/tmux
Outdated
| SOCKET="${CMUX_SOCKET_PATH:-/tmp/cmux.sock}" | ||
|
|
||
| # Pane target mapping file — maps tmux %N pane IDs to cmux surface refs. | ||
| PANE_MAP_DIR="${TMPDIR:-/tmp}/cmux-tmux-shim-$$" |
There was a problem hiding this comment.
PANE_MAP_DIR defined but never used
PANE_MAP_DIR is assigned on this line but referenced nowhere else in the script. It appears to be a leftover from an earlier design (the $$-suffixed per-process directory approach). Consider removing it to avoid confusion.
| PANE_MAP_DIR="${TMPDIR:-/tmp}/cmux-tmux-shim-$$" |
(Remove the line entirely.)
Sources/GhosttyTerminalView.swift
Outdated
| // Expose TMUX env var so tools that detect tmux (e.g. Claude Code agent | ||
| // teams split-pane mode) route through the cmux tmux shim at Resources/bin/tmux. | ||
| // Format mirrors real tmux: <socket-path>,<pid>,<window-index> | ||
| env["TMUX"] = "\(socketPath),\(ProcessInfo.processInfo.processIdentifier),0" |
There was a problem hiding this comment.
All surfaces share the same TMUX value
Every TerminalSurface receives the exact same TMUX string (socketPath + "," + appPID + ",0"), because ProcessInfo.processInfo.processIdentifier is the cmux app's PID (constant) and the window index is hardcoded to 0.
Real tmux sets TMUX to a per-client value that encodes which session the pane belongs to. When an agent calls tmux display-message -p '#{pane_id}' to discover its own pane, the shim returns %0 unconditionally (line 255). If two agent panes try to coordinate using their respective TMUX values to discover each other, they will both see the same TMUX string and the same pane_id, leading to one agent's commands silently targeting the wrong split.
Consider incorporating the surface UUID into the TMUX value (e.g. as the window index field, or an extra field) so that each surface has a distinct identifier agents can use to determine which pane they are running in.
There was a problem hiding this comment.
No issues found across 2 files
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
Resources/bin/tmux (4)
33-40: Potential regex metacharacter injection inresolve_pane.If
$targetcontains regex metacharacters (e.g.,.,*,+), the grep pattern^${target}=may match unintended lines. Since pane IDs are%Nformat, this is low risk in practice, but defensive quoting would be safer.🛡️ Proposed fix using grep -F for literal matching
resolve_pane() { local target="$1" - if [[ -f "$PANE_MAP" ]] && grep -q "^${target}=" "$PANE_MAP" 2>/dev/null; then - grep "^${target}=" "$PANE_MAP" | tail -1 | cut -d= -f2 + if [[ -f "$PANE_MAP" ]] && grep -Fq "${target}=" "$PANE_MAP" 2>/dev/null; then + grep -F "${target}=" "$PANE_MAP" | grep "^${target}=" | tail -1 | cut -d= -f2 else echo "$target" fi }Alternatively, use
awkfor exact matching:resolve_pane() { local target="$1" if [[ -f "$PANE_MAP" ]]; then awk -F= -v t="$target" '$1 == t { v=$2 } END { print (v ? v : t) }' "$PANE_MAP" else echo "$target" fi }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Resources/bin/tmux` around lines 33 - 40, The resolve_pane function uses grep with an unquoted regex pattern (^${target}=) which can misbehave if target contains regex metacharacters; update resolve_pane to perform a literal key match instead (e.g., use grep -F -x pattern or switch to awk with -F= and compare $1 == t) so that the lookup uses exact matching against PANE_MAP and falls back to echoing the original target when no match is found; reference the resolve_pane function and the pattern "^${target}=" in your change.
291-291: Unused variablescrollback.The
scrollbackvariable is defined but never used. Consider removing it or implementing scrollback support if cmux supports it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Resources/bin/tmux` at line 291, The config sets an unused variable "scrollback" (seen alongside "target" and "print_mode") which should be removed or wired up to the terminal handling code; either delete the "scrollback=false" token from the tmux invocation/config to remove the unused variable, or implement scrollback support by passing its value into the cmux/terminal setup logic (where target/print_mode are handled) and using it to enable/disable scrollback behavior.
24-24: Unused variablePANE_MAP_DIR.This variable is defined but never referenced anywhere in the script. Consider removing it or using it for its apparent intended purpose.
🧹 Proposed fix
-PANE_MAP_DIR="${TMPDIR:-/tmp}/cmux-tmux-shim-$$"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Resources/bin/tmux` at line 24, The variable PANE_MAP_DIR is declared but never used; either remove the declaration or implement its intended use: initialize and export PANE_MAP_DIR (currently set to "${TMPDIR:-/tmp}/cmux-tmux-shim-$$"), create the directory (mkdir -p), use it where pane mapping files are written/read, and add cleanup (trap) logic to remove it on exit; if you choose to remove it, simply delete the PANE_MAP_DIR="${TMPDIR:-/tmp}/cmux-tmux-shim-$$" line and any related dead references.
106-114: Sleep-based synchronization is fragile.The hardcoded
sleep 0.3andsleep 0.2delays assume the split is ready within that time. On slow systems or under load, the surface may not be ready, causingcdor command execution to fail silently.Consider whether cmux provides a synchronous mode or a ready-signal that could replace these sleeps, or document this as a known limitation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Resources/bin/tmux` around lines 106 - 114, The hardcoded sleeps before sending to the new surface (cmux_cmd send --surface "$surface_ref" ...) are fragile; replace them with a readiness check loop that polls the surface state (e.g., call a cmux readiness/status command or probe with a harmless no-op send) until ready or a short timeout, then send the cd and shell_cmd payloads; if cmux has no readiness API, implement a retry-with-backoff loop around cmux_cmd send for the cd and the shell_cmd with a clear timeout and error logging, and if neither approach is possible, add a comment documenting this limitation. Ensure you update the code paths that reference surface_ref, the cd send, and the shell_cmd send to use the new polling/retry logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Resources/bin/tmux`:
- Around line 181-201: The embedded Python snippet interpolates the shell
variable $format directly into the triple-quoted string (assigned to fmt),
allowing a malicious format containing ''' to break out and inject code; change
the invocation so the shell passes the format safely (e.g., export FORMAT or
pass it on stdin) and have the Python snippet read it from os.environ['FORMAT']
or from a separate JSON/stdin field instead of embedding '''$format''', update
references to fmt in the Python block to use the environment/input value, and
ensure you keep existing variables panes/data handling and exit behavior intact.
- Around line 50-59: The next_pane_id function has a race on reading/updating
"${PANE_MAP}.counter"; replace the naive read-increment-write with an exclusive
file lock around the whole sequence so concurrent shim invocations cannot
allocate the same ID. Use flock (or bash's exec + flock FD) to obtain an
exclusive lock on the counter file (create it if missing), then read the value,
increment, write it back, and release the lock; keep the function name
next_pane_id and the counter filename "${PANE_MAP}.counter" so callers remain
unchanged.
- Around line 124-130: The script parses -l into the variable literal but never
uses it; update the key translation/processing block (the code that currently
translates named keys like Enter/Space into tmux key codes) to check the literal
flag and skip translation when literal=true — i.e., if literal is true,
append/emit the provided key strings verbatim (without mapping) into the keys
array/argument construction, otherwise run the existing translation logic;
reference the existing variable literal and the key translation loop/logic to
locate where to add the conditional.
- Around line 328-338: The sends after creating a workspace are missing the
--surface flag so they may target the wrong surface; capture the new surface
reference from result (the output of cmux_cmd --json new-workspace stored in
result), extract the surface id/ref the same way split-window uses surface_ref,
and pass that ref to subsequent cmux_cmd send calls for both the cwd and
shell_cmd branches (i.e., use --surface "$surface_ref" when calling cmux_cmd
send after new-workspace) so commands go to the newly created workspace.
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2393-2399: The TMUX shim environment keys are being set before
merging in additionalEnvironment so callers can override/disable the shim; move
the assignments that set env["CMUX_SOCKET_PATH"], env["TMUX"] (built from
SocketControlSettings.socketPath() and
ProcessInfo.processInfo.processIdentifier) and the PATH prepend so they occur
after merging additionalEnvironment, or ensure the merge preserves these
internal values (e.g., merge with a policy that keeps existing env values);
apply the same change for the other occurrences around the code that references
these keys (the blocks around lines handling PATH and the TMUX/CMUX_SOCKET_PATH
assignments).
---
Nitpick comments:
In `@Resources/bin/tmux`:
- Around line 33-40: The resolve_pane function uses grep with an unquoted regex
pattern (^${target}=) which can misbehave if target contains regex
metacharacters; update resolve_pane to perform a literal key match instead
(e.g., use grep -F -x pattern or switch to awk with -F= and compare $1 == t) so
that the lookup uses exact matching against PANE_MAP and falls back to echoing
the original target when no match is found; reference the resolve_pane function
and the pattern "^${target}=" in your change.
- Line 291: The config sets an unused variable "scrollback" (seen alongside
"target" and "print_mode") which should be removed or wired up to the terminal
handling code; either delete the "scrollback=false" token from the tmux
invocation/config to remove the unused variable, or implement scrollback support
by passing its value into the cmux/terminal setup logic (where target/print_mode
are handled) and using it to enable/disable scrollback behavior.
- Line 24: The variable PANE_MAP_DIR is declared but never used; either remove
the declaration or implement its intended use: initialize and export
PANE_MAP_DIR (currently set to "${TMPDIR:-/tmp}/cmux-tmux-shim-$$"), create the
directory (mkdir -p), use it where pane mapping files are written/read, and add
cleanup (trap) logic to remove it on exit; if you choose to remove it, simply
delete the PANE_MAP_DIR="${TMPDIR:-/tmp}/cmux-tmux-shim-$$" line and any related
dead references.
- Around line 106-114: The hardcoded sleeps before sending to the new surface
(cmux_cmd send --surface "$surface_ref" ...) are fragile; replace them with a
readiness check loop that polls the surface state (e.g., call a cmux
readiness/status command or probe with a harmless no-op send) until ready or a
short timeout, then send the cd and shell_cmd payloads; if cmux has no readiness
API, implement a retry-with-backoff loop around cmux_cmd send for the cd and the
shell_cmd with a clear timeout and error logging, and if neither approach is
possible, add a comment documenting this limitation. Ensure you update the code
paths that reference surface_ref, the cd send, and the shell_cmd send to use the
new polling/retry logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 90042a16-003b-41e5-8b95-bb62eaee0d69
📒 Files selected for processing (2)
Resources/bin/tmuxSources/GhosttyTerminalView.swift
- Fix Python code injection: pass format string via env var instead of triple-quote interpolation - Fix new-window: extract surface_ref and store pane mapping - Fix race condition: use flock for atomic pane ID allocation - Fix send-keys: honor -l literal flag with early continue - Fix resolve_pane: use grep -F for literal matching (no regex metachar issues) - Fix display-message: use CMUX_SURFACE_ID for pane_id query - Remove unused PANE_MAP_DIR variable - Replace bare except with specific exception types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use surface UUID prefix in TMUX value so each surface gets a unique identifier (e.g. /tmp/cmux.sock,1234,A1B2C3D4 instead of ...,0) - Move TMUX and PATH assignments after additionalEnvironment merge so they cannot be accidentally overridden by caller-supplied env vars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
Resources/bin/tmux (3)
74-82: Unused variable:pane_formatis parsed but never used.The
-Fflag is accepted and stored but the format string is never applied to the output. The shim always prints just the pane ID regardless of the format requested.♻️ Suggested cleanup
Either remove the variable if format support isn't planned:
direction="right" cwd="" shell_cmd="" - pane_format="" while [[ $# -gt 0 ]]; do case "$1" in -h) direction="right"; shift ;; -v) direction="down"; shift ;; -c) cwd="$2"; shift 2 ;; -P) shift ;; # print pane info (we always do) - -F) pane_format="$2"; shift 2 ;; # format string + -F) shift 2 ;; # format string (ignored, always print pane ID)Or implement basic format support in a follow-up.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Resources/bin/tmux` around lines 74 - 82, The script parses a -F flag into the variable pane_format but never uses it; update the code handling the -F flag (the while [[ $# -gt 0 ]] loop and subsequent output path that prints the pane ID) to either remove the -F parsing if format support is not intended, or implement basic format support by applying pane_format when printing pane info (use the existing pane id variable and substitute it into the user-supplied format string before output). Ensure you reference and update the pane_format variable and the output/print logic so the -F option actually affects the script's output.
293-302: Unused variable:scrollbackis assigned but never read.The variable is set to
falsebut never checked. The-Sand-Eflags (scrollback range) are already handled by shifting past them.♻️ Suggested cleanup
capture-pane) - target="" print_mode=false scrollback=false + target="" print_mode=false while [[ $# -gt 0 ]]; do🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Resources/bin/tmux` around lines 293 - 302, Remove the unused variable assignment by deleting the declaration "scrollback=false" and any related unused references to "scrollback" in this script (the while/case that handles flags including "-S" and "-E" already shifts past those args), so only actually-used flags like "target" and "print_mode" remain; ensure no other code later expects a "scrollback" variable before committing.
316-321: Unused variable:pane_formatis parsed but never used.Same issue as
split-window- the-Fformat flag is captured but the output always just prints the pane ID.♻️ Suggested cleanup
new-window) - cwd="" shell_cmd="" pane_format="" + cwd="" shell_cmd="" while [[ $# -gt 0 ]]; do case "$1" in -c) cwd="$2"; shift 2 ;; -P) shift ;; - -F) pane_format="$2"; shift 2 ;; + -F) shift 2 ;; # format string (ignored)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Resources/bin/tmux` around lines 316 - 321, The loop currently parses the -F flag into the pane_format variable but never uses it, so either remove the -F case from the argument parsing or apply pane_format when producing output; update the while/case that sets pane_format to either drop handling of -F (and remove the pane_format variable) or pass pane_format into the code path that prints the result (the same place that currently prints the pane ID) so the final output respects the requested format; reference the pane_format variable and the argument-parsing while/case in the tmux script when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Resources/bin/tmux`:
- Around line 193-203: The loop is fabricating pane IDs ("%0", "%1", ...) that
don't match the real IDs stored in PANE_MAP (and assigned by next_pane_id() when
split-window is called), causing resolve_pane to fail; update the replacement
for '#{pane_id}' inside the loop to use the actual pane identifier (e.g.,
p.get('id') or a reverse lookup into PANE_MAP to map the pane's stored internal
id/index to the real tmux id assigned at creation) instead of '%' + str(i), and
keep other replacements the same so printed pane lines use the real IDs that
resolve_pane and split-window return.
---
Nitpick comments:
In `@Resources/bin/tmux`:
- Around line 74-82: The script parses a -F flag into the variable pane_format
but never uses it; update the code handling the -F flag (the while [[ $# -gt 0
]] loop and subsequent output path that prints the pane ID) to either remove the
-F parsing if format support is not intended, or implement basic format support
by applying pane_format when printing pane info (use the existing pane id
variable and substitute it into the user-supplied format string before output).
Ensure you reference and update the pane_format variable and the output/print
logic so the -F option actually affects the script's output.
- Around line 293-302: Remove the unused variable assignment by deleting the
declaration "scrollback=false" and any related unused references to "scrollback"
in this script (the while/case that handles flags including "-S" and "-E"
already shifts past those args), so only actually-used flags like "target" and
"print_mode" remain; ensure no other code later expects a "scrollback" variable
before committing.
- Around line 316-321: The loop currently parses the -F flag into the
pane_format variable but never uses it, so either remove the -F case from the
argument parsing or apply pane_format when producing output; update the
while/case that sets pane_format to either drop handling of -F (and remove the
pane_format variable) or pass pane_format into the code path that prints the
result (the same place that currently prints the pane ID) so the final output
respects the requested format; reference the pane_format variable and the
argument-parsing while/case in the tmux script when making this change.
list-panes was generating synthetic %0,%1,%2 IDs that didn't match the counter-based IDs returned by split-window. Now does a reverse lookup from the pane map (cmux surface ref → tmux %N ID) so agents can use IDs from list-panes with send-keys/kill-pane. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Resources/bin/tmux`:
- Around line 268-275: The current print_mode branch short-circuits token
matches in the case on "$message" and returns single canned values, which breaks
combined formats and returns the raw %${CMUX_SURFACE_ID:-0} instead of the
mapped pane id; change the logic in the print_mode handling to perform format
substitution on "$message" (e.g. replace occurrences of "#{pane_id}",
"#{session_name}", "#{window_index}", "#{pane_index}" rather than matching the
whole string) by doing ordered string replacements: lookup PANE_MAP for the
actual shim pane id and substitute it for "#{pane_id}" (falling back to
%${CMUX_SURFACE_ID:-0} only if no mapping), substitute "cmux" for
"#{session_name}", "0" for "#{window_index}" and "#{pane_index}", and then echo
the fully substituted string so combined formats like
"#{session_name}:#{window_index}.#{pane_index}" are preserved.
- Around line 283-301: The current option parsing for resize-pane ignores -x/-y
but leaves direction and amount set (defaulting to -R and 5), causing unintended
relative resizes; update the parsing logic that handles -x and -y to mark an
"absolute_size" error (e.g., set a flag like absolute_size=true) instead of
shifting and continuing, and then before calling resolve_pane/ cmux_cmd
resize-pane check that flag and bail out with a clear error message and non-zero
exit (or return) indicating absolute sizing is unsupported; ensure you reference
the same symbols in the change (the case branches for -x and -y, the variables
target/direction/amount, resolve_pane and the cmux_cmd resize-pane invocation)
so the code never falls through to perform a relative resize when -x/-y were
provided.
- Around line 185-217: The environment variables TMUX_SHIM_FORMAT and
TMUX_SHIM_PANE_MAP are currently being set for echo instead of the python3
process, so fmt and pane_map_path are empty; fix this by ensuring those vars are
exported/placed in the environment of the python3 invocation (so python3 sees
TMUX_SHIM_FORMAT and TMUX_SHIM_PANE_MAP and can populate fmt and pane_map_path).
Locate the shell line that runs python3 (the invocation that pipes "$panes" into
python3 -c "...") and move the assignments so they apply to the python3 process
(or use env/export before calling python3), ensuring the python code using fmt
and pane_map_path receives the correct values.
- Around line 94-95: The shim currently calls python3 to parse JSON (e.g., the
surface_ref extraction using: surface_ref=$(echo "$result" | python3 -c "import
sys,json; print(json.load(sys.stdin).get('surface_ref',''))" ...)), creating a
hard runtime dependency that fails silently; add an explicit runtime check for a
JSON parser at startup (e.g., test -x "$(command -v python3)" or "$(command -v
jq)"), and if none is found print a clear error message about the missing
dependency and exit, or refactor the three parsing sites (split-window,
list-panes format handling, new-window) to use jq when available (fall back to
python3 only if present) so the code no longer assumes python3 is on PATH and
users get actionable errors.
| surface_ref=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('surface_ref',''))" 2>/dev/null || echo "") | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "python3 => $(command -v python3 || echo MISSING)"
sw_vers
# Run this on the same clean macOS image / DMG install target as the PR.
# If python3 is missing, split-window/new-window/list-panes will fail here.Repository: manaflow-ai/cmux
Length of output: 152
🏁 Script executed:
# First, let's look at the actual file to understand the python3 usage
fd -t f "tmux" Resources/bin/ 2>/dev/null || find Resources/bin -name "tmux" -type fRepository: manaflow-ai/cmux
Length of output: 78
🏁 Script executed:
# Check project structure and distribution method
ls -la | head -20
cat README.md 2>/dev/null | head -50
cat setup.py 2>/dev/null || cat pyproject.toml 2>/dev/null || cat package.json 2>/dev/nullRepository: manaflow-ai/cmux
Length of output: 3550
🏁 Script executed:
# Look for distribution/packaging configuration
fd -t f "(dmg|app|setup|pyproject|package)" . --max-depth 2 2>/dev/null | grep -v node_modulesRepository: manaflow-ai/cmux
Length of output: 680
🏁 Script executed:
# Read the entire tmux file to understand the python3 usage
cat -n Resources/bin/tmux | head -150Repository: manaflow-ai/cmux
Length of output: 5471
🏁 Script executed:
# Look at the specific lines mentioned: 94-95, 185-217, 344-345
cat -n Resources/bin/tmux | sed -n '90,100p'
cat -n Resources/bin/tmux | sed -n '180,220p'
cat -n Resources/bin/tmux | sed -n '340,350p'Repository: manaflow-ai/cmux
Length of output: 2876
🏁 Script executed:
# Check how this shim is invoked - look for wrapper/setup scripts
rg -l "Resources/bin/tmux" . --type sh
cat Resources/bin/tmux | wc -lRepository: manaflow-ai/cmux
Length of output: 63
🏁 Script executed:
# Check if there are error handling paths or fallbacks for the python3 invocations
cat -n Resources/bin/tmux | sed -n '90,120p'
cat -n Resources/bin/tmux | sed -n '340,365p'
# Look for how the JSON responses are actually structured
rg "new-split|new-workspace|list-panes" Resources/bin/tmux -A 3 -B 1Repository: manaflow-ai/cmux
Length of output: 3465
🏁 Script executed:
# Check macOS specifics - look for bundled python or requirement declarations
fd -t f "requirements\|setup\|build\|script" . --max-depth 3 2>/dev/null | head -20
find . -name "*.sh" -path "*/scripts/*" -exec grep -l "python\|interpreter" {} \;Repository: manaflow-ai/cmux
Length of output: 176
🏁 Script executed:
# Verify actual error behavior - trace what happens when python3 parsing fails
cat -n Resources/bin/tmux | sed -n '93,99p'Repository: manaflow-ai/cmux
Length of output: 414
🏁 Script executed:
# Check Python3 availability claim - look at macOS-specific docs or setup scripts
cat scripts/smoke-test-ci.sh | head -50
cat scripts/run-tests-v1.sh | head -30
# Check if there's any python bundling or interpreter management
rg "python" Resources/ scripts/ --type sh -A 2 -B 2 | head -60Repository: manaflow-ai/cmux
Length of output: 4756
🏁 Script executed:
# Verify what the cmux --json outputs actually look like
# Check if there are integration tests showing expected JSON format
find . -name "*.py" -type f -exec grep -l "surface_ref\|new-split\|new-workspace" {} \;Repository: manaflow-ai/cmux
Length of output: 559
🏁 Script executed:
# Check if macOS ships with python3 by default - search for any docs/discussions
rg -i "python|system.python|brew" README.md CONTRIBUTING.md docs/ 2>/dev/null || echo "No references found"
# Look at actual cmux setup/requirements to see if python3 is expected
cat package.json | jq .
cat Package.swift 2>/dev/null | head -30 || echo "No Package.swift"Repository: manaflow-ai/cmux
Length of output: 1538
🏁 Script executed:
# Verify the actual behavior: does the shim gracefully handle missing python3?
# Look at what happens when python3 fails in line 94
cat -n Resources/bin/tmux | sed -n '92,100p'
# The pattern: surface_ref=$(... python3 ... 2>/dev/null || echo "")
# If python3 is missing: 'command not found' on stderr, then || echo "" runs
# Result: surface_ref=""
# Then: if [[ -z "$surface_ref" ]] → exit 1
# Check: is there any way the shim guarantees python3 exists?
rg "python|PATH|interpreter" Resources/bin/tmuxRepository: manaflow-ai/cmux
Length of output: 945
🏁 Script executed:
# Verify the error messaging when python3 fails
cat -n Resources/bin/tmux | sed -n '93,99p'
cat -n Resources/bin/tmux | sed -n '343,352p'Repository: manaflow-ai/cmux
Length of output: 1023
Avoid a hard runtime dependency on python3 in this shim.
The shim requires python3 to parse JSON responses in three critical agent-spawning paths: split-window (line 94), list-panes with format strings (lines 185–217), and new-window (line 344). While error handling exists (returning "Error: failed to create split" when parsing fails), it masks the real failure—missing python3 in PATH. On a clean macOS install where python3 is not guaranteed, agent team spawning will fail with a generic error message instead of clearly indicating the missing dependency.
Consider either bundling a Python interpreter with the DMG or removing the JSON parsing dependency by using alternative tools like jq or native macOS utilities.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Resources/bin/tmux` around lines 94 - 95, The shim currently calls python3 to
parse JSON (e.g., the surface_ref extraction using: surface_ref=$(echo "$result"
| python3 -c "import sys,json;
print(json.load(sys.stdin).get('surface_ref',''))" ...)), creating a hard
runtime dependency that fails silently; add an explicit runtime check for a JSON
parser at startup (e.g., test -x "$(command -v python3)" or "$(command -v jq)"),
and if none is found print a clear error message about the missing dependency
and exit, or refactor the three parsing sites (split-window, list-panes format
handling, new-window) to use jq when available (fall back to python3 only if
present) so the code no longer assumes python3 is on PATH and users get
actionable errors.
| TMUX_SHIM_FORMAT="$format" TMUX_SHIM_PANE_MAP="$PANE_MAP" echo "$panes" | python3 -c " | ||
| import sys, json, os | ||
| fmt = os.environ.get('TMUX_SHIM_FORMAT', '') | ||
| pane_map_path = os.environ.get('TMUX_SHIM_PANE_MAP', '') | ||
| try: | ||
| data = json.load(sys.stdin) | ||
| except (json.JSONDecodeError, ValueError): | ||
| sys.exit(0) | ||
| # Build reverse map: cmux surface ref -> tmux pane ID | ||
| rev = {} | ||
| if pane_map_path: | ||
| try: | ||
| for line in open(pane_map_path): | ||
| line = line.strip() | ||
| if '=' in line: | ||
| tid, sref = line.split('=', 1) | ||
| rev[sref] = tid | ||
| except FileNotFoundError: | ||
| pass | ||
| panes = data if isinstance(data, list) else [] | ||
| for i, p in enumerate(panes): | ||
| line = fmt | ||
| sref = p.get('surface_ref', p.get('id', '')) | ||
| pane_id = rev.get(sref, '%' + str(i)) | ||
| line = line.replace('#{pane_id}', pane_id) | ||
| line = line.replace('#{pane_index}', str(i)) | ||
| line = line.replace('#{pane_width}', str(p.get('width', 80))) | ||
| line = line.replace('#{pane_height}', str(p.get('height', 24))) | ||
| line = line.replace('#{pane_active}', '1' if p.get('focused', False) else '0') | ||
| line = line.replace('#{pane_current_command}', p.get('command', 'bash')) | ||
| line = line.replace('#{pane_pid}', str(p.get('pid', 0))) | ||
| print(line) | ||
| " 2>/dev/null |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TMUX_SHIM_FORMAT="pane" echo '[]' | python3 -c 'import os; print(repr(os.environ.get("TMUX_SHIM_FORMAT")))'
# Expected: None / empty output, because the variable is scoped to `echo`, not `python3`.Repository: manaflow-ai/cmux
Length of output: 64
🏁 Script executed:
#!/bin/bash
# Check if the file exists and verify the code around lines 185-217
if [ -f "Resources/bin/tmux" ]; then
echo "=== File found, showing lines 180-220 ==="
sed -n '180,220p' "Resources/bin/tmux" | cat -n
else
echo "File not found at Resources/bin/tmux"
find . -name "tmux" -type f 2>/dev/null | head -20
fiRepository: manaflow-ai/cmux
Length of output: 1956
Pass the format env vars to python3, not to echo.
TMUX_SHIM_FORMAT=... TMUX_SHIM_PANE_MAP=... echo "$panes" | python3 ... only sets those variables for echo. python3 sees empty values (None), causing fmt and pane_map_path to be empty strings. This breaks formatting entirely and loses the reverse lookup functionality, resulting in blank output.
Suggested fix
- TMUX_SHIM_FORMAT="$format" TMUX_SHIM_PANE_MAP="$PANE_MAP" echo "$panes" | python3 -c "
+ TMUX_SHIM_FORMAT="$format" TMUX_SHIM_PANE_MAP="$PANE_MAP" python3 -c "
import sys, json, os
fmt = os.environ.get('TMUX_SHIM_FORMAT', '')
pane_map_path = os.environ.get('TMUX_SHIM_PANE_MAP', '')
try:
data = json.load(sys.stdin)
@@
-\" 2>/dev/null
+\" 2>/dev/null <<<\"$panes\"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Resources/bin/tmux` around lines 185 - 217, The environment variables
TMUX_SHIM_FORMAT and TMUX_SHIM_PANE_MAP are currently being set for echo instead
of the python3 process, so fmt and pane_map_path are empty; fix this by ensuring
those vars are exported/placed in the environment of the python3 invocation (so
python3 sees TMUX_SHIM_FORMAT and TMUX_SHIM_PANE_MAP and can populate fmt and
pane_map_path). Locate the shell line that runs python3 (the invocation that
pipes "$panes" into python3 -c "...") and move the assignments so they apply to
the python3 process (or use env/export before calling python3), ensuring the
python code using fmt and pane_map_path receives the correct values.
| if [[ "$print_mode" == true ]]; then | ||
| case "$message" in | ||
| *pane_id*) echo "%${CMUX_SURFACE_ID:-0}" ;; | ||
| *session_name*) echo "cmux" ;; | ||
| *window_index*) echo "0" ;; | ||
| *pane_index*) echo "0" ;; | ||
| *) echo "$message" ;; | ||
| esac |
There was a problem hiding this comment.
Make display-message -p do format substitution, not token short-circuiting.
The current case returns a single canned value based on the first matching placeholder. Combined formats like #{session_name}:#{window_index}.#{pane_index} lose surrounding text, and #{pane_id} returns %${CMUX_SURFACE_ID:-0} instead of the shim pane id stored in PANE_MAP. Callers that fetch the current pane id and feed it back into send-keys/kill-pane can miss the mapping.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Resources/bin/tmux` around lines 268 - 275, The current print_mode branch
short-circuits token matches in the case on "$message" and returns single canned
values, which breaks combined formats and returns the raw %${CMUX_SURFACE_ID:-0}
instead of the mapped pane id; change the logic in the print_mode handling to
perform format substitution on "$message" (e.g. replace occurrences of
"#{pane_id}", "#{session_name}", "#{window_index}", "#{pane_index}" rather than
matching the whole string) by doing ordered string replacements: lookup PANE_MAP
for the actual shim pane id and substitute it for "#{pane_id}" (falling back to
%${CMUX_SURFACE_ID:-0} only if no mapping), substitute "cmux" for
"#{session_name}", "0" for "#{window_index}" and "#{pane_index}", and then echo
the fully substituted string so combined formats like
"#{session_name}:#{window_index}.#{pane_index}" are preserved.
| target="" direction="-R" amount="5" | ||
| while [[ $# -gt 0 ]]; do | ||
| case "$1" in | ||
| -t) target="$2"; shift 2 ;; | ||
| -x) shift 2 ;; # absolute width (not directly supported) | ||
| -y) shift 2 ;; # absolute height (not directly supported) | ||
| -L) direction="-L"; shift ;; | ||
| -R) direction="-R"; shift ;; | ||
| -U) direction="-U"; shift ;; | ||
| -D) direction="-D"; shift ;; | ||
| [0-9]*) amount="$1"; shift ;; | ||
| *) shift ;; | ||
| esac | ||
| done | ||
|
|
||
| if [[ -n "$target" ]]; then | ||
| surface=$(resolve_pane "$target") | ||
| cmux_cmd resize-pane --pane "$surface" "$direction" --amount "$amount" 2>/dev/null | ||
| fi |
There was a problem hiding this comment.
resize-pane -x/-y currently performs the wrong resize.
This branch ignores -x/-y, but direction still defaults to -R and amount to 5. So tmux resize-pane -t %1 -x 120 becomes a relative “grow right by 5”, which is worse than a no-op. If absolute sizing is unsupported, please bail out explicitly instead of mutating pane size incorrectly.
Minimal guard
-resize-pane)
- target="" direction="-R" amount="5"
+resize-pane)
+ target="" direction="" amount="5" absolute_resize=false
@@
- -x) shift 2 ;; # absolute width (not directly supported)
- -y) shift 2 ;; # absolute height (not directly supported)
+ -x|-y) absolute_resize=true; shift 2 ;;
@@
- if [[ -n "$target" ]]; then
+ if [[ "$absolute_resize" == true ]]; then
+ exit 0
+ fi
+ if [[ -n "$target" && -n "$direction" ]]; then
surface=$(resolve_pane "$target")
cmux_cmd resize-pane --pane "$surface" "$direction" --amount "$amount" 2>/dev/null
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| target="" direction="-R" amount="5" | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -t) target="$2"; shift 2 ;; | |
| -x) shift 2 ;; # absolute width (not directly supported) | |
| -y) shift 2 ;; # absolute height (not directly supported) | |
| -L) direction="-L"; shift ;; | |
| -R) direction="-R"; shift ;; | |
| -U) direction="-U"; shift ;; | |
| -D) direction="-D"; shift ;; | |
| [0-9]*) amount="$1"; shift ;; | |
| *) shift ;; | |
| esac | |
| done | |
| if [[ -n "$target" ]]; then | |
| surface=$(resolve_pane "$target") | |
| cmux_cmd resize-pane --pane "$surface" "$direction" --amount "$amount" 2>/dev/null | |
| fi | |
| target="" direction="" amount="5" absolute_resize=false | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -t) target="$2"; shift 2 ;; | |
| -x|-y) absolute_resize=true; shift 2 ;; | |
| -L) direction="-L"; shift ;; | |
| -R) direction="-R"; shift ;; | |
| -U) direction="-U"; shift ;; | |
| -D) direction="-D"; shift ;; | |
| [0-9]*) amount="$1"; shift ;; | |
| *) shift ;; | |
| esac | |
| done | |
| if [[ "$absolute_resize" == true ]]; then | |
| exit 0 | |
| fi | |
| if [[ -n "$target" && -n "$direction" ]]; then | |
| surface=$(resolve_pane "$target") | |
| cmux_cmd resize-pane --pane "$surface" "$direction" --amount "$amount" 2>/dev/null | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Resources/bin/tmux` around lines 283 - 301, The current option parsing for
resize-pane ignores -x/-y but leaves direction and amount set (defaulting to -R
and 5), causing unintended relative resizes; update the parsing logic that
handles -x and -y to mark an "absolute_size" error (e.g., set a flag like
absolute_size=true) instead of shifting and continuing, and then before calling
resolve_pane/ cmux_cmd resize-pane check that flag and bail out with a clear
error message and non-zero exit (or return) indicating absolute sizing is
unsupported; ensure you reference the same symbols in the change (the case
branches for -x and -y, the variables target/direction/amount, resolve_pane and
the cmux_cmd resize-pane invocation) so the code never falls through to perform
a relative resize when -x/-y were provided.
Summary
tmuxshim atResources/bin/tmuxthat translates tmux commands to cmux equivalents, enabling AI coding agent teams to natively spawn teammates in cmux splitsTMUXenv var in every cmux shell (GhosttyTerminalView.swift) so tools that detect tmux route through the shimHow it works
AI coding agents with team/multi-agent features detect tmux via the
TMUXenv var, then call:tmux split-window -h→ shim runscmux new-split righttmux send-keys -t %N "command" Enter→ shim runscmux send --surface surface:N "..."tmux capture-pane -t %N -p→ shim runscmux capture-pane --surface surface:Ntmux kill-pane -t %N→ shim runscmux close-surface --surface surface:NThe shim maintains a
%N→surface:Npane ID mapping file so all commands target the correct cmux surfaces.Since
Resources/bin/is prepended toPATHin every cmux shell, the shim interceptstmuxcalls before the real tmux binary (if installed).Supported tmux commands
split-window -h/-vnew-split right/downsend-keys -t %Nsend --surface surface:Ncapture-pane -t %N -pcapture-pane --surface surface:Nkill-pane -t %Nclose-surface --surface surface:Nselect-pane -t %Nfocus-panel --panel surface:Nresize-paneresize-pane(passthrough)has-sessiondisplay-message -plist-panes -Flist-paneswith format translationnew-windownew-workspaceTest plan
split-window→ creates visible cmux split, returns clean%Npane IDsend-keys→ sends text and Enter to correct surface, no stdout noisecapture-pane→ reads terminal content from target surfacekill-pane→ closes the correct surface cleanlyhas-session/list-sessions/display-message→ return expected valuesResolves #1080
Summary by cubic
Adds a
tmuxshim and exports a per-surfaceTMUXenv var so tmux-aware tools can split, send keys, and capture panes natively incmux. Resolves #1080.New Features
Resources/bin/tmux: translates coretmuxcommands tocmux(split-window,send-keys,capture-pane,kill-pane,select-pane,list-panes,display-message,resize-pane,new-window; others passthrough/no-op).%N→surface:*mapping and sets per-surfaceTMUX=<socket>,<pid>,<surface>inGhosttyTerminalView.swiftso tools route through the shim.Bug Fixes
list-panes -F: returns real%NIDs via reverse map (no synthetic indices); prevents format injection via env-passed format; usesgrep -Ffor literal matching.%Nallocation withflock; honorssend-keys -l; extractssurface_refonnew-window;display-message -pusesCMUX_SURFACE_ID.TMUXand prependsResources/bin/toPATHafter env merge so caller vars can’t override the shim.Written for commit 5f0863b. Summary will update on new commits.
Summary by CodeRabbit
New Features
Environment