背景
困っていたのはシンプルで、私は MacBook を 2 台持っていて(A と B)、両方で Claude Code と Codex を仕事や Side Project で日常的に使っています。この 2 台の間をシームレスに行き来して、いつでも前のセッションを resume したい、というのが希望でした。
実際の会話履歴はサービス側に保存されていて、インデックスにあたるセッションファイルがローカルに置かれている——つまり、2 台間でセッションファイルが同期できれば、どちらの Mac からでも resume して中断した場所から続けられるはず。まずは同期の問題を解決する、というのが一段目の考え方です。
二つ目の悩みは、claude --resume や codex --resume で表示される一覧が、それぞれ一行程度のテキストしかなく、どれがどのセッションだったのか目視ではほぼ判別できないこと。これが検索層の問題です。
なぜクラウド版を直接使わないのか
まず正直に認めておくと、これは完全にクラウドで解ける問題です。Claude.ai/code、Cursor のクラウド chat 同期、GitHub Codespaces 上で Claude Code を動かす、あるいは Devin / Replit Agent / Lovable / v0 のような完全クラウド sandbox——どれもこの記事より短い道筋です。
それでもローカル CLI を選んでいるのは、以下の個人的なこだわりがあるからです:
- Claude Code と Codex の両方を同時に立ち上げて切り替えながら使いたい。クラウド方式だとどれか 1 社にロックインされがち
- 個人プロジェクトが Pi 上の docker daemon、ssh key、
.envrcなどローカル環境に依存していて、クラウドの sandbox では一から再構築する必要がある - Homelab が好きで、自分の手で問題を解決するのが好き
なので、この記事は「ローカル CLI 派」の方を想定して書いています。
まずは各ツールの設定ファイル構造
~/.claude/projects/ 配下は cwd をエンコードしたディレクトリ構造で、その中にその cwd で発生したセッションの jsonl ファイルが入っています:
1 -Users-cheerchen-Documents-CheerChen-session-sync/
2 5a3f8b2c-...jsonl
3 8d1e4f7a-...jsonl
4 -Users-cheerchen-Documents-CheerChen-other-project/
5 1c2d3e4f-...jsonl
~/.codex/sessions/ は日付バケットでディレクトリ分けされていて、その日付下に発生したセッションの jsonl ファイルが入っています:
1 2026/
2 06/
3 19/
4 5a3f8b2c-...jsonl
5 8d1e4f7a-...jsonl
6 2026/
7 06/
8 18/
9 1c2d3e4f-...jsonl
Claude のセッションはパスに紐づき、Codex のセッションはパスと無関係。Claude のセッションは追記書き込み、Codex のセッションは一括書き込みという違いがあります。構造を理解した上で、有効な同期方法とセッションを特定する方法を探していきました。
以下、2 つの層に分けて紹介します——同期層は Syncthing、検索層は自作の小さなツール、その名も session-index-viewer です。
同期層の方針
Syncthing で 2 台の Mac の ~/.claude/projects/ と ~/.codex/sessions/ を、家にある Raspberry Pi(以下 Pi)経由で相互に同期します。
トポロジーはスター型:A ↔ Pi ↔ B、A と B は直接ペアリングしません。
なぜ A↔B 直接ではないか?
MacBook はどちらも閉じたりスリープしたりするので、2 台が同時にオンラインになる時間帯がそもそも短いんです。一方で Pi は常時稼働なので、常に起きている中継ノードとして機能してくれます。A/B のどちらか起きている側が Pi と同期し、オフライン側の状態は Pi が保持。これで A と B が互いに待ち続ける状態を回避できます。
Pi の役割:Read-Only 中継
Pi 上の Syncthing は Receive Only に設定します——A と B からの更新を受け取るだけで、「Pi 側のローカル変更」を逆に push することはしません。
加えて Staggered File Versioning(デフォルト 30 日保持)を有効にしておくと、A や B から来た削除や上書きはすべて Pi 上の .stversions/ に履歴として残ります。
Pi 上の compose
Pi 上のサービスはすべて /opt/stacks/<name>/ 配下に統一していて、dockge で管理しています。Syncthing も同じパターン:
1# /opt/stacks/syncthing/compose.yaml
2services:
3 syncthing:
4 image: syncthing/syncthing:latest
5 container_name: syncthing
6 hostname: pi-relay
7 restart: unless-stopped
8 # host mode is required: LAN discovery (21027/udp multicast) and
9 # direct LAN sync (22000) don't work cleanly through bridge NAT.
10 network_mode: host
11 environment:
12 - PUID=1000
13 - PGID=1000
14 # Pin version via image tag; prevent self-upgrade inside container.
15 - STNOUPGRADE=1
16 volumes:
17 - ./config:/var/syncthing/config
18 - ./data/claude-projects:/var/syncthing/claude-projects
19 - ./data/codex-sessions:/var/syncthing/codex-sessions
network_mode: host は必須です:Syncthing の LAN ディスカバリは 21027/udp のマルチキャスト、直接同期は 22000 を使います。
Pi 起動後、GUI は http://<pi-ip>:8384。claude-projects と codex-sessions の 2 つの folder を作って、両方とも Receive Only + Staggered Versioning(30 日) に設定します。
A 側(B も同様)
1brew install syncthing
2brew services start syncthing
http://127.0.0.1:8384 を開いて、Pi の device ID を Remote Devices に追加。2 つの folder は両方 Send & Receive にして、パスはそれぞれ ~/.claude/projects と ~/.codex/sessions に向けます。
いくつかのポイント
~/.claude/全体を Syncthing に入れてはいけません。配下のprojects/だけを選びます。
~/.claude/ には他に以下のようなディレクトリがあります:
statsig/—— SDK のステートキャッシュ、Claude Code を起動するたびに更新されるshell-snapshots/—— セッションごとに生成されるtodos/—— 内部の task ステートファイルcache/—— 文字どおりキャッシュ
これらの共通点は:高頻度の小さいファイル、ローカル限定のランタイムステート、機器間で共有する意味がない、ということです。~/.claude/ 全体を同期してしまうと、Syncthing がこれらのディレクトリに振り回されることになります。
- Claude Code / Codex のパスエンコーディング
~/.claude/projects/ 配下のサブディレクトリ名は、カレントワーキングディレクトリの絶対パスを - でエンコードしたものです:
1~/Documents/CheerChen/session-sync
2 ↓
3-Users-cheerchen-Documents-CheerChen-session-sync
つまり「実行時のディレクトリ」でセッションのストレージが切り分けられている、ということです。このルールがあるため、同期しても、同じ repo で Claude Code を開くと、セッションファイルはデフォルトでは互いに見えません(All タブに切り替える必要があります)。
私の場合、2 台の Mac のユーザー名が一致していない——「セッションファイルはパス単位で同期されるが、プロジェクトの意味単位ではない」という結果を受け入れて、別の方法で機器をまたいだ resume の問題を解決します。それが後ほど紹介する session-index-viewer です。
Codex の ~/.codex/sessions/ 配下は YYYY/MM/DD/<UUID>.jsonl、日付バケット + UUID ファイル名で、カレントワーキングディレクトリとは無関係です。ただし実際に確かめてみると、Codex のセッションにも cwd 情報は含まれていて、各 jsonl の先頭行に書かれています。
検索層の方針
ここまでで、データ同期の問題は解決しました。でも検索の問題はまだ残っています。
セッションをたくさん持っている人なら分かると思いますが、claude --resume で出てくる一覧には有効な情報がほとんどありません:
1 ❯ Complete three tickets and prepare staging masking
2 2 weeks ago · develop · 676.6KB
3
4 commit & push
5 2 weeks ago · feature/INFRA-999 · 269.4KB · xxx/xxx-masking-platform#93
6
7 Review xxx-masking-platform PR #63 feedback
8 2 weeks ago · feature/INFRA-999 · 819.9KB · xxx/xxx-masking-platform#63
9
10 Review S3 lifecycle policy changes
11 2 weeks ago · feature/INFRA-999 · 99KB
12
13 ...
この一行表記では「バグ修正に使ったセッション」と「プレゼン資料を相談したセッション」を目視で見分けるのは無理です。
session-index-viewer はこの層のために書いた小さなツールです。リポジトリ:https://github.com/CheerChen/session-index-viewer (macOS only、stdlib Python、外部依存なし)。
何をしているか
~/.claude/projects/と~/.codex/sessions/の 2 ディレクトリをスキャン- 各セッションをパース:最初の prompt(あなたの最初の発話)+ 最後の返信(AI の最後の返答)をカード化
- 上部に source(Claude / Codex)とマシン名のフィルタ
- 各カードの右側にボタン:押すと新しい Terminal ウィンドウを開いて、自動で
cd <cwd> && claude --resume <id>を実行(Codex も同様)
server.py という単一ファイル、標準ライブラリの HTTP server がローカルポート 127.0.0.1:7333 で起動します。install.sh で launchd に登録して起動時に自動起動。設定後はメンテナンス不要です。
いくつかのディテール
- マシン名タグは cwd から推測している
各セッションファイルには当時の cwd が記録されています。viewer は cwd の /Users/<name>/ または /home/<name>/ からユーザー名を抜き出して、マシン名タグとしてカードに付けます。
- 機器をまたいだ resume 時にパスを自動で書き換え
これはパスエンコーディングの罠とセットで使います。2 台のパスが一致していない(たいてい一致していない)場合、自動で現在のマシンのパスに合わせる——Open Terminal した後の cd がちゃんと走るように。
- セッションファイル自体が SOT
viewer は直接ファイルシステムをスキャンします。ファイルシステムを唯一の真実の源(SOT)として扱うことで、「いつインデックスをリビルドするか」という悩みを省いています。mtime キャッシュがあるので、アクセスはミリ秒単位で済みます。個人用のローカルツールとしてはこれで十分です。
運用してみての効果
A + Pi + B の 3 台すべて Up to Date、LAN 内で片方の保存が相手に見えるまで大体 15–20 秒(FSEvents → hash → push のリンクオーバーヘッド)。2 つの folder の現状の規模:
| folder | ファイル数 | 容量 |
|---|---|---|
~/.claude/projects/ |
249 | 106 MB |
~/.codex/sessions/ |
122 | 70 MB |
ローカルで viewer をインストールしたあとは、http://localhost:7333 で session-index-viewer を開き、会話で出てきそうなキーワードを検索すれば該当セッションが見つかります。カードをクリック → 会話復元、で完了です。
まとめ
実装して得られた知見をまとめておくと、まず問題を 2 つの層に分割しました:同期層 と 検索層、それぞれ Syncthing と session-index-viewer で解決しています。同期層の核心は Raspberry Pi を中継点に置き、A/B を直接ペアリングしないこと。検索層の核心はファイルシステムを SOT とし、最初の prompt + 最後の返信でカード表示することです。