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

> Source: <https://dev.to/bokuwalily/swarming-claude-code-and-codex-in-parallel-running-multiple-agents-at-once-with-tmux-and-git-1fej>
> Published: 2026-07-18 01:00:55+00:00

In my [previous post about a cost budget advisor](https://zenn.dev/bokuwalily/articles/budget-cap-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 started` → `running` → `completed` / `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.

```
# ① dry-run (default): show what would be created, as JSON
node scripts/orchestrate-worktrees.js plan.json

# ② --write-only: no worktree creation; only writes task.md / status.md / handoff.md
node scripts/orchestrate-worktrees.js plan.json --write-only

# ③ --execute: full run, including worktree creation and tmux launch
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 branches`materializePlan`

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

With`replaceExisting: false`

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

or manually`tmux 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 by`seedPaths`

get committed after they're edited in the worktree. If you seed a global`package.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 stuck`true`

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 once`task.md`

/ `status.md`

/ `handoff.md`

, so Claude Code can review them together afterward`launcherCommand`

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](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*
