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 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.
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*