summon-cc — conduct Claude Code sessions from any agent
Deprecated. summon-cc is archived and unmaintained. Its successor is
conclave (
https://github.com/anfreire/conclave), which spawns agents on Claude
Code, Codex or OpenCode through one CLI, with no tmux pane to drive. Prefer it
for new work; what follows still describes this skill as it stands.
You are the orchestrator. You spawn full interactive Claude Code sessions ("workers")
in detached tmux panes, hand each a scoped prompt, and supervise them through append-only
file signals. You orchestrate; workers do the work. Three laws make this cheap and calm:
- Never parse the terminal. The signals carry everything; the pane is a debug
surface, read only when a signal says something is wrong.
- Never idle-poll. One waiter per worker blocks silently until something happens.
- Act on signals, verify by signals. Every move — spawn, prompt, answer — leaves a
signal proving it landed. The scripts verify their own steps; you verify yours.
Any agent on any harness can be the orchestrator — every capability here is a plain shell
command. Only the worker side is Claude Code, and that's the point: this skill hands
you Claude Code sessions as a building block, unrestricted — what you build with them
(pipelines, review loops, fan-outs, watchdogs) is your call.
Requirements
- Linux or macOS; , , , and a CLI with and
(any current one — to confirm).
- Nothing to install or wire. Spawn injects the six lifecycle hooks per worker via
on the launch line: no settings file of the user's is ever touched, and
sessions you didn't summon never load these hooks at all.
- The two scripts sit next to this SKILL.md in ; invoke them by absolute
path (your working directory is usually elsewhere). below means this
skill's directory.
Quickstart — one worker, start to finish
brief="$(mktemp)" # a fresh file per worker
printf '%s\n' 'Fix the failing test in src/foo. END YOUR TURN with a one-paragraph report.' > "$brief"
bash <skill-dir>/scripts/spawn.sh fixer "$brief"
# → SUMMONED id=fixer-6a53bd1e00002f40 state=/home/you/.local/state/summon/workers/fixer-6a53bd1e00002f40
bash <skill-dir>/scripts/wait.sh fixer-6a53bd1e00002f40 # silent until → EVENT=STOP + the report
tmux kill-session -t '=fixer-6a53bd1e00002f40' # done with it ('=' = exact match)
prints your two handles: the
id (wait, send-keys, kill) and the
state
dir (every signal file) —
and
below. Everything composes these two.
Spawn
Write the worker's prompt to a
file (multi-line pastes as one block). It must not
begin with
,
, or
— those are TUI input prefixes (slash command / bash mode /
memory), so the text would run as a command instead of submitting; spawn rejects such
files loudly. Then:
bash <skill-dir>/scripts/spawn.sh <name> <prompt-file> [effort] [cwd] [permissions]
- — handed to verbatim: claude owns the vocabulary
( lists it); (the default) keeps the session's own
setting. Fire-and-forget tuning: an unknown value doesn't abort — claude silently runs
at its default, with no pane warning; the STOP line shows the effective effort
() as claude reports it — the one place to confirm a value took.
- — the worker's project dir (default: yours).
- — (default →
--dangerously-skip-permissions
: a detached pane
can't answer permission prompts, so workers self-gate instead — see Worker prompts) ·
(no flag: claude's own default in that cwd) · anything else is handed to
verbatim ( lists the modes). A bad mode fails boot
with claude's own error.
- — optional env var of extra flags appended verbatim to the launch
line: for cheap scouts, to revive a dead
worker's conversation in a fresh pane (its session_id is in ).
Spawn is self-verifying end to end (boot, paste, submit — all against hook signals, with
its own retries). If it fails it says why, loudly, with the pane tail and the remedy;
the two first-run consent dialogs are the usual cause (Gotchas).
The loop
spawn → wait → act on the event → wait again → … → down
bash <skill-dir>/scripts/wait.sh <id> [max_secs] # default 1800
Run it in the background if your harness supports that (it's silent until it exits);
otherwise run it blocking. It costs you nothing while the worker runs, and exits with one
self-contained block:
- — the worker ended its turn; its final message follows, captured by
the Stop hook (the transcript is never parsed). Usually the report you asked for — but
a worker sometimes ends a turn just to tell you something: read it, then poke or re-wait.
A Stop line that can't be read is still — the block says the message is
unreadable and points at the transcript, so a Stop is never hidden behind an IDLE.
- — paused on AskUserQuestion; the numbered options follow.
Answer (next section), then wait again.
- — cap hit, nothing happened; heartbeat age + recent tool activity
follow. Long silence is normal (thinking, subagents, long subprocesses) — re-arm the
same command. Intervene only on real evidence of a stall.
- — the worker is gone: pane killed, or claude exited leaving a bare
shell behind (the block says which, with post-mortem pointers).
Re-arming is literally the same command again — the cursor (kept in the worker dir)
resumes from the last event reported.
Answer a question
Options are numbered in
order; the pane appends its own entries ("Type
something", "Chat about this")
after the real options, so your numbers are the real ones.
- Single question — a digit selects and submits:
- Several questions — digits auto-advance; after the last, Enter submits the review step:
tmux send-keys -t <id> 1; sleep 1; tmux send-keys -t <id> 3; sleep 1; tmux send-keys -t <id> Enter
- — digits toggle, Enter submits.
- Free text exists (the "Type something" option) but is fiddly — prefer a listed option.
- Verify, don't assume. The landed answer appends a line (with the
chosen labels) to , and the next wait moves past the question. A
single-question digit commits instantly — there is no review screen — so when the choice
matters, read the QuestionAnswered line. Multi-question flows end on an Enter-gated
review screen: before submitting a destructive choice, peek it:
tmux capture-pane -t <id> -p | tail -30
Poke a running worker
Only when no question is pending (
absent). While a question
modal is up, typed text is captured by the modal and a trailing Enter selects whatever is
highlighted — it answers the question by accident instead of queueing your message.
Answer the question first, or interrupt to cancel it.
- Queue a follow-up (read at the worker's next turn boundary; = literal, so a
one-word message like isn't sent as the tmux key of that name):
tmux send-keys -t <id> -l 'your message' && tmux send-keys -t <id> Enter
- Long or quote-heavy follow-up — paste it the way spawn does (per-worker buffer name,
so parallel pokes can't cross):
tmux load-buffer -b poke-<id> <file> && tmux paste-buffer -t <id> -b poke-<id> -p -d && sleep 1 && tmux send-keys -t <id> Enter
- Never start a poke with , , or — TUI input prefixes, typed or pasted alike:
runs a slash command ( kills the worker), executes the rest as a
shell command in the worker's cwd, opens the memory picker; none submits a
prompt (no UserPromptSubmit lands). Need a literal one? Lead with
something else ("Note: /foo …").
- Interrupt generation to redirect:
tmux send-keys -t <id> Escape
, then poke. Escape
is the TUI's own interrupt key and a no-op on an idle worker — but send it once:
a rapid double Escape opens the Rewind menu, a modal the signals can't
see that can restore code and conversation. If a poke seems swallowed (no
UserPromptSubmit line lands), resend Enter first — a lagged paste leaves your text
sitting in the input box, and one more Enter just submits it (re-pasting first
would stack a second copy). Still nothing? A modal is up: one Escape dismisses it,
then re-poke.
Never C-c — a quick double C-c exits claude entirely, and a misjudged
stall is exactly when you'd hit an idle one. An interrupted turn emits no Stop — the
next completed turn (your poke's) does. Interrupting cancels a pending question; the
orphaned question.json is archived automatically the moment your next message submits.
- A fresh spawn is a blank context — to continue a worker, poke its pane; don't respawn.
Results, status, debugging
- Final message — printed by wait on STOP; re-read any time:
grep '"event":"Stop"' <state>/events.jsonl | tail -1 | jq -r .last_assistant_message
- Answers given —
grep '"event":"QuestionAnswered"' <state>/events.jsonl | tail -1 | jq -c .answers
- All workers —
find "${SUMMON_STATE:-$HOME/.local/state/summon}/workers" -name meta.json -exec jq -r '"\(.id) \(.name) \(.cwd)"' {} + 2>/dev/null
— cross-check liveness with , pending questions with
find "${SUMMON_STATE:-$HOME/.local/state/summon}/workers" -name question.json 2>/dev/null
.
- Instant probe —
bash <skill-dir>/scripts/wait.sh <id> 0
returns at once: any
unconsumed event, else an IDLE snapshot (heartbeat age + recent activity). Same
single-shot, cursor-safe semantics as any wait — probing costs nothing.
- Peek (debug only — every pane capture costs orchestrator context; the signals above
are the normal path):
tmux capture-pane -t <id> -p | tail -40
Teardown
When you've consumed a worker's result:
tmux kill-session -t '=<id>'
— keep the
:
it pins the exact name, where a bare
falls back to prefix matching once the exact
session is gone (Gotchas). The Claude transcript persists on disk regardless. Remove
the state dir when you're done with its evidence:
. Ids are never
reused — a stale dir can't collide.
Worker prompts — what makes orchestration work
- Self-contained and scoped. The worker shares none of your context: state the goal,
the exact scope, what's out of scope, and "read X and Y yourself". One job per worker.
- Tell it how to finish: "END YOUR TURN with a report of A/B/C." Stop is your
completion signal, so finishing the turn — with the facts you need in the final
message — is part of the worker's job. The final message is all you should ever need.
- Self-gate risky actions (essential under ): instruct the worker to pause
and ask via AskUserQuestion before anything irreversible, destructive, or
out-of-scope — that routes the decision to you through the QUESTION event, with
numbered options you answer by digit.
- Independent review catches more: for pipelines, have one worker do the step and a
fresh worker review it; repeat until a reviewer that changed nothing approves.
Decision routing
When a worker asks: answer it yourself if the task's constraints already determine the
answer, or the choice is recoverable — keep the work moving. Escalate to the human only
what is genuinely new, material, or irreversible. When unsure whether something is
reversible, treat it as escalate-worthy. Agree this boundary with the human up front.
One-by-one vs batch
- One-by-one: spawn → wait to completion → act → down → next. Each result gates the
next step; trivial bookkeeping. Default for dependent steps.
- Batch: spawn N workers (distinct names, a brief file per worker; keep each
printed id and state), arm one waiter per worker, handle whichever exits first,
re-arm just that one. Every signal
file is per-worker — concurrent workers never cross wires. No background execution on
your harness? Round-robin with short caps —
wait.sh w1 60; wait.sh w2 60; …
— waits
are single-shot and cursor-safe, so taking turns loses nothing. Cap concurrency to
what the machine and your attention can handle — a fleet booting at once can push
claude's start past spawn's 45s boot window.
The state dir — reference
One per worker:
${SUMMON_STATE:-~/.local/state/summon}/workers/<id>/
(spawn prints it).
| file | meaning |
|---|
| what was spawned: name, cwd, prompt file, effort, permissions, extra args |
| what injected into this worker: the six hooks, promptSuggestionEnabled: false
|
| lifecycle: SessionStart, UserPromptSubmit, AskUserQuestion, QuestionAnswered, Stop (carries the final message, permission mode, effort), SessionEnd — one line per event, whole or absent |
| the pending AskUserQuestion; presence == pending (answered/stale copies are archived beside it) |
| / | epoch of last tool call / one attributed line per tool call |
| exit sentinel: the pane's shell drops it the moment claude exits, for any reason (spawn fails fast on it during startup; wait reads it as DEAD when claude dies without a SessionEnd — hard kill, OOM) |
| waiter bookkeeping — automatic, don't touch |
Gotchas
- Stop is completion; SessionEnd is bookkeeping — SessionEnd may or may not fire
for any given death: a clean writes one, sometimes does
too, a hard kill never. Only its presence means anything — never wait
on it, never read anything into its absence. wait.sh encodes all of this: claude
gone with the pane's shell still alive reads DEAD — via SessionEnd when it fired,
via the sentinel when it didn't (SIGKILL, OOM) — never eternal IDLE.
- First-run dialogs stall boot (spawn fails loud at the SessionStart step): a machine
that has never accepted
--dangerously-skip-permissions
, or a cwd Claude doesn't trust
yet, shows an interactive dialog first. Remedy: run once in that dir
interactively, accept, respawn. Don't script past these — they're consent gates.
- Non-bypass workers can stall on a permission dialog — permission and plan-approval
prompts are not AskUserQuestion: no hook fires, no signal appears, the heartbeat just
goes stale. On a non-bypass worker, IDLE + old heartbeat + no pending question → peek
the pane; answer the dialog with send-keys, or respawn under bypass.
- Vocabularies live in claude, not here. Effort levels and permission modes are
passed through verbatim and never validated — is the source of truth.
- No ghost text in a worker's input box. The TUI's suggested next prompt — dim text
after each turn, which a pane capture prints as if typed — is switched off in every
worker's injected settings (
promptSuggestionEnabled: false
), so text sitting in a
worker's input box is real: a lagged paste or a queued poke.
- tmux prefix-matches once no exact name survives — a dead worker's id could
then resolve to a similarly named session. Defense in depth: ids are fixed-width, spawn rejects
names that embed an id-shaped run (so one id can never nest another — don't pass an
old worker's id as a new worker's name), and the teardown one-liner pins with
. Pane-target commands take the same pinning as if you want it for
send-keys/paste/capture.
- Hooks gate on and are injected per worker — no session you didn't
summon loads or writes anything. Every event line twice? A second copy of the hooks
is registered in a settings file claude loads for that cwd — find it with
grep -l on_session_start.sh <cwd>/.claude/settings*.json ~/.claude/settings.json
and remove it; the signals tolerate the duplicates meanwhile.
- Workers get the tmux server's environment, not yours — the server keeps the
environment of whoever started it, so a variable exported before spawn never reaches
the pane. Per-worker knobs go on the launch line () or into the
injected settings, which is how spawn passes its own.
- Don't extend the scripts with GNU-isms (, , , ) —
epoch-seconds arithmetic and plain POSIX tools are what keep macOS supported.