| name | cmux-steer |
| description | Read and steer another cmux terminal tab through the Unix socket. |
cmux-steer — Control another cmux terminal tab via the Unix socket
REQUIRED (2026-06-09): This skill is deprecated for runtime steering. The bare cmux send + cmux send-key enter pattern documented here does NOT include proof of submission. Use the canonical wrapper instead: python3 -c "from cmux_client import send_and_submit; print(send_and_submit('workspace:N', 'surface:M', 'text'))" and include the returned proof + proof_ts in your reply. See ~/.hermes_prod/skills/cmux-send-submit/SKILL.md. This skill is preserved for socket read operations (workspace.list, surface.read_text, tree, system.ping) only.
⚠️ SUPERSEDED — Use cmux CLI, not raw socket protocol
The raw nc -U $SOCK examples below 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.
What this skill still does well
The raw nc -U $SOCK blocks below remain useful for low-level debugging
(system.tree, system.identify, custom JSON-RPC methods) and for
environments where the cmux CLI binary is not on PATH. The CLI recipes
above are the default; fall back to the raw socket blocks only when the
CLI fails or is unavailable.
Known cmux CLI bugs (verified)
cmux read-screen --workspace <ws> --surface <surface> can silently
return the focused surface's content instead of the named one (see
~/.hermes/skills/cmux/references/surface-read-routing-bug.md). Use the
focus-then-read recipe above for any non-focused surface.
surface.read_text with workspace_ref/surface_ref ignores the ref
params and returns the focused surface's text. Same focus-then-read fix.
Usage: Read and follow this skill directly; no /cmux-steer slash command is defined.
Purpose: Read and steer another agent's terminal pane (e.g. a coding agent)
from within cmux, without disrupting the user's active workspace navigation.
Socket path
SOCK="$HOME/Library/Application Support/cmux/cmux.sock"
cat ~/Library/Application\ Support/cmux/dev-may-18-last-socket-path
lsof -p $(pgrep -f "cmux DEV may-18") | grep -E "\.sock"
CMUX_SOCKET_PATH=/tmp/cmux-debug-may-18.sock cmux list-workspaces
Rule 1: Always find workspace by NAME, not index
The user can switch workspaces at any time, shifting surface indices. Never
hardcode an index. Always look up by the workspace's display name:
printf "list_workspaces\n" | nc -U $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" | grep -oE '[A-F0-9-]{36}')
Rule 2: Use JSON API for headless cross-workspace sends
The plain-text send_surface command FAILS cross-workspace. Always use the JSON API:
| Method | Cross-workspace? | Notes |
|---|
surface.send_text (JSON) | ✅ always works | Include \n in text for enter |
send_surface (plain text) | ❌ fails cross-workspace | Only works if workspace is selected |
read_screen | ❌ current workspace only | Use index, not UUID |
Sending text (headless, cross-workspace)
printf '{"method":"surface.send_text","params":{"surface_id":"'"$CODER_UUID"'","text":"your instruction here\\n"}}\n' | nc -U $SOCK
This works from any workspace without switching focus. The \n at end acts as Enter.
Checking if idle
result=$(printf '{"method":"surface.send_text","params":{"surface_id":"'"$CODER_UUID"'","text":""}}\n' | nc -U $SOCK)
Creating workspaces and surfaces
printf '{"method":"workspace.create","params":{"title":"my-workspace"}}\n' | nc -U $SOCK
printf '{"method":"workspace.rename","params":{"workspace_id":"UUID","title":"new name"}}\n' | nc -U $SOCK
printf '{"method":"surface.create","params":{"workspace_id":"UUID"}}\n' | nc -U $SOCK
Special keys (when \n in text isn't enough)
For key combos, fall back to plain text protocol (requires workspace focus):
printf "send_key_surface $CODER_UUID ctrl-c\n" | nc -U $SOCK
printf "send_key_surface $CODER_UUID ctrl-a\n" | nc -U $SOCK
printf "send_key_surface $CODER_UUID ctrl-k\n" | nc -U $SOCK
Available: ctrl-c, ctrl-d, enter, tab, escape, up, down, left, right
Reading a pane's screen (current workspace only)
read_screen requires a surface index (not UUID) and only works when the target is in the user's currently focused workspace. Obtain the index from list_surfaces output (e.g. 1: F05FCE84-... cmux_coder → index is 1).
IDX=1
printf "read_screen $IDX --lines 40\n" | nc -U $SOCK
printf "read_screen $IDX --lines 80 --scrollback\n" | nc -U $SOCK
If the user is in a different workspace, infer state from the filesystem instead:
cd /path/to/project && git log --oneline -5
ls -lt src/
Full headless example
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 "cmux_coder" | grep -oE '[A-F0-9-]{36}')
printf '{"method":"surface.send_text","params":{"surface_id":"'"$CODER_UUID"'","text":"cargo build && cargo test\\n"}}\n' | nc -U $SOCK
sleep 30
cd /path/to/project && git log --oneline -3
Creating a fresh workspace with two tabs
SOCK="/tmp/cmux-debug-appclick.sock"
WS=$(printf '{"method":"workspace.create","params":{"title":"my-test"}}\n' | nc -U $SOCK \
| python3 -c "import sys,json; print(json.loads(sys.stdin.read())['result']['workspace_id'])")
printf '{"method":"workspace.rename","params":{"workspace_id":"'"$WS"'","title":"test: a/b"}}\n' | nc -U $SOCK
TAB2=$(printf '{"method":"surface.create","params":{"workspace_id":"'"$WS"'"}}\n' | nc -U $SOCK \
| python3 -c "import sys,json; print(json.loads(sys.stdin.read())['result']['surface_id'])")
TAB1=$(printf "list_surfaces $WS\n" | nc -U $SOCK | grep "0:" | grep -oE '[A-F0-9-]{36}')
printf '{"method":"surface.send_text","params":{"surface_id":"'"$TAB1"'","text":"echo tab1\\n"}}\n' | nc -U $SOCK
printf '{"method":"surface.send_text","params":{"surface_id":"'"$TAB2"'","text":"echo tab2\\n"}}\n' | nc -U $SOCK
Rules summary
- Never use
select_workspace — it switches the user's visible workspace.
- Find workspace by NAME (
list_workspaces → grep name → get UUID).
- Use JSON API for sends —
surface.send_text works headlessly cross-workspace.
- Append
\n to text — acts as Enter key submission.
- Read by index only —
read_screen requires being in the right workspace; use git/filesystem for cross-workspace monitoring.
- Create workspaces via JSON —
workspace.create + surface.create for multi-tab setups.