← ブログに戻る

Claude Codeを3セッション並列で8時間動かしたら、2回コンテキストを上書きしあった

claudecodeaiharnessproductivity

午後にやりたいことが3つあって、ターミナルウィンドウも3つ開いていました。計算は単純です。Claude Codeを3セッション、それぞれ別のworktreeで起動して独立したブランチを進めれば、午後のスループットはおおよそ3倍になるはず。公式ドキュメントもこのパターンを推奨していて、デスクトップアプリは新規セッションごとに自動でworktreeを作る実装になっています。「安全な並列パターン」として紹介されているやり方そのものです。

8時間後、私の手元には壊れたメモリファイルが2つ、書いた覚えのないスキル説明が1つ、そして別のworktreeに既に存在していた作業を再生成するのに使ったトークン代として約47ドルの請求が残っていました。worktreeのセットアップは確かに安全でした。共有された状態は安全ではありませんでした。

この記事は、その8時間の記録です。何をセットアップして、いつ衝突が起き、何が上書きされ、いま私が並列セッション同士が互いを食い合わないために使っている3つの小さなパターンの話です。

安全に見えたセットアップ

同じリポジトリの別worktreeでClaude Codeを3セッション起動しました。ブランチは3つ、feat/voice-bufferfix/og-emitfeat/citation-tracker。どのブランチも同じソースファイルには触れない構成で、それは事前に2回確認しました。

# Terminal A
git worktree add ../wt-voice-buffer feat/voice-buffer
cd ../wt-voice-buffer && claude

# Terminal B
git worktree add ../wt-og-emit fix/og-emit
cd ../wt-og-emit && claude

# Terminal C
git worktree add ../wt-citations feat/citation-tracker
cd ../wt-citations && claude

各セッションが読み込むシステムコンテキストは同じです。リポジトリの CLAUDE.md、ユーザー階層の ~/.claude/CLAUDE.md~/.claude/skills/、そして ~/.claude/projects/<repo>/memory/ ディレクトリ。worktreeはgitのレイヤーでは独立していますが、それ以外は全部共有です。

この含意に気付いたのは、8時間目に壊れたメモリファイルを開いたあとでした。worktreeはソースコードを分離してくれます。Claudeの「脳」までは分離してくれません。

衝突1: 3時間42分後、スキルファイル

最初に壊れたのは、その日一度も自分で触っていないスキルファイルでした。

セッションAは音声バッファの修正中に「WebRTCバッファ用のスキルってあったかな」と自問しました。なかったので、~/.claude/skills/voice-buffer/SKILL.md に新しく書き出して作業を続けました。ほぼ同じ8分のウィンドウで、セッションCはcitation trackerを作りながら「ソース帰属パース用のスキルってあったかな」と自問しました。なかったので ~/.claude/skills/citation-source/SKILL.md に新しく書き出しました。

ここまでは衝突なしです。別ファイル、別トピック。公式ドキュメント的にも何も問題ありません。

衝突が起きたのは3つ目のファイル、~/.claude/skills/_index.md でした。両セッションとも、新スキルを登録するときにこのインデックスを更新する判断をしました。セッションAが先に更新。30秒後にセッションCが読み込んだのは、Aの書き込み「前」のバージョン。Cはそこに自分のスキルを追記して保存しました。Aが登録したvoice-bufferスキルの行は、インデックスから消えました。セッションAは知る由もなく、もう次の作業に進んでいました。

5時間目に、淡々と動いていたセッションB(OGタグ修正担当)に「いまスキルインデックスにvoice-bufferは入ってる?」と聞いて気付きました。「入っていません」との返答。確認したら本当にそうでした。Aが書いたスキルファイル本体はディスクにあるのに、そこを指すインデックスは消されていた。

これがロックなしの共有状態の見た目です。書き手2人、last-write-wins、警告なし、マージなし。

衝突2: 6時間18分後、メモリファイル

2つ目の衝突はもっと厄介でした。残しておきたかった作業を食われました。

