Claude Code と並列でブログを書いていたら dev サーバーの取り合いになったので、git worktree で解決した
このブログの記事は、Claude Code に手伝ってもらいながら書いています。それも1本ずつではありません。「1タスク1セッション」— 課題や記事ごとに専用のセッションを立てる運用ルールにしているので、常時3〜4本が並列で進んでいる状態です。片方のセッションで技術課題を解決しながら「いったんドラフト版を作っておいて」と頼み、別のセッションでは書き上がった記事の公開作業をする。文脈が混ざらないこの運用は気に入っているのですが、困りごともついて回っていました。
- 記事をレビューしようとブラウザで localhost:4321 を開いたら、dev サーバーが止まっていて見られない
- 別の記事の公開作業をするために、レビュー用に立ち上げていた dev サーバーを止める必要がある
- AI が先に dev サーバーを立てていると、私が立てたものはポート 4322 に逃げてしまう。4321 をブラウザにお気に入り登録しているので、地味に不便
前提を整理しておきます。このブログは Astro という静的サイトジェネレーターで作っていて、記事の「公開作業」は、全ページを HTML に変換するビルド (npm run build) を実行し、その生成物を配信先 (Cloudflare) にアップロードする、という流れです。一方、執筆中の記事の「レビュー」は dev サーバー (npm run dev) の担当で、公開前のドラフトを含めて localhost:4321 でプレビューできます。つまり公開とレビューは別々の仕組みなのですが、この2つが裏で同じファイルを共有していました。図にするとこうです。
図の左がレビュー用の dev サーバー (ドラフト _drafts/ も表示できる)、右が公開用のビルド (ドラフトは含めない)、下の .astro/ が両者の共有キャッシュです。赤い矢印の「上書き」が何をどう壊すのかは、このあとの実験で特定していきます。なお、図の中の文字をコマンド名やパスだけにしてあるのは、ブラウザの自動翻訳が画像内の文字には効かないためです。説明は本文に置く方針にしました。
ひとつひとつは小さいのですが、「公開作業とレビューが互いを止め合う」構造がある限り、並列執筆が増えるほど頻発します。この記事は、原因を実験で特定して、git worktree で根本から解決するまでの記録です。実験と実装は Claude Code に任せ、私は困りごとの整理と要件の回答、動作確認を担当しました。
まず困りごとを言葉にする
作業に入る前に、まず私の困りごとを正確に把握してもらうことにして、Claude Code に要件のヒアリングをさせました。話しながら整理すると、譲れないものとそうでもないものが分かれてきます。
- 公開作業の最中に、別のセッションへ「ドラフト書いておいて」と頼んでも事故らないこと (必須)
- レビュー用のプレビューは止まらないのが理想。ただし一時的なら許容範囲
- レビューは localhost:4321 で見たい。厳密に固定でなくてもよいが、どこを見ればいいか分かること
- メモリ 8GB の PC なので、同時にどれくらい立ち上げて大丈夫かの目安が欲しい
「理想」と「許容範囲」を分けておいたのは正解でした。あとで対策の選択肢を絞るときの判断基準になります。
実験1: dev サーバーは2本同時に動かせるのか
まず「AI 用と私用で dev サーバーを2本立てたらどうなるか」を実験してもらいました。同じ作業ディレクトリでポートだけ変えて2本 (4321 と 4322) 起動します。
結果は「表示はどちらも正常」でした。トップページも記事もドラフトも両方で見られて、ファイルを編集すればホットリロードも両方に反映されます。ただ、ログには時々こんなエラーが出ていました。
[ERROR] [UnknownFilesystemError]
Caused by:
ENOENT: no such file or directory,
rename '.astro/data-store.json.tmp' -> '.astro/data-store.json'
2本の dev サーバーが同じ .astro/data-store.json (コンテンツの管理台帳のようなファイル) に、一時ファイル経由のリネームで書き込もうとして、片方が先にリネームした一時ファイルをもう片方が失う、という競合です。今回はエラーログだけで自己回復しましたが、「dev サーバー同士も内部では取り合いをしている」ことが分かりました。不要な2本目は立てないのが無難です。
実験2: dev 中に build すると何が壊れるのか
本命はこちらです。このブログでは以前から「dev サーバー起動中に本番ビルドをすると、dev 側の画像解決が断続的に壊れる」という現象があり、「dev を止めてからビルドする」という運用ルールで回避していました。公開のたびにレビューが中断される原因はこのルールです。今回、壊れる仕組みを特定するところまで実験してもらいました。
dev サーバーを起動したまま npm run build を実行すると、ビルド自体は成功します。ところが直後から、dev サーバーで見ていたドラフト記事が 500 エラーになりました。ヒーロー画像を持つドラフトではこんなエラーです。
[ERROR] [LocalImageUsedWrongly] `Image`'s and `getImage`'s `src` parameter
must be an imported image or an URL, it cannot be a string filepath.
Received `../../../assets/posts/gsc-duplicate-canonical/hero.png`.
原因はファイルの上書きでした。build は .astro/content-assets.mjs (画像のインポート対応表) と content-modules.mjs を書き出すのですが、本番ビルドはドラフト記事を対象外とするため、ドラフトの載っていない対応表で dev と共有のファイルを上書きしてしまいます。dev サーバーはその対応表を参照しているので、ドラフトの画像が「インポートされていないただの文字列」扱いになって落ちる、という流れです。
「断続的に壊れる」ように見えていた理由も分かりました。壊れた後にドラフトの mdx ファイルを編集 (あるいは touch) すると、そのファイルの分だけ台帳が再生成されて一時的に復活するのです。ただし画像の対応表が古いままのことがあり、その場合ヒーロー画像持ちのドラフトは 500 のまま。回復したりしなかったりするので、原因の見当がつくまでは不可解な挙動でした。確実に直すには「dev 停止 → .astro ディレクトリ削除 → dev 再起動」しかありません。
設定で回避できないかも調べてもらいましたが、dev サーバーがこのディレクトリを置く場所は Astro のソースコード上で new URL(".astro/", config.root) と固定されていて、設定項目がありません (content-layer.ts の getDataStoreFile がこの分岐です)。つまり同じ作業ディレクトリで dev と build を共存させる道はない、というのが実験の結論でした。
解決策: 公開作業を git worktree に隔離する
同じ場所で作業するから壊れるのなら、公開作業を別の場所でやればいい。ここで git worktree の出番です。
git worktree は Git の標準機能で、git worktree add <パス> を実行すると、同じリポジトリの作業コピーがもう1セット、指定したディレクトリに展開されます。追加インストールも外部サービスも不要、料金の話も出てきません (無料というより「Git そのもの」です)。クローンと違ってコミット履歴のデータベースは1つを共有するので、メインの作業ディレクトリでコミットした内容を、worktree 側からすぐチェックアウトできます。
デメリットとして把握しておくことは2つありました。1つは作業コピーの分だけディスクを使うこと (このブログの場合、チェックアウトと node_modules で合計 300MB 台。今のディスク事情では気になりませんでした)。もう1つは作業場所が2つになるので「いまどっちを触っているのか」と混乱しうることです。後者が嫌だったので、worktree は人間や執筆セッションが触る場所にはせず、公開スクリプト専用の裏方に限定することにしました。ちなみに worktree は1つのリポジトリにいくつでも追加できるので、今後まったく別の用途 (たとえば大がかりな改修の隔離) で使いたくなっても、この公開用と衝突することはありません。
その公開スクリプトはこう動きます。
- 常設 worktree (なければ自動作成) で、コミット済みの最新 main をチェックアウト
- 依存パッケージが変わっていたときだけ
npm ci(npm のキャッシュが効くので初回でも8秒でした) - worktree の中でビルドしてデプロイ
メインの作業ディレクトリの .astro には一切触れないので、公開作業が何度走ってもレビュー用の dev サーバーは無傷です。実際にデプロイを実行しながらドラフトページを監視する検証もして、エラーが1件も出ないことを確認しました。分離後の全体像はこうなります。
左がいつもの作業場所で、執筆セッションと dev サーバー (4321 が私のレビュー用、4322 以降が AI 用) はここだけで動きます。右が公開専用の worktree で、公開スクリプトがコミット済みの最新 main を取得してビルドし、Cloudflare へデプロイします。2つの作業コピーが共有するのは下のコミット履歴 (.git) だけで、問題の .astro/ はそれぞれが独立して持ちます。
思わぬ副次効果もありました。worktree が見るのは「コミット済みの main」だけなので、書きかけの未コミット変更がうっかり公開される事故が構造的になくなったのです。複数セッションで同じリポジトリを触っている身としては、これはかなり安心材料です。逆に「コミットし忘れた変更は公開されない」ことになるので、スクリプトは未コミットの公開対象ファイルがあると警告を出すようにしてあります。
あわせて、従来のビルドスクリプトをメインの作業ディレクトリで直接実行しようとするとブロックして worktree 方式へ誘導するガードも入れました。並列で動いている古いセッションが昔の手順のまま公開しようとしても、物理的に旧手順が通らないようにするためです。運用ルールは破られる前提で、仕組み側で止める方が確実です。
ポート設計: うっかりの着地点を安全な側にする
ポート問題にも手を入れました。方針は「localhost:4321 は私のレビュー専用。AI は使わない」です。
面白かったのは、この設計の穴を見つけたのが AI ではなく私だったことです。当初は「AI はポート 4322 以降を明示して使う」というルールだけだったのですが、「うっかりあなたが何かミスしたときに、4321 で立ち上げようとしちゃわない?」と聞いてみました。実際そのとおりで、素の npm run dev は既定ポートの 4321 を取ってしまいます。
対策として、dev サーバーの既定ポート自体を 4322 に変更しました。これで AI がポート指定を忘れて起動しても 4322 に着地し、4321 が奪われることはありません。私の 4321 は、デスクトップに作ったショートカット (ポートを明示して起動する) 専用になりました。ルールで縛るのではなく、うっかりしたときに落ちる先を安全な側に変えておく、という発想です。
あわせて Vite の strictPort を有効にして、「使用中のポートだと黙って隣の番号に逃げる」挙動もやめさせました。衝突したらエラーで止まって気づけます。
窓が一瞬で消えると、人は気づけない
ショートカットまわりでもう一つ。strictPort でエラー終了するようになった結果、「既に 4321 が動いているのにショートカットを2度押しすると、ターミナルの窓が一瞬開いてすぐ消える」という挙動になりました。安全ではあるものの、何が起きたか分かりません。私は問題発生時にはそのことを忘れている自信があったので、わかりやすい表示にしてもらいました。
✅ レビュー用 dev サーバーは既に起動中です。そのままブラウザで見られます:
http://localhost:4321/
(この窓は新しく何も起動していません。閉じてOKです)
Enter キーで閉じる...
起動済みなら上の案内を出して窓を残す。異常終了ならエラーと対処のヒントを出して窓を残す。自分で Ctrl+C した正常終了だけ静かに閉じる。この出し分けのロジックはショートカット本体ではなくリポジトリ内のシェルスクリプトに置いたので、今後表示を変えたくなってもショートカットの再作成やタスクバーのピン留め直しは不要です。
メモリはどれくらい使うのか
PC のメモリが 8GB (WSL への割り当ては 5.8GiB) なので、同時にどれだけ立ち上げられるかも実測してもらいました。
| 対象 | 実測 |
|---|---|
| astro dev 1本 | 約 240〜320MB |
| dev 2本並走 | 約 550MB |
| npm run build のピーク | 約 760MB |
再起動直後の状態から dev 2本と build を同時に動かしても、空きメモリは 2GiB 以上残りました。「レビュー用の dev 1本 + AI の一時的な dev 1本 + 公開ビルド」という最大構成でも余裕があります。とはいえ Claude Code のセッション自体もメモリを使うので、AI 側には「build や追加の dev を立てる前に空きメモリを確認して、少なければ申告する」というルールを足しておきました。
そもそも、最初からこうできなかったのか
解決してから、ひとつ疑問が残りました。「dev を止めてからビルドする」という運用ルールを決めた時点で、なぜ今回の構成にしておかなかったのか、です。
時系列を追うと、少し耳の痛い話でした。あの運用ルールができたのは今回の6日前、サイトの見た目を整える作業の中で dev サーバーが壊れる現象に初めて出会ったときです。そしてその時点で、並列執筆も、冒頭に挙げた困りごとも、すでに存在していました。公開作業のときはレビューを中断してもらう。dev の取り合いになったら譲る。そうやって運用でしのげてしまっていたので、「課題」として言語化されないまま日々が回っていたのです。issue として起票したのは、公開作業のためにレビュー用の dev をまた止めてもらった日 — つまり我慢の回数が閾値を超えた日でした。
AI は、聞かれた問題を最短で解決するのは得意です。でも、聞かれていない「我慢」までは拾ってくれません。私はというと、解決方法はきっとあるだろうと思いながら、優先度を下げて起票していませんでした。つまり、困りごとの存在は認識していたのに、AI との間でそれが「取り組むべき課題」として言語化される機会がないまま過ぎていた、というのが実際のところです。
ではどうすればよいか。いま思っているのは、システムの要件と仕様を明記して、随時更新していくという、ソフトウェア開発では当たり前のプロセスに近づけることです。「どういう要件に基づいて、いまの仕様 (挙動) になっているのか」が書いてあれば、要件を追加・変更したときに、仕様も考え直す必要があると分かります。現在の問題点 — 運用で我慢していること — も一緒にまとめておけば、それが要件として言語化される機会になります。今回の反省から、回避策やルールを書き残すときには「何という前提に依存しているか」を一言添え、私が運用で我慢している兆候に気づいたら課題化を提案してもらう方針を、Claude Code が毎セッション参照する共有メモリに追加してもらいました。
まとめ
- Astro の dev と build は
.astroディレクトリを共有していて、同じ作業ディレクトリでの共存は設定では回避できない - 公開作業を git worktree に隔離すれば、レビュー用 dev サーバーを止めずに公開できる。「コミット済みの内容だけが公開される」という安心もついてくる
- ポートやショートカットは、ルールで縛るより「うっかりの着地点を安全にする」設計に寄せる
- 回避策には「何という前提に依存した回避策か」を書き添え、運用で我慢していることは言語化して課題にする
- 一人 + AI の並列作業で起きる衝突は、チーム開発が先に解いている — 解決策は必要な分だけ輸入すればよい
これで、複数の記事を並列で書きながら、どれかの公開作業が走っても手元のレビューは止まらなくなりました。決めた運用は Claude Code が毎セッション読む指示ファイル (CLAUDE.md) に書いてあるので、私が覚えておくのは「レビューはいつもの 4321」だけです。
振り返ってみると、今回の取り合いは「AI ならでは」の問題ではありません。もし人間10人のチームで開発していたら、同じ問題はとっくに起きていて、とっくに解決されていたはずです。各メンバーは自分のマシンで dev サーバーを動かすので取り合いにならず、本番ビルドは CI (継続的インテグレーション) の専用環境が担い、リリース手順も整備されている。そう考えると、今回やったことは「ビルド専用の小さな CI 環境をローカルに1つ作った」ことに相当します。
私は開発プロジェクトに長くいて、CI やリリース管理が整った環境で仕事をしてきました (自分で構築する側ではありませんでしたが)。だからこの整理には納得しつつ、正直なところ、こうも思っています — チーム開発にその仕組みがあることは AI 自身がよく知っているのだから、最初に提案してくれてもよかったのでは、と。「一人 + AI」は実質チーム開発なのだから、チーム開発の定石を環境設計に持ち込む提案を、これからは AI 側に期待したいところです (前節の共有メモリへの追加は、その注文の第一歩でもあります)。
一方で、一人 + AI の開発が良いのは、チームの定石をフル装備で導入しなくてよいところです。合意も承認もいらないので、CI サーバーの代わりに worktree 1つ、リリース管理の代わりにスクリプト1本、と必要な分だけつまみ食いできます。AI との並列作業で衝突に出会ったら、「チーム開発ならこれをどう解いているか」を輸入してくるのが近道かもしれません。この記事がその一例として参考になればうれしいです。
参考リンク
公式・一次情報
- LocalImageUsedWrongly - Astro Docs — 今回 dev 側で発生していたエラーの公式リファレンス
- content-layer.ts (astro@5.18.1) — dev と build でデータストアの置き場所を切り替えている
getDataStoreFileの実装 - git-worktree - Git Documentation — 1つのリポジトリに複数の作業ツリーを持つ機能の公式ドキュメント
同じような課題に取り組まれた記事
- Claude Code × Git Worktree × Superset で実現する並列開発 (株式会社ライトコード) — Claude Code の並列開発でローカルサーバーのポートが競合する問題を、worktree ごとのポート自動割当で解決されています。同じように Claude Code で並行開発していて、git worktree に行き着いているのですね。コンテナを使っている点はうちと違いますが、worktree で分けるという考えは同じでした
- Claude Code × Git Worktree で実現する並列開発ワークフロー (Qiita) — PR レビュー待ちの空き時間を worktree の並列作業で埋める実体験記です。PR レビュー待ちという場面はうちの一人開発にはまだありませんが、今後開発が大きくなって並列作業が増えたり環境が変わったりしたときに参考になりそうで、頭にメモしておきました