Nicholas Clooney

Start the Chain: My Three-Agent Setup Is Now One Command

Part of a series

In Messy Chat with Your Agents? I added one more agent to my setup: a main agent whose only job is to talk to me. It hands work to a secondary, which coordinates a Codex worker, which splits the work across its own spawned agents. I only ever talk to the main agent.

That setup started as a few paragraphs in one project's AGENTS.md. A week later it's built into tmux-agents, and I run it in every project.

From a recipe to one command

The first time, I set it up by hand. Open a new pane, connect it to the existing Claude agent, tell it that it's now the main agent and only talks to me, tell the old agent that it's now the coordinator, and make sure the main agent is not connected to Codex. It worked, but it was a recipe I had to repeat for every project.

Now the roles live in the agent-chain skill. I tell a Claude agent "start the chain" and it does the rest: it renames itself to the main agent, spawns the secondary to its right, and spawns the worker below the secondary. Or, from a shell:

tmux-agents start chain ~/Source/Projects/my-project

That opens a new window for the project and starts Claude with "start the chain" as its first prompt.

One tmux window with the blog's main agent on the left, the secondary Claude agent top right and the Codex worker bottom right
The blog's chain, while I was telling the main agent what to write in the "After a restart" section below: main agent on the left, secondary top right, Codex worker bottom right.

What changed with real use

The idea stayed the same. A few details changed once I'd lived with it for a week.

FYIs are notices, not requests. The original rules said to mark FYIs "no reply needed". That wasn't enough. In tmux-agents a request puts the receiver back to work and makes the sender wait for an answer, whatever the text says. So an agent that had finished got woken up by an FYI, and if it then didn't reply, it showed up as needing me. Now there's a separate notice message that expects no reply and no work.

The secondary and worker are members, not sub-agents. At first the chain was built from ordinary spawned agents. That meant they showed up in the sub-agent list and status chips next to short-lived helpers, and cleaning up finished sub-agents could close them too. Now they're long-lived members: out of the default list and bulk cleanup, but still flagged when they need me.

The chain is visible. Spawned agents normally run in hidden windows, and you peek at them from a list. For the chain I want all three on screen, so the secondary and worker open as splits in the main agent's window.

The coordinator is called the secondary. A small one, but every skill, guide and message now uses the same name for it.

Across projects

The rule I like most is about projects talking to each other. Each project's secondary is its interface to other projects. Requests go secondary to secondary, and agents only edit their own project's files.

Here's a real one. When tmux-agents ships a release, the blog gets a timeline entry for it. When v1.12.2 went out, the tmux-agents secondary didn't open the blog repo and write the entry itself. It asked the blog's secondary, which wrote and committed it, then told the blog's main agent that the commit was ready but not pushed, because pushing deploys the site. The main agent asked me, and I pushed it.

Nobody reached into a project that wasn't theirs, and the one decision that needed me reached me as a single question.

After a restart