~/.claude/projects/<repo>/memory/ は、セッションをまたいで残しておきたい小さなメモを置く場所として使っています。システムのコンポーネントマップを書いた architecture.md、文体の好みを書いた feedback.md、現在の優先事項を書いた project.md。どれもClaude自身がたまに書き加えます。ユーザーが「これ覚えておいて」と言ったとき、もしくはエージェント自身が「これは残す価値がある」と判断したとき。

6時間18分の時点で、セッションAが音声バッファ作業を終え「このバッファの不変条件、残しておこうかな」と自問しました。architecture.md を読み、節を追加して保存。6時間19分、セッションBがOG修正を終え「og

二重発火バグをgotchaとして記録しておこうかな」と自問しました。architecture.md を読みましたが、それはコンテキストにキャッシュされたAの書き込み「前」のバージョン。そこに自分の節を追加して保存しました。

Aが書いたvoice-bufferの不変条件は消えました。注意深くまとめた8分の作業が、まったく無関係だが正しい内容のmetaタグ節に置き換わって、跡形もない。

翌朝、たまたま「buffer invariant」でgrepして何も出てこなかったから気付きました。もし検索しなかったら、そのメモは今後のClaude Codeセッションにとって「存在しないも同然」になっていたはず。エージェントは「そういえばあの不変条件は」と思い出すきっかけすらありません。「兄弟プロセスがメモリファイルを静かに上書きしました」というエラーログはどこにも残らないので。

本当に壊れていたもの

worktreeはファイルシステムレベルの問題を解決します。2つのセッションが同じ src/voice/buffer.ts に書き込めば、gitコンフリクトが派手に出るので気付けるし、回復もできます。2つのセッションが同じ ~/.claude/skills/_index.md に書き込むと、静かな上書きが起きます。気付けないし、回復もできません。

具体的に何が壊れていたかと言うと、こうです。公式ガイドは「あるセッションでの編集が別セッションのファイルに触れることはない」と書いていて、worktreeレイヤーではこれは事実です。でも、ハーネスレイヤーではそうではありません。ハーネス(メモリ、スキル、フック、設定)はworktreeより1階層上の ~/.claude/ にあり、そこは並列セッションが調整なしに自由に書き込む場所です。

危険なファイルは3クラスあって、痛みが大きい順に並べるとこうなります。

  1. 設定ファイル (~/.claude/settings.json)。エージェントがここに書くことは少ないので衝突頻度は低いです。ただし書く瞬間(スキルが権限追加を要求するなど)は確実にlast-write-wins。
  2. スキルファイル (~/.claude/skills/)。中頻度。実際の発火点は個別のSKILL.mdではなく、インデックスや共有カタログのほう。
  3. メモリファイル (~/.claude/projects/<repo>/memory/)。一番痛い。エージェントがここに書くタイミングは、まさに「いま学んだことを残しておきたい」瞬間です。つまり、失いたくない作業がちょうど消える。

Anthropicの並列worktreeパターンはコード向けの設計です。ハーネスは1セッション同時稼働を前提に作られています。両方を同時にやるのは、利用者の側のバグです。

3つのClaude Codeセッションが3つのworktreeで動き、1つの ~/.claude/ ディレクトリを共有する図: 衝突点はスキルインデックスとメモリファイル。

47ドルの授業料

現金の損失は再作業ぶんです。メモリファイルの衝突のあと、セッションAが直前に導出したvoice-bufferの不変条件はどこにも残っていませんでした。翌朝、新しいセッションを立ち上げてバッファを拡張するよう頼んだら、同じ不変条件を、ほぼ同じ手順で、ゼロから再導出しました。40分ほどのトークン消費です。ダッシュボードを見たら、Sonnet 4.6のトークン代でだいたい47ドル。あと、少しだけ不機嫌な朝。

もちろん最初の導出にも料金は払っています。なので実際には「失った」というよりは「二度払った」が正確で、その二度目のほうが回避可能だった、という構図です。Brooks’s Lawには誰も引用しない脚注があります。「並列プロセスは互いのメモを上書きするので、同じ作業を2回支払うことになる」。

いま私が使っている3つのパターン

衝突の日のあと、3つだけ変えました。どれも小さい変更で、Anthropic側に何かを出してもらう必要はありません。

