Launches and manages Claude Code worker sessions in tmux: delegate tasks via prompts, monitor lifecycle events, approve tool calls, enable multi-turn conversations, and collect results. For orchestrating parallel workflows.
npx claudepluginhub jmagar/lab --plugin claude-session-driverThis skill uses the workspace's default tool permissions.
You can launch other Claude Code sessions as "workers" in tmux, send them prompts, monitor their progress through lifecycle events, read their output, and hand them off to a human operator. This gives you the ability to delegate work, run tasks in parallel, or set up supervised workflows.
Spawns and manages persistent tmux-based Claude Code CLI sessions with bidirectional communication. Subcommands: spawn, send, read, status, list, kill for parallel peer orchestration and multi-turn steering.
Orchestrates multiple parallel Claude Code sessions across projects using tmux, enables remote monitoring and control via WhatsApp approvals and status commands.
Launches git worktrees with tmux background sessions running Claude Code agents for non-blocking execution of large multi-step coding tasks like major refactors or new services.
Share bugs, ideas, or general feedback.
You can launch other Claude Code sessions as "workers" in tmux, send them prompts, monitor their progress through lifecycle events, read their output, and hand them off to a human operator. This gives you the ability to delegate work, run tasks in parallel, or set up supervised workflows.
Workers are full interactive Claude Code sessions launched with --dangerously-skip-permissions so they never block on interactive permission prompts. A plugin (claude-session-driver) injects hooks that emit lifecycle events to a JSONL file, which you poll to track worker state. A PreToolUse hook gives the controller a window to inspect and approve or deny every tool call before it executes. The scripts handle all the plumbing: tmux management, session IDs, event files, and cleanup.
These must be available on the system:
All scripts live at ../../scripts/ relative to this skill's base directory. Set a convenience variable:
SCRIPTS="<this-skill's-base-directory>/plugin/scripts"
RESULT=$("$SCRIPTS/launch-worker.sh" my-worker /path/to/project)
SESSION_ID=$(echo "$RESULT" | jq -r '.session_id')
TMUX_NAME=$(echo "$RESULT" | jq -r '.tmux_name')
The script:
my-workerclaude with the session-driver plugin loaded and --dangerously-skip-permissionssession_start event (up to 30s)session_id, tmux_name, and events_filePermission bypass is automatic. The worker's PreToolUse hook provides controller-based gating (see Tool Approval below), so the built-in interactive permission dialog is redundant.
Pass extra claude CLI arguments after the working directory:
# Use a specific model
RESULT=$("$SCRIPTS/launch-worker.sh" my-worker /path/to/project --model sonnet)
For most interactions, use converse.sh — it sends the prompt, waits for the worker to finish, and returns the assistant's response in one call:
RESPONSE=$("$SCRIPTS/converse.sh" my-worker "$SESSION_ID" "Refactor the auth module to use JWT tokens" 300)
echo "$RESPONSE"
It handles --after-line tracking automatically, so multi-turn conversations just work:
R1=$("$SCRIPTS/converse.sh" my-worker "$SESSION_ID" "Write tests for the auth module" 300)
R2=$("$SCRIPTS/converse.sh" my-worker "$SESSION_ID" "Now add edge case tests for expired tokens" 300)
For finer control, use the individual scripts. send-prompt.sh sends text without waiting:
"$SCRIPTS/send-prompt.sh" my-worker "Refactor the auth module to use JWT tokens"
"$SCRIPTS/wait-for-event.sh" "$SESSION_ID" stop 300
This blocks until the worker emits a stop event (meaning it finished processing and is waiting for input) or the timeout (in seconds) expires. Exit code 0 means the event arrived; exit code 1 means timeout.
The matching event JSON line is printed to stdout:
{"ts":"2025-01-15T10:30:00Z","event":"stop"}
# All events
"$SCRIPTS/read-events.sh" "$SESSION_ID"
# Last 3 events
"$SCRIPTS/read-events.sh" "$SESSION_ID" --last 3
# Only stop events
"$SCRIPTS/read-events.sh" "$SESSION_ID" --type stop
# Follow in real-time (blocks -- run in background Bash job for monitoring)
"$SCRIPTS/read-events.sh" "$SESSION_ID" --follow &
MONITOR_PID=$!
# ... do other work ... then stop monitoring:
kill $MONITOR_PID 2>/dev/null
Event types emitted by the plugin:
| Event | Meaning | Extra fields |
|---|---|---|
session_start | Worker session initialized | cwd |
user_prompt_submit | A prompt was submitted to the worker | |
pre_tool_use | Worker is about to call a tool | tool, tool_input |
stop | Worker finished processing, waiting for input | |
session_end | Worker session terminated |
The worker's full conversation (prompts and responses) is stored in Claude's JSONL session log. The path uses an encoded form of the working directory where / becomes - with a leading -:
/Users/jesse/myproject encodes to -Users-jesse-myprojectThe log is at: ~/.claude/projects/<encoded-path>/<session-id>.jsonl
To read the last assistant response:
ENCODED_PATH=$(echo "/path/to/project" | sed 's|/|-|g')
LOG_FILE=~/.claude/projects/${ENCODED_PATH}/${SESSION_ID}.jsonl
grep '"type":"assistant"' "$LOG_FILE" | tail -1 | jq -r '.message.content[] | select(.type=="text") | .text'
"$SCRIPTS/stop-worker.sh" my-worker "$SESSION_ID"
The script:
/exit to the tmux sessionsession_end event/tmp/claude-workers/If you want a human to take over an active worker session:
The worker is running in tmux session 'my-worker'. You can:
- Watch live: tmux attach -t my-worker
- Take over: just start typing in the attached session
- Return to me: detach with Ctrl-B d
Leave the worker running. Do not stop it when handing off.
| Script | Usage | Description |
|---|---|---|
converse.sh | <tmux-name> <session-id> <prompt> [timeout=120] | Send prompt, wait, return response |
launch-worker.sh | <tmux-name> <working-dir> [claude-args...] | Start a worker session |
send-prompt.sh | <tmux-name> <prompt-text> | Send a prompt to a worker |
wait-for-event.sh | <session-id> <event-type> [timeout=60] [--after-line N] | Block until event or timeout |
read-events.sh | <session-id> [--last N] [--type T] [--follow] | Read event stream |
stop-worker.sh | <tmux-name> <session-id> | Gracefully stop and clean up |
approve-tool.sh | <session-id> <allow|deny> | Respond to a pending tool approval |
read-turn.sh | <session-id> [--full] | Format last turn as markdown |
All scripts exit 0 on success, non-zero on failure. Error messages go to stderr.
RESULT=$("$SCRIPTS/launch-worker.sh" task-worker ~/myproject)
SESSION_ID=$(echo "$RESULT" | jq -r '.session_id')
"$SCRIPTS/send-prompt.sh" task-worker "Run the test suite and fix any failures"
"$SCRIPTS/wait-for-event.sh" "$SESSION_ID" stop 600
# Read what happened, then clean up
"$SCRIPTS/read-events.sh" "$SESSION_ID"
"$SCRIPTS/stop-worker.sh" task-worker "$SESSION_ID"
# Launch workers for different tasks
R1=$("$SCRIPTS/launch-worker.sh" worker-api ~/myproject)
S1=$(echo "$R1" | jq -r '.session_id')
R2=$("$SCRIPTS/launch-worker.sh" worker-ui ~/myproject)
S2=$(echo "$R2" | jq -r '.session_id')
# Send each their task
"$SCRIPTS/send-prompt.sh" worker-api "Add pagination to the /users endpoint"
"$SCRIPTS/send-prompt.sh" worker-ui "Add a loading spinner to the user list page"
# Wait for both (sequentially -- first one to finish unblocks its wait)
"$SCRIPTS/wait-for-event.sh" "$S1" stop 600
"$SCRIPTS/wait-for-event.sh" "$S2" stop 600
# Clean up
"$SCRIPTS/stop-worker.sh" worker-api "$S1"
"$SCRIPTS/stop-worker.sh" worker-ui "$S2"
Pass one worker's output to the next:
# Worker 1: Generate an API spec
R1=$("$SCRIPTS/launch-worker.sh" worker-spec ~/myproject)
S1=$(echo "$R1" | jq -r '.session_id')
"$SCRIPTS/send-prompt.sh" worker-spec "Generate an OpenAPI spec for the users endpoint and save it to /tmp/api-spec.yaml"
"$SCRIPTS/wait-for-event.sh" "$S1" stop 300
# Worker 2: Implement from the spec that Worker 1 produced
R2=$("$SCRIPTS/launch-worker.sh" worker-impl ~/myproject)
S2=$(echo "$R2" | jq -r '.session_id')
"$SCRIPTS/send-prompt.sh" worker-impl "Implement the API endpoint defined in /tmp/api-spec.yaml"
"$SCRIPTS/wait-for-event.sh" "$S2" stop 600
# Clean up both
"$SCRIPTS/stop-worker.sh" worker-spec "$S1"
"$SCRIPTS/stop-worker.sh" worker-impl "$S2"
The key: workers communicate through files on disk. The controller orchestrates the sequence.
converse.sh handles --after-line tracking automatically, so multi-turn is straightforward:
RESULT=$("$SCRIPTS/launch-worker.sh" supervised ~/myproject)
SESSION_ID=$(echo "$RESULT" | jq -r '.session_id')
R1=$("$SCRIPTS/converse.sh" supervised "$SESSION_ID" "Write tests for the auth module" 300)
R2=$("$SCRIPTS/converse.sh" supervised "$SESSION_ID" "Now add edge case tests for expired tokens" 300)
"$SCRIPTS/stop-worker.sh" supervised "$SESSION_ID"
converse.sh returns only the final text response. To see the full turn (thinking, tool calls, results), use read-turn.sh:
# After a converse.sh call, review what the worker actually did
"$SCRIPTS/read-turn.sh" "$SESSION_ID"
# Show complete tool results (default truncates to 5 lines)
"$SCRIPTS/read-turn.sh" "$SESSION_ID" --full
Output is formatted as markdown: thinking in blockquotes, tool calls as code blocks, results in fenced blocks, and text responses inline.
If the tmux session disappears, send-prompt.sh will fail with "tmux session does not exist." Check before sending:
if ! tmux has-session -t my-worker 2>/dev/null; then
echo "Worker is gone -- need to relaunch"
fi
The event file at /tmp/claude-workers/<session-id>.events.jsonl will still contain events emitted before the crash.
wait-for-event.sh exits 1 on timeout. The worker may still be running. Choose how to handle it:
wait-for-event.sh again with a new timeoutstop-worker.shsend-prompt.sh sends text literally via tmux send-keys -l, which handles multi-line text and special characters correctly. Very long prompts (tens of KB) may hit tmux buffer limits. For extremely large inputs, consider writing the instructions to a file and telling the worker to read it:
echo "Your detailed instructions here..." > /tmp/worker-instructions.txt
"$SCRIPTS/send-prompt.sh" my-worker "Read /tmp/worker-instructions.txt and follow those instructions"
Every tool call the worker makes emits a pre_tool_use event and waits up to 30 seconds for a controller decision before auto-approving. This gives you a window to inspect and approve or deny each tool call.
To monitor and auto-approve (default): Do nothing. If no decision is written within the timeout, the tool call proceeds.
To actively review tool calls:
# Watch for pending tool approvals
PENDING_FILE="/tmp/claude-workers/${SESSION_ID}.tool-pending"
# Check if a tool call is waiting for approval
if [ -f "$PENDING_FILE" ]; then
cat "$PENDING_FILE" # Shows tool_name and tool_input
# Approve it
"$SCRIPTS/approve-tool.sh" "$SESSION_ID" allow
# Or deny it
"$SCRIPTS/approve-tool.sh" "$SESSION_ID" deny
fi
The timeout is configurable via the CLAUDE_SESSION_DRIVER_APPROVAL_TIMEOUT environment variable (default: 30 seconds). To change it, pass the env var when launching:
RESULT=$(CLAUDE_SESSION_DRIVER_APPROVAL_TIMEOUT=60 "$SCRIPTS/launch-worker.sh" my-worker ~/project)
stop-worker.sh handles this, but if the script itself fails, clean up manually:
tmux kill-session -t my-worker 2>/dev/null
rm -f /tmp/claude-workers/<session-id>.events.jsonl
rm -f /tmp/claude-workers/<session-id>.meta
ts and event fields.launch-worker.sh will fail if a session with that name already exists.