CodexのWorktreeモードに痺れて、3本並列のAI開発環境を整えた
1. はじめに
最近はGPTモデルの存在感が高まってきましたが、皆さんはCodexのデスクトップアプリを使ったことがありますか?
私はWorktreeモードというのがお気に入りで、AIと並列で開発する手軽さがさらに一段上がった気がします。
複数のブランチを同時に動かしたいとき、ターミナル時代は
- tmuxで画面を分割して、
-
git worktree addして、 - 各ペインで
direnv allowして……
という段取りが必要でした。これがスレッド作成画面で 「Worktree」を選ぶだけに圧縮されます。1クリックです。
ただし、Worktreeモードだけで並列開発が完成するわけではありません。.envrcなどのgitignoredファイルはコピーされないですし、DB周りも1つの環境で複数の作業が動いていると競合が起きる可能性があります。このあたりはCodexの手が届かないので、自分で埋める必要があります。
この記事では、Codex AppのWorktreeモードに足りないものをsetupスクリプトとskill、そしてdocker-compose側の工夫で埋めて、Codexと複数ブランチを「真の意味で」並列で回せる環境を組み上げた話を書きます。
2. CodexのWorktreeモードがすごい
最近はもっぱらClaude CodeよりはCodexを使っているのですが、理由としてはWorktreeモードのボタンを押すだけで並列開発の基盤が出来上がるから、というのが大きいです。
Claude Codeをターミナルで使っていた頃、複数ブランチを並列で動かそうとすると、毎回こんな段取りが必要でした。
- tmuxやcmuxで複数セッションを並べる
- それぞれで
git worktree addする - worktreeごとに
.envrcをコピーしてdirenv allow - どのセッションがどのブランチかを頭で覚えておく
ツール自体は強力ではあるものの、並列で動かす足場は全部自前なので、地味な作業を繰り返すのがネックでした。
CodexのWorktreeモードなら、この作業がボタン1つ押すだけになるので、とても便利です。

スレッド作成時にLocal / Worktree / Cloudから選び、Worktreeを選択。Codexが裏でgit worktreeを切り、スレッドはそのworktreeに紐付いた状態で起動します。worktreeのパスもブランチ名も意識する必要はありません。サイドバーに「どのスレッドがどのworktreeにいるか」が並ぶので、5本同時に走らせても迷子になりません。
さらにHandoffで、スレッドをLocalモードとWorktreeモードの間で行き来できます。Worktreeで雑に試して、いけそうならLocalに持ち込んで仕上げる、という運用もできます。
つまり「並列開発のための環境構築」のうち、
- worktreeを作る
- スレッドと紐付ける
- スレッド間を行き来する
の3つはCodexが丸ごと面倒を見てくれるようになりました。Codexのデスクトップアプリが単にCodexのモデルを使用するためというよりは、並列開発のインターフェースとして機能してくれているイメージですね。
3. 3つの詰まりどころを埋める
Worktreeモードのおかげで、worktreeを作るところまでは完全に楽になりました。が、実際に並列で動かしてみると、すぐ別の壁にぶつかります。
私が良く触るのはdocker compose up一発でAPI・フロント・DBが立ち上がるRails / Next.jsのプロジェクトです。これを2〜3ブランチで同時に動かそうとすると、3つの壁が見えてきました。それぞれ順番に潰していきます。
空なgitignoredファイルは、setupスクリプトで埋める
Worktreeモードで作られるworktreeには、git管理下のファイルしか入っていません。.envrcもnode_modulesもvendor/bundleも、全部空です。
そのままだとdirenv allowも通らず、yarn devもbundle execも動きません。スレッドを作るたびにメインworktreeから手でコピーするのも面倒です。
ここはCodexのlocal environment機能を使えば自動化できます。プロジェクトルートに.codex/local-environment/setup.shを置いておくと、Codexが新しいworktreeを切ったあとに自動でそれを叩いてくれる、という仕組みです。