I don't start chains one at a time anymore. I have a small script, tmux-main-sequence, that builds my whole tmux session: a dashboard window, a window for my home directory, one for the blog, and one for each project in a config file. Every window except the dashboard starts with a chain, using the same "start the chain" prompt.

		
  1. #!/usr/bin/env bash
  2. # tmux-main-sequence — join the standard project session group
  3. #
  4. # Usage: tmux-main-sequence [--add-missing] [--no-attach]
  5. #
  6. # Windows: 0 dashboard (shell), 1 ~, 2 blog, then configured projects.
  7. # Each client joins through its own main-N session, with independent window selection.
  8. # Existing group windows are unchanged unless --add-missing is given.
  9. # Per-client sessions are destroyed on detach; the base session stays available.
  10. # Config: ~/.config/tmux-main-sequence/projects, one directory per line.
  11. # Blank lines and full-line # comments are ignored; spaces in paths are supported.
  12. # Example config (local and untracked):
  13. # ~/Source/Projects/tmux-agents
  14. # ~/Source/Projects/projects-stone-age
  15. # ~/Source/Projects/job-hunting/career-ops
  16. set -euo pipefail
  17. usage() {
  18. cat <<'EOF'
  19. Usage: tmux-main-sequence [--add-missing] [--no-attach]
  20. Create or join the main session group through a separate main-N client session.
  21. Client sessions share windows but select them independently and vanish on detach.
  22. An existing group or a base session named main is reused.
  23. --add-missing Add missing window names with fresh agents, then join.
  24. Use the intended index if free; otherwise append at the end.
  25. --no-attach Skip joining: do not create a per-client session or attach.
  26. -h, --help Show this help.
  27. Environment:
  28. TMUX_MAIN_SEQUENCE_SESSION Group/base name (default: main; clients: main-N).
  29. TMUX_MAIN_SEQUENCE_CONFIG Project list (default: ~/.config/tmux-main-sequence/projects).
  30. TMUX_MAIN_SEQUENCE_AGENT Shell command (default: tmux-agents start claude -- "start the chain").
  31. TMUX_MAIN_SEQUENCE_SOCKET Isolated tmux socket name (tmux -L).
  32. Project list: one directory per line, with ~ expansion, blank lines and
  33. full-line # comments. Missing directories are warned about and skipped.
  34. EOF
  35. }
  36. add_missing=false
  37. attach=true
  38. for arg in "$@"; do
  39. case "$arg" in
  40. --add-missing) add_missing=true ;;
  41. --no-attach) attach=false ;;
  42. -h|--help) usage; exit 0 ;;
  43. *) printf 'tmux-main-sequence: unknown argument: %s\n' "$arg" >&2
  44. usage >&2; exit 2 ;;
  45. esac
  46. done
  47. session=${TMUX_MAIN_SEQUENCE_SESSION:-main}
  48. config=${TMUX_MAIN_SEQUENCE_CONFIG:-$HOME/.config/tmux-main-sequence/projects}
  49. script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
  50. tmux_cmd=(tmux)
  51. if [[ -n ${TMUX_MAIN_SEQUENCE_SOCKET:-} ]]; then
  52. tmux_cmd+=(-L "$TMUX_MAIN_SEQUENCE_SOCKET")
  53. fi
  54. attach_session() {
  55. local number=1 client_id
  56. if "$attach"; then
  57. while "${tmux_cmd[@]}" has-session -t "=$session-$number" 2>/dev/null; do
  58. number=$((number + 1))
  59. done
  60. client_id=$("${tmux_cmd[@]}" new-session -d -t "$session_id" -s "$session-$number" -P -F '#{session_id}')
  61. if "$fresh"; then
  62. "${tmux_cmd[@]}" select-window -t "$client_id:1"
  63. fi
  64. # Queue the option and attach together: a standalone set-option would
  65. # immediately destroy this still-unattached session before we could join it.
  66. if [[ -n ${TMUX:-} ]]; then
  67. "${tmux_cmd[@]}" set-option -t "$client_id" destroy-unattached on \; switch-client -t "$client_id"
  68. else
  69. "${tmux_cmd[@]}" set-option -t "$client_id" destroy-unattached on \; attach-session -t "$client_id"
  70. fi
  71. fi
  72. }
  73. fresh=false
  74. session_id=
  75. base_id=
  76. # A group may survive after its originally named session has been removed.
  77. while IFS='|' read -r id name group; do
  78. if [[ $group == "$session" ]]; then
  79. session_id=$id
  80. break
  81. fi
  82. if [[ $name == "$session" ]]; then base_id=$id; fi
  83. done < <("${tmux_cmd[@]}" list-sessions -F '#{session_id}|#{session_name}|#{session_group}' 2>/dev/null || true)
  84. session_id=${session_id:-$base_id}
  85. if [[ -n $session_id ]]; then
  86. if ! "$add_missing"; then
  87. attach_session
  88. exit 0
  89. fi
  90. else
  91. fresh=true
  92. session_id=$("${tmux_cmd[@]}" new-session -d -s "$session" -n dashboard -c "$HOME" -P -F '#{session_id}')
  93. "${tmux_cmd[@]}" set-option -t "$session_id" base-index 0
  94. "${tmux_cmd[@]}" set-option -t "$session_id" renumber-windows off
  95. initial_index=$("${tmux_cmd[@]}" display-message -p -t "$session_id:" '#{window_index}')
  96. if [[ $initial_index != 0 ]]; then
  97. "${tmux_cmd[@]}" move-window -s "$session_id:$initial_index" -t "$session_id:0"
  98. fi
  99. "${tmux_cmd[@]}" set-window-option -t "$session_id:0" automatic-rename off
  100. "${tmux_cmd[@]}" set-window-option -t "$session_id:0" allow-rename off
  101. fi
  102. add_window() {
  103. local index=$1 name=$2 dir=$3 start_agent=$4 existing max=-1 occupied=false pane
  104. while IFS= read -r existing; do
  105. [[ $existing != "$name" ]] || return 0
  106. done < <("${tmux_cmd[@]}" list-windows -t "$session_id" -F '#{window_name}')
  107. if [[ ! -d $dir ]]; then
  108. printf 'tmux-main-sequence: skipping missing directory: %s\n' "$dir" >&2
  109. return 0
  110. fi
  111. while IFS= read -r existing; do
  112. if (( existing == index )); then occupied=true; fi
  113. if (( existing > max )); then max=$existing; fi
  114. done < <("${tmux_cmd[@]}" list-windows -t "$session_id" -F '#{window_index}')
  115. if "$occupied"; then
  116. printf 'tmux-main-sequence: index %s occupied; appending %s at %s\n' "$index" "$name" "$((max + 1))" >&2
  117. index=$((max + 1))
  118. fi
  119. if "$start_agent"; then
  120. "$script_dir/tmux-chain" "$dir" --target "$session_id:$index" --name "$name" >/dev/null
  121. else
  122. pane=$("${tmux_cmd[@]}" new-window -d -t "$session_id:$index" -n "$name" -c "$dir" -P -F '#{pane_id}')
  123. "${tmux_cmd[@]}" set-window-option -t "$pane" automatic-rename off
  124. "${tmux_cmd[@]}" set-window-option -t "$pane" allow-rename off
  125. fi
  126. }
  127. add_window 0 dashboard "$HOME" false
  128. add_window 1 '~' "$HOME" true
  129. add_window 2 blog "$HOME/Source/Projects/subspace-collection/blog.clooney.io" true
  130. index=3
  131. if [[ -f $config ]]; then
  132. while IFS= read -r dir || [[ -n $dir ]]; do
  133. # Trim surrounding whitespace, preserving spaces inside directory names.
  134. dir="${dir#"${dir%%[![:space:]]*}"}"
  135. dir="${dir%"${dir##*[![:space:]]}"}"
  136. case "$dir" in
  137. ''|'#'*) continue ;;
  138. '~') dir=$HOME ;;
  139. '~/'*) dir="$HOME/${dir:2}" ;;
  140. esac
  141. while [[ $dir != / && $dir == */ ]]; do dir=${dir%/}; done
  142. add_window "$index" "$(basename "$dir")" "$dir" true
  143. index=$((index + 1))
  144. done < "$config"
  145. fi
  146. if "$fresh"; then
  147. "${tmux_cmd[@]}" select-window -t "$session_id:1"
  148. fi
  149. attach_session

That's great right up until the machine restarts. tmux goes away with every agent in it. Running tmux-main-sequence again brings back all the windows and starts fresh chains, but they're new sessions. The main agent no longer knows what we were talking about, and the secondary has forgotten which decisions were made.

This isn't specific to my script. It happens to anyone using tmux-agents start chain: after a restart the chain starts over, and the context is gone.

The fix is simple. Both Claude Code and Codex keep their past sessions on disk, and both have a /resume command that lets you search them. So after a restart, I run /resume in the main agent's pane, search for the old session, and pick up where we left off.

Usually that's the only one I resume. A fresh secondary and worker are fine when nothing important was in flight. But it's case by case: if the secondary or the worker was in the middle of something that matters, I resume them too, so the work picks up where it stopped instead of starting over.

Resuming works much better if your sessions have names. If they're called something like "blog main agent" and "blog secondary", finding the right one takes a second. If they're not, you're scrolling through a list of first prompts that all say "start the chain".

Try it

Install tmux-agents, the agent-chain skill included, with the skills CLI:

npx skills add TheClooneyCollection/tmux-agents -g

Then tell your agent "set up tmux-agents", and once that's done, "start the chain".