| name | dispatch-claude |
| description | Dispatch work to parallel Claude Code sessions via cmux workspaces. Use this skill for "$dispatch-claude" or whenever you need to hand off one or more independent tasks to separate Claude instances โ for example, when acting as a dispatcher and the user asks you to "run these prompts", "fan out", "spawn sessions", "dispatch Claude", or when you've prepared prompts that should each run in their own Claude session. This is agent-invokeable only โ the agent uses it to launch Claude work, not the user directly. Always prefer this over asking the user to copy-paste prompts into new terminals. |
| dependencies | ["cmux"] |
Dispatch Claude
Launch Claude Code sessions in new cmux workspaces. Each dispatch creates an
isolated workspace, starts claude with the prompt, and never steals focus from
the user's current terminal.
This skill depends on cmux. Turning dispatch-claude on through shan should
also turn on cmux, because every real dispatch workflow uses the cmux CLI and
its workspace model.
When to use
- You've prepared multiple independent prompts (e.g. "fix tests" + "push to CI")
- The user asks you to kick off parallel work
- You're acting as a coordinator and need to hand off tasks
How it works
- Write the prompt to a temp file (avoids all quoting/escaping issues)
- Write a launcher script that:
execs claude with --session-id, --name, and the prompt read from file
- removes its temp prompt + launcher files after startup
- Pick a group color โ read the parent workspace's
dispatch-group status
color. If it has one, reuse it. If not, scan all workspaces for taken colors
and pick the first unused color from the palette, then set it on the parent too.
- Create a cmux workspace with
--name + --cwd, then poll cmux tree
for the first terminal surface
- Name the workspace as
{parent} โบ {slug} so children are instantly
recognizable as belonging to the parent workspace
- Reorder adjacent โ move the new workspace immediately after the parent
(or after the last sibling from this batch) so the dispatch group is
contiguous in the sidebar
- Set group color + origin status โ color-code the group and add a
dispatch-origin status line showing โ {parent} on each child card
- Warm the PTY without stealing focus โ cmux lazy-spawns terminal
processes on first focus, so a brand-new workspace's surface has no shell
to receive input until something focuses it. Dispatch:
- records the user's currently-selected workspace in the caller's window,
- calls
cmux focus-pane on the new pane to force the PTY to spawn,
- polls
cmux read-screen until fish's prompt is visible,
- calls
cmux select-workspace to put the user's view back,
- then sends a single atomic
cmux send containing Ctrl-C + the launcher
command + \n (\003bash /tmp/dispatch-.../launch.sh\n). One socket
round-trip โ no race window between the vi-NORMAL reset and the body.
Why each step is load-bearing (cmux 0.64.6 audit, 2026-05-14)
The four-step dance survived an empirical audit against every cmux primitive
that could plausibly subsume it. Each step is pinned to a concrete cmux
behavior that has no workaround. Do not strip a step without re-running the
matching test.
| Step | Why it exists | Audit evidence |
|---|
focus-pane to warm the PTY | cmux 0.64.6 lazy-spawns terminal PTYs on first focus. cmux send succeeds (returns OK) but the bytes are queued into a surface that doesn't yet exist; cmux read-screen reports "Terminal surface not found" until something focuses the pane. | new-workspace --command "echo X > /tmp/F" against an unfocused workspace produces no file and no readable surface. |
read-screen prompt-glyph poll | cmux emits workspace.created / surface.created / surface.focused events but no shell.ready / pty.spawned event. surface-health.in_window toggles with workspace visibility (it flips back to false after select-workspace restore), so it cannot be used as a readiness signal. The prompt-glyph regex is the only deterministic signal available without a fish shell-integration script (cmux ships bash + zsh integrations only). | cmux events --no-heartbeat during workspace creation shows the structural events but no readiness event; surface-health.in_window was false even after the prompt glyph appeared, because the user's view had been restored. |
select-workspace to restore the user's view | focus-pane --workspace W --pane P selects workspace W in its window โ not just the pane within it. Verified: caller workspace flipped from workspace:5 โ workspace:55 after focus-pane. The dispatch contract is "user's focus never moves," so we must select back. | Test L in the audit. |
Atomic \003 + command + \n in one cmux send | Fresh fish sessions on this machine land in vi-NORMAL mode (the โฎ prompt glyph confirms this). Without the leading Ctrl-C, e, c, h, o of echo โฆ are eaten as motions and the o opens a new INSERT line, producing "fish: Unknown command: o". The atomic single-send form ($'\003'"โฆ"$'\n') collapses what used to be two sends with a 100ms sleep between them. | Test D / E (vi-NORMAL trap reproduced visually); Test I (atomic send confirmed working). |
Alternatives considered and ruled out
| Alternative | Why rejected |
|---|
new-workspace --command "โฆ" | Same lazy-PTY trap โ the command is sent before the PTY exists. With --focus true to force the spawn, it both steals focus AND still races vi-NORMAL. |
new-workspace --layout '{"pane":{"surfaces":[{"type":"terminal","command":"โฆ"}]}}' | Layout-embedded commands race the same lazy-PTY + vi-NORMAL window; no file produced in the test. |
cmux paste-buffer after cmux set-buffer | Does not use bracketed paste in a way that fish vi-mode honors as a literal block. First chars still get consumed as motions. No improvement over cmux send. |
cmux respawn-pane --command "โฆ" | Identical to cmux send semantics for our case. Needs focus-pane first (same lazy-PTY trap) and still races vi-NORMAL. |
cmux events --name <shell-ready-event> | No such event is emitted. Only structural lifecycle events (workspace.created, surface.created, surface.focused, surface.selected, pane.focused) exist. |
cmux surface-health.in_window as a readiness gate | Means "is this surface currently visible in its window's selected workspace" โ flips back to false once we restore the user's view, so it's the wrong semantic. |
cmux set-hook <event> <command> | tmux-compat hook event names are not the cmux structural events; no documented hook for "PTY spawned" or "shell prompt printed." |
When to re-audit
Re-run the audit if any of the following ship upstream:
- A
shell.ready, pty.spawned, or surface.alive event on cmux events.
- A
--wait-for-ready / --wait-shell flag on cmux send or cmux new-workspace.
- A fish shell-integration script in
/Applications/cmux.app/Contents/Resources/shell-integration/ (currently bash and zsh only).
- A
new-workspace --no-focus-warm or similar primitive that spawns the PTY eagerly without focusing.
The workspace gets its own terminal process running Claude, and the session is
named for claude --resume <slug> via Claude's native --name flag.
Timing and Patience
Do not assume a Claude review, audit, or large analysis is hung just because it
is quiet.
Rules:
- For large reviews, audits, or corpus-scale analysis, be willing to wait up to
10 minutes before declaring the run stuck.
- Prefer patient polling over killing and restarting. Restart churn wastes more
time than a quiet long-running review.
- Only declare a run stuck early if there is hard evidence of failure: the
process exited non-zero, auth failed, the session never started, or the
command is clearly wedged without a Claude process behind it.
- If the user is waiting on a review result, tell them the expected turnaround
up front instead of improvising impatience mid-run.
- When you need captured findings rather than pure fire-and-forget dispatch, a
headless
claude -p review is valid, and the same 10-minute patience rule
applies for large prompts or large diffs.
Why workspaces, not surfaces (tabs)
Surfaces (tabs) steal keyboard focus when created โ the new tab grabs input and
the user's keystrokes bleed into it. Workspaces don't auto-focus. They appear in
the sidebar but the user stays where they are.
Each workspace also gets its own sidebar status via the claude hook integration,
so the user sees per-task progress indicators (working/ready/blocked).
Workspace naming
Keep names short (2-4 words), title case, action-oriented:
E2E Importer Fixes not "Run the e2e test suite for the importer"
CI Check Push not "Push and fix CI failures"
Lint Step3 Cleanup not "Fix lint warnings in Step3OfferMapping.tsx"
Names must be unique across active workspaces. Avoid names that are substrings of
other workspace names โ find-window uses substring matching, so Fix Tests would
ambiguously match both itself and Fix Tests E2E.
Implementation
The dispatch script lives at ~/.claude/skills/dispatch-claude/dispatch.sh.
~/.claude/skills/dispatch-claude/dispatch.sh <slug> <prompt-file> [cwd]
For multiple dispatches in sequence, the script exports DISPATCH_LAST_WS so
subsequent calls chain adjacently in the sidebar.
Writing prompt files
Write each prompt to a temp file. Use heredocs with single-quoted delimiters
to avoid shell expansion:
cat > /tmp/dispatch-1.txt << 'PROMPT_EOF'
Your multi-line prompt here.
It can contain "quotes", 'apostrophes', $variables, and **markdown** safely.
PROMPT_EOF
Example: dispatching two tasks
cat > /tmp/dispatch-1.txt << 'PROMPT_EOF'
Run the importer e2e tests. Fix any failures and commit.
PROMPT_EOF
~/.claude/skills/dispatch-claude/dispatch.sh "E2E Importer Fixes" /tmp/dispatch-1.txt ~/projects/myapp
cat > /tmp/dispatch-2.txt << 'PROMPT_EOF'
Push and monitor CI checks on PR
PROMPT_EOF
~/.claude/skills/dispatch-claude/dispatch.sh "CI Check Push" /tmp/dispatch-2.txt ~/projects/myapp
rm /tmp/dispatch-1.txt /tmp/dispatch-2.txt
Both sessions launch instantly in parallel workspaces. The user stays in their
current terminal the entire time. If the parent workspace is named Subimp UI,
the sidebar shows:
Subimp UI โ parent
Subimp UI โบ E2E Importer Fixes โ child 1 (adjacent)
Subimp UI โบ CI Check Push โ child 2 (adjacent)
Each child also has a โ Subimp UI status line on its sidebar card.
Fallback: headless dispatch
Use only when:
- cmux is unavailable or failing, and
- the user explicitly approves headless mode
Do not silently substitute this for the main workflow.
Headless dispatch launches claude -p processes instead of interactive sessions.
Only for prompts that are self-contained and need no clarifying questions.
Prefer this for:
- second-opinion review passes
- architecture critique
- narrow textual analysis
Avoid this for:
- implementation work that may need clarification
- tasks that rely on approvals or interactive tool use
- long-running tasks where you expect back-and-forth
claude -p \
--tools "" \
--no-session-persistence \
--permission-mode dontAsk \
"$PROMPT"
Communicating with dispatched workspaces
After dispatching, you may need to send follow-up messages to a running workspace
(e.g., adding requirements, relaying a bug found by a sibling). Use the cross-workspace
send pattern from the cmux skill:
ws_ref=$(cmux --json find-window "E2E Importer Fixes" | jq -r '.matches[0].ref')
cmux send --workspace "$ws_ref" "Also fix the timezone handling in parse_date"
cmux send-key --workspace "$ws_ref" enter
Never use cmux send "workspace-name" "message" โ this sends both strings as text
to your own workspace. The --workspace flag requires a ref (workspace:N), not a name.
Use find-window to resolve names to refs.
If find-window returns no matches or multiple ambiguous matches, do not send to any
workspace. Tell the user what you searched for, what was found (or not), and that you
cannot reliably identify the target. Let the user resolve it.
For reading a workspace's terminal output:
ws_ref=$(cmux --json find-window "E2E Importer Fixes" | jq -r '.matches[0].ref')
cmux read-screen --workspace "$ws_ref" --scrollback --lines 50
When find-window finds nothing โ the agent is probably a surface
find-window matches workspace and window titles only โ not surface (tab)
titles. A dispatched agent starts as its own workspace, but the user routinely
moves it into another workspace's tab strip as a surface to group agents.
Once moved, find-window "<the dispatch slug>" returns zero โ the slug is now a
surface title, and the workspace it lives in has a different title. The refs
printed by dispatch.sh also go stale after such a move.
So a zero from find-window is not "agent gone" โ search the surfaces:
surface_ref=$(cmux --json tree | jq -r '
[ .. | objects
| select(.type? == "terminal" and ((.title? // "") | test("NX Envar Rename"; "i"))) ]
| if length == 1 then .[0].ref else "" end')
cmux send, send-key, and read-screen all accept --surface <id|ref|index>
exactly like --workspace:
cmux send --surface "$surface_ref" "Also fix the timezone handling"
cmux send-key --surface "$surface_ref" enter
cmux read-screen --surface "$surface_ref" --scrollback --lines 50
Notes:
- In cmux JSON, the human label is
.title โ not .name. jq '.matches[].name'
/ jq '.workspaces[].name' silently yields null; use .title.
- The surface-search is also subject to the "exactly one match or don't send"
rule above โ if the tree search yields zero or many, surface it to the user.
Important notes
- Each dispatched session is fully independent โ include all necessary context
in the prompt (directory path, branch name, file paths, what was already done).
- Sessions are automatically named via Claude's native
--name flag. The
slug appears in both the cmux sidebar and claude --resume history.
- To follow a dispatched task: switch to its workspace in the cmux sidebar.
- To resume after disconnect:
claude --resume <slug> or claude -c in the
task's directory.
- Dispatches are instant and parallel โ no sequential boot waiting.
- Clean up temp prompt files after dispatch:
rm /tmp/dispatch-*.txt
- The launcher script is temporary and removed automatically by
dispatch.sh
- To open a markdown file for viewing, use
cmux markdown open <path> โ not
cmux open <path> (which only works for directories). The markdown viewer
renders formatted content with live reload.
- For CI check workflows, dispatched agents can follow the
gh/pr checks skill
which provides a complete analyze-fix-push-watch loop.
- Dispatched agents that need to poll (CI status, deploy health, etc.) can use
/loop for recurring checks rather than implementing manual polling.