パターン1: セッションごとのメモリ名前空間。 共有された ~/.claude/projects/<repo>/memory/ ではなく、各並列セッションは ~/.claude/projects/<repo>/memory/<branch-name>/ 配下に書き込むようにします。worktreeごとの CLAUDE.md でエージェントに自分のサブディレクトリを指し示しておくだけ。セッション終了時に手動か簡単なスクリプトでメインの memory/ にマージします。衝突はファイル名の重複として表面化するので、派手に気付けるし回復できます。

<!-- worktreeごとの CLAUDE.md -->

## メモリ書き込み先

メモリファイルは
`~/.claude/projects/repo/memory/feat-voice-buffer/`
配下にのみ書き込むこと。
`~/.claude/projects/repo/memory/` 直下には書かないこと。

パターン2: 共有インデックスへのwrite lock。 名前空間で分離できないファイル(スキルインデックス、settings.json)には、エージェントの書き込みに flock 系のロックを噛ませます。エージェントは小さなラッパースクリプト経由で書き込みを行い、~/.claude/locks/skills-index.lock の排他ロックを取ってから対象ファイルに触ります。last-write-winsの構造そのものは変わりませんが、書き込みが直列化され、書き込み前の読み込みが整合した状態を見るようになります。ラッパーはシェルで20行程度。

#!/usr/bin/env bash
# ~/.claude/bin/locked-write.sh
target="$1"
lockfile="$HOME/.claude/locks/$(basename "$target").lock"
mkdir -p "$(dirname "$lockfile")"
exec 9>"$lockfile"
flock 9
cat > "$target"

パターン3: .claude/sessions/ 経由の調整。 動いている各セッションが ~/.claude/sessions/<pid>.json にハートビートファイルを書きます。ブランチ名、開始時刻、ハーネスレイヤーで触る予定のファイルを記録しておくイメージです。共有インデックスやメモリファイルに書き込む前に、sessions/ ディレクトリを兄弟プロセスの主張についてgrepします。重なる主張があれば待つかスキップする。3つの中で一番重いパターンで、私が一番使わないやつです。パターン1と2でほとんどの実衝突は捕まるので。

サブエージェントの並列レビューを使ったことがあれば形は同じだとわかるはずです。問題はモデルではありません。利用者が「ここにあるとは思っていなかった」統合レイヤーのほうです。サブエージェントは1セッション内で意見について衝突し、並列セッションはハーネスをまたいで状態について衝突します。

いま私が信じていること

並列Claude Codeセッションはタダではありません。マルチエージェントのコードレビューがタダでないのと同じ仕組みです。コストは姿を変えますが、ゼロにはなりません。並列セッションの場合、コストはハーネスディレクトリ内の静かな上書きとして現れます。開始から8時間後、2つ目のターミナルを開いたときには想像もしなかったファイルで。

公式ガイドの言い分はソースコードレイヤーでは正しいです。「あるセッションでの編集が別セッションのファイルに触れることはない」。ただし1階層手前で止まっています。~/.claude/ 配下の編集は、互いのファイルに触れることに躊躇がありません。スケジュールはlast-write-wins、エラーログはあとからgrepするとどこにもない。

この記事から1つだけ持ち帰るとしたら、こうです。2つ目のworktreeで2つ目のClaude Codeセッションを開く前に、10秒だけ立ち止まって考えてみてください。この2つのセッションはスキル、メモリ、設定を共有するのか、そしてどちらかが他方の書き込みを静かに食ったとして自分は困るのか。困るなら、今日のうちにパターン1を入れて、実際に衝突を踏んだ日にパターン2を足す。パターン3は5並列までスケールしたときに考えれば十分です(公式ドキュメントが「やめておけ」とやんわり書いてくる手前のあたり)。

並列セッションは今もまだ使っています。worktreeの境界が境界の全部だ、というふりをやめただけです。


この話をもっと詳しく: ハーネスレイヤー、Claude Codeセットアップを構成する6つのモジュール、共有状態の失敗モードについては、ハーネス・エンジニアリング で詳しく扱っています。ターミナルを3つ開くだけで終わらせず、Claude Codeを本気で運用したいエンジニア向けのフィールドガイドです。

このブログの関連記事: