cd /news/developer-tools/swarming-claude-code-and-codex-in-pa… · home topics developer-tools article
[ARTICLE · art-64277] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Swarming Claude Code and Codex in Parallel: Running Multiple Agents at Once with tmux and Git Worktrees

A developer built a system to run multiple AI coding agents (Claude Code and Codex) in parallel using tmux and git worktrees. The three-layer protocol uses a plan.json declaration, an orchestrator script that creates isolated worktrees and launches tmux sessions, and worker scripts that execute tasks without interfering with each other. The approach eliminates branch conflicts and enables parallel execution of refactoring, cleanup, and testing tasks.

read6 min views1 publishedJul 18, 2026

In my previous post about a cost budget advisor, I built a mechanism that checks how much quota is left before it runs anything. This time I want to take that idea one step further: instead of cycling a single Codex through jobs one after another, run several of them at the same time as a swarm.

I'll walk through the actual code for a three-layer protocol where workers declared in plan.json

are expanded by orchestrate-worktrees.js

into a tmux session plus git worktrees, so each Codex runs in parallel without interfering with the others.

When you run Codex jobs one after another on a single repository, you hit issues like:

Separating branches with git worktree

reduces the conflict risk to zero. Combining that with tmux to launch everything in parallel is the design for this post.

plan.json              ← declaration layer (what goes to which worker)
      ↓
orchestrate-worktrees.js  ← orchestrator layer (creates worktrees, launches tmux)
      ↓
orchestrate-codex-worker.sh  ← worker layer (runs Codex, writes artifacts)

The orchestrator doesn't know about the workers, and the workers don't know about each other. Artifacts are consolidated into three files under .orchestration/{session}/{worker_slug}/

.

File Role
task.md
Work instructions for the worker (generated by the orchestrator)
status.md
State: not startedrunningcompleted / failed
handoff.md
Codex output + git status (written by the worker)
{
  "sessionName": "refactor-sprint",
  "repoRoot": "~/my-project",
  "worktreeRoot": "~/worktrees",
  "coordinationRoot": "~/my-project/.orchestration",
  "baseRef": "HEAD",
  "replaceExisting": true,
  "launcherCommand": "bash ~/.claude/scripts/orchestrate-codex-worker.sh {task_file} {handoff_file} {status_file}",
  "seedPaths": ["package.json", "tsconfig.json"],
  "workers": [
    {
      "name": "api-types",
      "task": "src/api/ のレスポンス型を zod スキーマに移行する。既存のテストが全て通ること。",
      "seedPaths": ["src/api/"]
    },
    {
      "name": "ui-cleanup",
      "task": "src/components/ 内の PropTypes を削除し TypeScript 型に一本化する。",
      "seedPaths": ["src/components/"]
    },
    {
      "name": "test-coverage",
      "task": "src/utils/ のユニットテストを追加しカバレッジ 80% 以上にする。",
      "seedPaths": ["src/utils/"]
    }
  ]
}

seedPaths

can be specified in two places: globally and per worker. The global package.json

/ tsconfig.json

are copied into every worker's worktree, while a worker's own seedPaths

are dropped only into that worker.

The entry point has three modes.

node scripts/orchestrate-worktrees.js plan.json

node scripts/orchestrate-worktrees.js plan.json --write-only

node scripts/orchestrate-worktrees.js plan.json --execute

When you invoke --execute

, executePlan

from lib/tmux-worktree-orchestrator.js

runs, processing in this order:

git rev-parse --is-inside-work-tree

and tmux -V

replaceExisting: true

, clean up existing sessions, worktrees, and branchesmaterializePlan

writes the three-file set under .orchestration/

git worktree add -b <branch> <path> <baseRef>

tmux new-session -d -s <session>

split-window -P -F '#{pane_id}'

→ set the layout to tiled → set the pane title → send the launch commandBranch names and worktree paths are generated automatically.

branch:   orchestrator-{session_name}-{worker_slug}
worktree: {repo_name}-{session_name}-{worker_slug}/   (directly under worktreeRoot)

To attach after running, all you need is tmux attach -t refactor-sprint

. Three panes sit side by side, each running its own Codex.

Note

WithreplaceExisting: false

(the default), the run stops with an error if a tmux session of the same name already exists. When re-running, either setreplaceExisting: true

or manuallytmux kill-session -t <name>

first.

The worker script takes three arguments.