環境についての設定ページ。プロジェクトごとに設定することができる
やりたいのは「メインworktreeにあるgitignoredファイルを、今いるworktreeにコピーする」だけ。20行ほどで収まります。
#!/usr/bin/env bash
# .codex/local-environment/setup.sh
set -euo pipefail
# メインworktreeのパスをgitから引く
MAIN_WORKTREE=$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')
# コピーしたいファイル/ディレクトリを明示
COPY_LIST=(
".envrc"
".env.development.local"
"node_modules"
"vendor/bundle"
)
for path in "${COPY_LIST[@]}"; do
if [ -e "$MAIN_WORKTREE/$path" ]; then
mkdir -p "$(dirname "$path")"
rsync -a "$MAIN_WORKTREE/$path" "./$(dirname "$path")/"
fi
done
echo "✓ setup complete"
設計のポイントは3つ。
- メインworktreeのパスは
git worktree list --porcelainの最初の行から引く - コピー対象はwhitelistで明示——全gitignoredを取り込むとビルドキャッシュまで巻き込む
-
rsync -aで再帰コピー——シンボリックリンクや権限の扱いがcp -Rより安定
これでworktree作成直後からdirenv allowが通り、yarn devも動く状態になります。Docker周りはまだ別の課題があるので、次へ進みます。
ポートの競合は、env化 + ハッシュで自動採番する
メインブランチでRailsがlocalhost:3002を握っている状態で、別ブランチのworktreeからdockerコンテナを立ち上げようとすると、ポート競合でエラーが出ます。
.envrcをコピーしただけでは解決せず、ブランチごとに別のポート番号を割り当てる必要があります。フロントのdevサーバーも、DBのホストポートも同様です。
まずdocker-compose.ymlのポート指定を環境変数ベースに書き換えます。
services:
api:
ports:
- ${API_PORT:-3002}:3000
front:
ports:
- ${FRONT_PORT:-8001}:8000
db:
ports:
- ${DB_PORT:-5433}:5432
:-3002のデフォルト値があるので、メインブランチで.envrcが未設定でも今まで通り動きます。worktree側でAPI_PORT=3456を.envrcに書けば、そちらが優先されます。
問題はポート番号をどう割り当てるか。手動で連番を振る運用を試しましたが、
- ブランチを作るたびに次の番号を覚える必要がある
- 消したブランチのポートを回収するのが面倒
- 他worktreeと被っていないか毎回確認
と、並列数が増えるほど破綻しました。結局、ブランチ名のハッシュから自動計算することに落ち着いています。
HASH=$(echo "$BRANCH" | shasum | cut -c1-4)
OFFSET=$((16#$HASH % 900))
API_PORT=$((3100 + OFFSET))
FRONT_PORT=$((8100 + OFFSET))
DB_PORT=$((5600 + OFFSET))
shasumの上4桁を16進から10進に直して900で割った余りを、3100〜3999 / 8100〜8999 / 5600〜6499の枠に振り分けるだけです。同じブランチ名なら同じポートになるので、「さっきのブランチは何番だっけ」を思い出す必要がなくなります。
900枠を4〜5本同時に動かすと、奇跡的に同じになる確率が数%程度残ります。ぶつかったら.envrcを手で上書きすればよいと割り切って、今のところ実害は出ていません。
DBの競合は、COMPOSE_PROJECT_NAMEで物理的に分離する
これが一番厄介でした。
ブランチAでdb:migrateした直後にブランチBに切り替えると、ブランチBのRailsはブランチAのmigrationが乗ったDBを見にいきます。スキーマが想定と違うので、テストが落ちたり、レコードが想定外の構造で保存されたりと、地味な事故が起きる。
最初は「ブランチごとにpg_dumpして別DBにrestoreするしかないか」と思っていましたが、結論から言うと不要でした。Dockerの仕組みを使えば、DBの中身ごとブランチで分けられます。
まずDB名を.envrcで切り替えられるよう、database.ymlを書き換えます。
development:
primary:
database: <%= ENV.fetch('DB_NAME', 'api_development') %>
これでDB_NAME=api_my_featureを.envrcに書けば、ブランチごとの論理DBに接続するようになる、つもりでした。が、これだけでは解決しません。
docker-compose.ymlの末尾を見てみます。
volumes:
postgres-db:
driver: local
postgres-dbというvolumeを1つ宣言しているだけで、何の変哲もない指定です。しかし、Dockerが実際にどう扱っているかをdocker volume lsで覗くと、別の側面が見えてきます。
DRIVER VOLUME NAME
local my_project_postgres-db
Dockerはvolumeの実体名を<プロジェクト名>_<volume名>で作っています。プロジェクト名はデフォルトで「composeファイルがあるディレクトリ名」で決まるので、同じプロジェクト名で起動している限り、volumeも同じものを共有するということです。
DB名を.envrcで変えていても、実体のPostgreSQLデータが入っているvolumeは1つ。そこに複数の論理DBが同居している状態なので、あるブランチで流したmigrationの影響が別のブランチに残る事故が起きえます。
解決策はシンプル。COMPOSE_PROJECT_NAMEを.envrcでブランチごとに切り替えるだけです。
# .envrc
export COMPOSE_PROJECT_NAME=my_project_my_feature
これだけで、Docker側のvolume名が勝手に分岐します。
DRIVER VOLUME NAME
local my_project_postgres-db
local my_project_my_feature_postgres-db
local my_project_other_branch_postgres-db
ブランチごとに新しいvolumeが自動で生まれて、DBの中身が物理的に分かれます。migrationもseedも、全部ブランチローカルになります。
DB名を変えるだけで済まなかったのは、やってみるまで気づきにくい落とし穴でした。Docker Composeがproject名込みでvolumeを管理している、という仕様のおかげで、並列開発の体験がずいぶん変わりそうだな、と思いました。
全部まとめる /worktree-setup skill
ここまでの流れで、ブランチごとに必要な工程は4つです。
- ブランチ名からポートとDB名と
COMPOSE_PROJECT_NAMEを計算 -
.envrcに追記 direnv allow- DBコンテナを起動して
db:create db:migrate
毎回手でやるには工程が多いので、Codexのskillにまとめてあります。.envrcへの追記はマーカーで冪等化しています。
# 想定: PROJECT_NAME / API_PORT / FRONT_PORT / DB_PORT / DB_NAME はこの直前で計算済み
# worktree-env セクションが既にあれば消してから書き直す
if grep -q '# worktree-env' .envrc; then
sed -i '' '/# worktree-env start/,/# worktree-env end/d' .envrc
fi
cat >> .envrc <<EOF
# worktree-env start
export COMPOSE_PROJECT_NAME=${PROJECT_NAME}
export API_PORT=${API_PORT}
export FRONT_PORT=${FRONT_PORT}
export DB_PORT=${DB_PORT}
export DB_NAME=${DB_NAME}
# worktree-env end
EOF
# worktree-env startと# worktree-env endで囲んでおくことで、skillを何度流しても既存セクションだけが置き換わります。手書き部分と自動生成部分が共存できるので、.envrcにAPIキー等を書いている場合でも安心です。
skillの冒頭にはリポジトリ検証のステップも入れています。他のリポジトリで間違って走らせるとポート番号の割り振り前提が崩れるので、git remote get-urlでリポジトリを確認し、想定と違ったら即中断する、というものです。本来であればどのプロジェクトでも通用するように設計すべきですが、私がメインで触るコードがこの1つのプロジェクトだけなので、念のためのガードレールとして入れています。
4. 実運用フロー
ここまでの仕込みが終わると、新しいタスクを始めるときの手順がだいぶ短くなります。実際の作業フローを書いておきます。
スレッドをWorktreeモードで作る
Codexのサイドバーから新規スレッドを作り、モードにWorktreeを選択。ベースブランチ(main)と新しいブランチ名(例: feature/add-search-filter)を指定して開始します。
これだけで、Codexが裏でworktreeを切り、そのworktreeに紐付いたスレッドが起動します。
setupスクリプトが自動で走る
worktreeが作られた直後、.codex/local-environment/setup.shが自動実行されます。
メインworktreeから.envrc、node_modules、vendor/bundleなどがコピーされて、新しいworktreeも「最初から動く状態」になります。ここではユーザーが何かをする必要はありません。
/worktree-setup skillで初期化する
スレッドの最初のメッセージで/worktree-setupを打ちます。skillが以下を順にやってくれます。
- ブランチ名から
COMPOSE_PROJECT_NAME/ 各ポート / DB名を計算 -
.envrcの# worktree-envセクションを書き換え direnv allowdocker compose up -d dbbin/rails db:create db:migrate
ここまで完了すれば、このブランチ専用のDBと環境変数が揃った状態。docker compose upを打てば全コンテナが立ち上がります。
「この工程もスクリプト化してしまえば楽なのでは?」という見方もあるとは思いますが、以下の理由から分離させています。
- 必ずしもDBセットアップしなくてもよい作業がある
- PRレビューをブランチチェックアウトして行いたいとき、など
- 「コンテナ立ち上げる = コンテナのビルドが走る」なので、時間がかかる。そこまで待つのが面倒な場合が多い
AIに実装を任せる
あとは普通に実装タスクを投げるだけです。「issue #123の機能を実装して」でも「このバグの原因を調査して」でもよく、Codexはworktreeの中で自由にコードを書きます。
メインブランチで動いているdocker compose upには影響しないので、別のスレッドで他のタスクを並行させても、ポートもDBもコンテナも全部独立しています。私の場合は頭のキャパも考えて同時に走らせる上限を3本にしていますが、マシンリソースと脳内リソースが許せばもっと走らせられます。
片付ける
スレッドの作業が終わったら、/worktree-cleanup skillを叩きます。skillは順番にこれをやります。
- 現在のブランチがmain / masterでないことを確認。念のための措置です
-
docker compose down -vでコンテナとvolumeを一括で落とす - メインworktreeに戻る
-
git worktree removeとgit branch -Dでworktreeとブランチを削除
中身の主要部分はこんな感じです。
# main / master では実行しない(ガード)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
echo "Cannot run on main/master"
exit 1
fi
WORKTREE_PATH=$(pwd)
MAIN_WORKTREE=$(git worktree list --porcelain | awk '/^worktree /{print $2; exit}')
# コンテナと volume を一括で落とす
docker compose down -v
# メイン worktree に戻ってから worktree とブランチを削除
cd "$MAIN_WORKTREE"
git worktree remove "$WORKTREE_PATH" --force
git branch -D "$BRANCH"
docker compose down -vの-vが地味に重要で、これでブランチ専用に作ったvolumeも一緒に削除されます。COMPOSE_PROJECT_NAMEで分けていたおかげで、他のブランチのvolumeは無傷です。
間違えてmainで叩いたらmainのvolumeを吹き飛ばしてしまうので、最初にgit rev-parseでブランチ名を確認して、main / masterなら即中断するようにしています。
慣れるとここまでが5分くらいで終わります。スレッドを作る → skillを叩く → 実装を任せる → 片付ける、という流れに乗ってしまえば、3本並列で動かしても認知負荷はそれほど上がりません。
5. トレードオフ
以下に、この方式のデメリットも書いておきます。
DB volumeが増え続ける
COMPOSE_PROJECT_NAMEでブランチごとにvolumeを分けているので、片付けを忘れるとブランチを切るたびにvolumeが増えていきます。
/worktree-cleanupを叩いていれば問題ないのですが、worktreeを消し忘れたまま月をまたぐと、docker volume lsで「これ何のvolumeだっけ」というやつが大量に並びます。
消えたworktreeに紐付くvolumeを自動で見つけて削除するスクリプトを書けば一括掃除できますが、私は週末にdocker volume lsを眺めて、明らかに古いものをdocker volume rmで消す、くらいで済ませています。
ポートがたまに衝突する
ハッシュで自動採番している都合、4〜5本同時に動かすと数%の確率で同じポートにぶつかります。
実害は今のところ出ていなくて、ぶつかったら.envrcを手で上書きするだけです。しかし頻繁に並列数が増える人は、衝突検知して別のオフセットに退避する処理をskillに足すのもアリだと思います。
CodexのWorktreeモードに依存してしまう
Codexが作るworktreeのパスはCodex管理になるので、他のターミナルやIDEから触りに行くときに少し迷います。実際のパスはCodexのUIからコピーできるので致命的ではないですが、ターミナルメインで作業する人だと違和感はあるかもしれません。
あとはHandoffでLocal ↔ Worktreeを行き来する機能、最初は挙動が読めませんでした。gitの状態を裏で動かしてくれるので便利なのですが、初見だと「今どっちにいるんだっけ」となります。
マシンリソース
並列数を増やすほど当然リソースは食います。Rails / Next.js / Postgresを3セット同時に立ち上げると、メモリが20GB近くまで埋まることもあります。
メモリに余裕がない環境では、並列で動かすのは2本までくらいに抑えるのが現実的です。Codex自体の認知負荷も並列数に比例するので、無理に増やしても効率は落ちます。
6. まとめ
最初は「CodexのWorktreeモードすごいな」から始まった取り組みでした。tmuxのセッション分割やgit worktree addの作業が消えるだけで、複数ブランチを同時にAIに任せる感覚がガラッと変わったからです。
ただ実際に並列で動かしてみると、Codexの手が届かない領域がいくつかありました。gitignoredファイルが空で生まれること、ブランチごとに環境変数を変える必要があること、そして一番厄介なDBの共有問題など。こういった基盤周りのところを触るのにはあまり慣れていないので一度諦めかけましたが、AIと壁打ちして「COMPOSE_PROJECT_NAMEでvolumeの名前空間を分けるだけで済む」と気づき、自分の求める形での解決につながりました。
そこからsetupスクリプト1本とskill 2つを足して、今の構成に落ち着いています。合計100行ちょっとで、ライブラリに依存していないのでメンテはほぼゼロです。
CodexはAI並列開発の道具として、現時点で一番尖っているもののひとつかな、と思っています。Worktreeモードのいいところを活かしつつ、足りないところは自分で100行書く。これくらいの労力でAIコーディングの利便性が向上するなら、十分元が取れる投資だと思います。みなさんもAIコーディングツール、いい感じに使い倒していきましょう。