| name | cmux-socket-control |
| description | Control cmux tabs, workspaces, and terminal panes via Unix socket. Use when reading terminal output, sending commands to another agent's pane, switching tabs, or monitoring coder progress. |
⚠️ Submit Discipline (MANDATORY — read this before every cmux steer)
cmux send does NOT press Enter. This is the #1 recurring cmux failure mode
(verified 2026-07-16: user explicitly flagged "you always forget to send" after the
fable iOS pivot bootstrap). The 4-step ritual below is a hard contract for every
send to a cmux surface. Skip ANY step and the message sits in the input buffer
without ever reaching the agent.
⚠️ SUPERSEDED — Use cmux CLI, not raw socket protocol (for name → surface resolution)
The raw nc -U $SOCK examples further down still work for low-level access,
but they have a name → surface resolution gap that has caused repeated
failures ("why do you always get confused when I name a surface"). The
canonical recipe is the cmux CLI, which exposes workspace/surface/tab refs
directly.
Always start here when the user names a surface, tab, or workspace:
WS=$(cmux identify --json | python3 -c '
import sys, json
d = json.load(sys.stdin)
c = d.get("caller") or {}
print(c.get("workspace_ref") or d.get("focused", {}).get("workspace_ref"))
')
cmux tree --all --workspace "$WS"
Three anti-patterns this skill now warns against
- Global name search —
cmux list-pane-surfaces | grep <name> matches a
same-named tab in another workspace and you steer the wrong agent.
- grep-filtering the tree —
cmux tree --all | grep <name> hides sibling
tabs and the ◀ here / ◀ active markers you need to disambiguate.
list-pane-surfaces defaults — defaults to ONE pane and omits tabs in
other panes; use cmux tree --all --workspace "$WS" for the full picture.
The raw nc -U $SOCK blocks below remain useful for low-level debugging
(system.tree, system.identify, custom JSON-RPC methods).
The 4-step ritual
cmux send --workspace workspace:N --surface surface:M "your message"
cmux send-key --workspace workspace:N --surface surface:M enter
sleep 8
cmux capture-pane --workspace workspace:N --surface surface:M --lines 25
⚠️ Output Contract — typed text + terminal response (MANDATORY)
Every reply that reports a cmux send action MUST include, in the same reply:
- The exact text that was typed — verbatim copy of the string passed to
cmux send.
- The cmux terminal response — verbatim transcript of what
cmux capture-pane /
cmux read-screen returned AFTER the cmux send-key enter settle window
(typically 5-15s). Specifically, the agent's first action after absorption.
- Submission status — explicit verdict: "submitted (churning label X)",
"not submitted (text still at ❯ prompt)", or "blocked (no churn, retried N times)".
Treat as not working until we see a response. A reply that does NOT include
both the typed text AND a terminal response is invalid evidence that the
steer landed. The operator cannot distinguish a successful send from a failed
send that left text in the input buffer.
Canonical contract + echo-back template: ~/.hermes/skills/cmux/references/output-contract-mandatory.md.
⚠️ LLM-Provenance Caveat (MANDATORY footer)
Every reply that quotes cmux output, terminal text, or agent actions produced
by another LLM (the worker agent OR the assistant's own synthesis of agent
output) MUST end with this verbatim footer:
This was generated from another LLM and not the actual user, so feel free
to push back if you disagree and we can discuss.
Full caveat rules + scope: ~/.hermes/skills/cmux/references/output-contract-mandatory.md § "LLM-Provenance Caveat".
Echo-back proof (MANDATORY)
Every cmux steering action MUST be followed by an echo-back proof in the same
turn or the immediate next turn to your operator (Slack thread, terminal reply,
or whichever channel triggered the steer). The proof MUST follow the template
in ~/.hermes/skills/cmux/references/output-contract-mandatory.md and include
the typed text + terminal response + submission status, not just the
churning label.
◀ sent to surface:55 (LEFT/claudec) at <HH:MM:SS PT> — typed: "<first 80 chars>";
response: "<first 80 chars of the churning label or first agent line>"; status:
submitted (churning label "Forming… 9s · ↓ 4.9k tokens").
Banned (these are the failure modes the user keeps flagging):
- "I sent the message" (no Enter proof)
- "The agent should have received it" (no churning label)
cmux send with no follow-up cmux send-key enter
- Sending to a surface that hasn't been focused (the global focus may be on a
different workspace; use the raw RPC
surface.focus if needed)
Worktree-pointer strategy for long briefs
For task briefs >200 chars (e.g. orchestrating iOS app pivot, multi-PR review),
do NOT paste the full text into the input. Write the brief to a file in the
agent's cwd (e.g. .cmux-<task>-brief.md) and send a 1-2 line pointer. This
avoids the autocompleter contamination pitfall where shell-style tokens inside
long text trigger tab completion mid-stream.
Canonical reference
Full recipe + edge cases + the 2026-06-25 worked example live at:
~/.hermes/skills/cmux/references/send-submit-proof-2026-06-25.md
This rule was added 2026-07-16 after the fable iOS pivot bootstrap surfaced
"you always forget to send" / "make sure you press submit and the work starts
on the cmux input" (Slack ts 1784185650.528089). Apply it uniformly to every
cmux-touching skill.
cmux Socket Control
Find the socket
SOCK=$(ls /tmp/cmux-debug-*.sock /tmp/cmux-debug.sock /tmp/cmux.sock 2>/dev/null | head -1)
echo "Using: $SOCK"
Common paths:
- Tagged debug build:
/tmp/cmux-debug-<tag>.sock
- Untagged debug:
/tmp/cmux-debug.sock
- Release:
/tmp/cmux.sock
Rule 1: ALWAYS look up workspace by NAME, never by index
The user can switch workspaces at any time — indices shift. UUID stays stable.
SOCK="/tmp/cmux-debug-appclick.sock"
WS_UUID=$(printf "list_workspaces\n" | nc -U $SOCK | grep "cmux: ubuntu" | grep -oE '[A-F0-9-]{36}')
printf "list_surfaces $WS_UUID\n" | nc -U $SOCK
CODER_UUID=$(printf "list_surfaces $WS_UUID\n" | nc -U $SOCK | grep "cmux_coder\|1:" | grep -oE '[A-F0-9-]{36}' | head -1)
Known stable UUIDs (cmux: ubuntu workspace, current session):
| Surface | UUID | Role |
|---|
| 0 | 87DB76A9-60A8-43FC-BFC2-51A5DECEA9B8 | supervisor (cmux) |
| 1 | F05FCE84-ECA7-4944-BCAA-7DFFC105D0D9 | coder (cmux_coder) |
Re-run list_surfaces if you get unexpected errors — UUIDs reset on app restart.
Sending to a pane (cross-workspace safe)
send_surface works regardless of which workspace the user has focused.
send_key_surface sends key chords.
SOCK="/tmp/cmux-debug-appclick.sock"
UUID="F05FCE84-ECA7-4944-BCAA-7DFFC105D0D9"
printf "send_key_surface $UUID ctrl-a\n" | nc -U $SOCK
printf "send_key_surface $UUID ctrl-k\n" | nc -U $SOCK
printf "send_surface $UUID your message here\n" | nc -U $SOCK
sleep 0.2
printf "send_key_surface $UUID enter\n" | nc -U $SOCK
Reading a pane
read_screen only works in the current workspace (where user is focused).
UUID-based reads fail cross-workspace. Use git log as fallback.
printf "read_screen 1 --lines 40\n" | nc -U $SOCK
printf "read_screen 1 --lines 60 --scrollback\n" | nc -U $SOCK
git -C $HOME/projects_reference/cmux_ubuntu log --oneline -5
Checking if a pane is busy before sending
result=$(printf "send_surface $UUID probe\n" | nc -U $SOCK)
Wait-until-idle loop:
while true; do
result=$(printf "send_surface $UUID probe\n" | nc -U $SOCK)
if [ "$result" = "OK" ]; then
printf "send_key_surface $UUID ctrl-a\n" | nc -U $SOCK
printf "send_key_surface $UUID ctrl-k\n" | nc -U $SOCK
break
fi
sleep 2
done
Full pattern: find coder + check busy + send task
SOCK="/tmp/cmux-debug-appclick.sock"
WS_UUID=$(printf "list_workspaces\n" | nc -U $SOCK | grep "cmux: ubuntu" | grep -oE '[A-F0-9-]{36}')
CODER_UUID=$(printf "list_surfaces $WS_UUID\n" | nc -U $SOCK | grep "1:" | grep -oE '[A-F0-9-]{36}')
result=$(printf "send_surface $CODER_UUID probe\n" | nc -U $SOCK)
if [ "$result" = "OK" ]; then
printf "send_key_surface $CODER_UUID ctrl-a\n" | nc -U $SOCK
printf "send_key_surface $CODER_UUID ctrl-k\n" | nc -U $SOCK
printf "send_surface $CODER_UUID <your task here>\n" | nc -U $SOCK
sleep 0.2
printf "send_key_surface $CODER_UUID enter\n" | nc -U $SOCK
else
echo "Coder busy — checking git for progress"
git -C $HOME/projects_reference/cmux_ubuntu log --oneline -3
fi
Common commands reference
printf "list_workspaces\n" | nc -U $SOCK
printf "list_surfaces <ws-uuid>\n" | nc -U $SOCK
printf '{"method":"system.tree"}\n' | nc -U $SOCK
printf '{"method":"system.identify"}\n' | nc -U $SOCK
printf "read_screen 0 --lines 30\n" | nc -U $SOCK
printf "read_screen 1 --lines 60 --scrollback\n" | nc -U $SOCK
printf "send_surface <uuid> <text>\n" | nc -U $SOCK
printf "send_key_surface <uuid> ctrl-c\n" | nc -U $SOCK
printf "send_key_surface <uuid> enter\n" | nc -U $SOCK
printf "send_key_surface <uuid> ctrl-a\n" | nc -U $SOCK
printf "send_key_surface <uuid> ctrl-k\n" | nc -U $SOCK
Future: MCP server (PR #3, jleechanorg/cmux)
scripts/cmux_mcp_server.py (Python) and scripts/cmux-mcp-server.mjs (JS) are thin
MCP adapters over the same socket API. Tool names map to our nc patterns:
| MCP tool | Equivalent nc command |
|---|
cmux_socket_discover | ls /tmp/cmux*.sock |
cmux_system_tree | {"method":"system.tree"} |
cmux_list_workspaces | list_workspaces |
cmux_list_surfaces | list_surfaces <uuid> |
cmux_send_text | send_surface <uuid> <text> + send_key_surface enter |
cmux_read_text | read_screen <index> --lines N |
cmux_socket_call | raw nc pass-through escape hatch |
cmux_select_workspace | ⚠ maps to select_workspace — check focus rules first |
Run: python scripts/cmux_mcp_server.py --socket /tmp/cmux-debug-appclick.sock
Known issues in PR (not yet fixed): credential leak in error log (mjs:156),
non-loopback HTTP binding (mjs:865). Do not expose HTTP port on non-loopback hosts.
Rules (non-negotiable)
- Never
select_workspace — it visibly switches the user's active workspace
- Always find by workspace NAME — never hardcode index
- Use UUID for sends — works cross-workspace without focus change
- UUID reads fail cross-workspace — use
read_screen <index> only when in the right workspace; otherwise use git log / filesystem
- send_surface types only — always follow with
send_key_surface enter to submit
- Clear first —
ctrl-a + ctrl-k before typing a new message
- Check busy before sending — probe first, clear probe text on OK