bash ~/.claude/scripts/orchestrate-codex-worker.sh \
  <task-file> <handoff-file> <status-file>

You can wire this straight into plan.json

's launcherCommand

using template variables.

"launcherCommand": "bash ~/.claude/scripts/orchestrate-codex-worker.sh {task_file} {handoff_file} {status_file}"

Here's the spot inside that calls Codex (actual code):

cat > "$prompt_file" <<EOF
You are one worker in an ECC tmux/worktree swarm.

Rules:
- Work only in the current git worktree.
- Do not touch sibling worktrees or the parent repo checkout.
- Complete the task from the task file below.
- Do not spawn subagents or external agents for this task.
- Report progress and final results in stdout only.
- Do not write handoff or status files yourself; the launcher manages those artifacts.
...

Task file: $task_file

$(cat "$task_file")
EOF

if codex exec -p yolo -m gpt-5.4 --color never -C "$(pwd)" -o "$output_file" - < "$prompt_file"; then

The key point is - < "$prompt_file"

, which pipes the prompt into stdin. Because -C "$(pwd)"

pins the execution directory to the worktree, there's no worry about Codex accidentally touching files in the parent repository.

On success, handoff.md

is written with Codex's output and git status --short

, and status.md

changes to completed

. On failure it becomes failed

, with no impact on the next worker.

The template variables available in launcherCommand

are as follows (from the actual source):

Variable Contents
{worker_name}
Worker name (e.g. api-types )
{worker_slug}
Slugified name (e.g. api-types )
{session_name}
tmux session name
{repo_root}
Repository root path
{worktree_path}
This worker's worktree path
{branch_name}
This worker's branch name
{task_file}
Absolute path to task.md
{handoff_file}
Absolute path to handoff.md
{status_file}
Absolute path to status.md

For each variable, a _sh

suffix version (shell-quoted) and a _raw

version are also generated. When passing a path that contains spaces to another shell script, using {worktree_path_sh}

is the safe choice.

The renderTemplate

implementation is simple: if it contains an undefined variable, it errors out immediately (Unknown template variable: xxx

). Typos are caught before execution.

A worktree right after git worktree add

is a snapshot as of baseRef

, but sometimes you need the latest locally-modified version of package.json

or a config file. seedPaths

solves that problem by copying.

function overlaySeedPaths({ repoRoot, seedPaths, worktreePath }) {
  for (const seedPath of normalizedSeedPaths) {
    const sourcePath = path.join(repoRoot, seedPath);
    const destinationPath = path.join(worktreePath, seedPath);
    fs.cpSync(sourcePath, destinationPath, {
      dereference: false, force: true, preserveTimestamps: true, recursive: true
    });
  }
}

That said, seedPaths

can't escape outside repoRoot

. Paths like ../../../etc/passwd

are rejected by the ..

check in normalizeSeedPaths

.

Warning

Files copied byseedPaths

get committed after they're edited in the worktree. If you seed a globalpackage.json

and then rewrite it in the worktree, that change rides along on the branch. To avoid unintended changes, seed only the files you actually need.

Even if executePlan

fails midway, it rolls back the resources it was in the middle of creating. It records what has been created so far in createdState

, and on error it calls rollbackCreatedResources

.

1. kill-session the tmux session
2. git worktree remove --force each worktree
3. git worktree prune --expire now
4. git branch -D the corresponding branches
5. delete coordinationDir if it didn't originally exist

This order is the reverse of creation (delete the most recently created first). If one of the rollbacks fails partway through, it continues with the rest and throws a consolidated error at the end.

replaceExisting

still false

and getting stucktrue

at first, or manually kill it while developing._sh

suffix versions like {task_file_sh}

.--write-only

before --execute

.codex exec

executePlan

checks for existence by running tmux -V

at the top. If it's missing, it errors immediately (with a clear message).workers

array in plan.json

, and a single --execute

launches worktree + tmux + Codex all at oncetask.md

/ status.md

/ handoff.md

, so Claude Code can review them together afterwardlauncherCommand

can be freely wired with template variables and swapped out for workers other than CodexNext time I'll write about the orchestrator loop where Claude Code reviews all the handoff.md

files each worker wrote and decides whether they can be merged.

*Written by Lily — I ship iOS apps and automate my content stack with Claude Code.

Follow along: Portfolio · X · GitHub*

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/swarming-claude-code…] indexed:0 read:6min 2026-07-18 ·