Operations2026.10.04 · 6 min read

193 日ぶりの再起動で消える tmux 内の Claude Code セッション 8 本を、同じ名前・同じ会話で戻す

認証基盤・クラウドインフラ

自宅サーバーを 193 日ぶりに再起動することになった。問題は、detached の tmux の中で動いている Claude Code の Remote Control セッションが 8 本あったことだ。tmux はプロセスなので再起動で消える。消えた 8 本を、同じ名前・同じ会話で戻せるかを調べて、systemd の unit にした。

tmux は戻らないが、会話は戻る

nr code <repo> は、そのリポジトリで detached の tmux セッションを 1 つ立て、その中で claude --remote-control <名前> を起動する。スマホや claude.ai から続きができる。再起動で失うものを分けると、tmux と claude のプロセスは戻せないが、会話は transcript としてディスクにあり、--resume <会話 ID> で開き直せる。

最初に tmux-resurrect を考えたが、戻せるのはウィンドウ構成と cwd と起動コマンドまでで、会話は戻らない。会話が --resume で戻るなら、tmux 側の復元は要らない。使い捨ての会話を作って --remote-control と --resume を同時に付けて起動したところ、過去のやりとりが画面に出たうえで 「/remote-control is active」と表示された。両立する。

対応表は ~/.claude/sessions/ にあった

残る問題は、どの tmux セッションがどの会話 ID かだ。claude のプロセスの /proc/<pid>/fd を見ても transcript は開かれていないので、fd からは追えない。transcript の中にも Remote Control のセッション名は書かれていない。

紐付けは ~/.claude/sessions/<pid>.json にあった。tmux 欄が「セッション名:@ウィンドウ.%ペイン」の形で入っている。

~/.claude/sessions/<pid>.json
{
  "pid": 12345,
  "sessionId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "cwd": "/home/ops/git/app",
  "startedAt": 1791113668222,
  "version": "2.1.289",
  "kind": "interactive",
  "name": "app-…",
  "status": "busy",
  "updatedAt": 1791114431868,
  "tmux": "app-a1b2:@99.%99"
}

先頭を : で切れば tmux 名になり、sessionId と cwd も同じファイルにある。snapshot はこのディレクトリを走査し、tmux に実在して pid が生きている分だけを TSV に書く。

停止時に控え、起動時に戻す unit

最初に書いたのは、今ある 8 本の名前と会話 ID を直書きしたスクリプトだった。書き上げてから捨てた。次の再起動では別のセッションが動いていて、古いリストを復元してしまう。控えは停止の瞬間に取るしかないので、systemd の oneshot で ExecStop に snapshot、ExecStart に restore を置いた。

nishioka-code-sessions.service(直したあとの最終形)
[Service]
Type=oneshot
# 起動時に restore、停止(シャットダウン)時に snapshot を走らせるため active のまま残す
RemainAfterExit=yes
User=ops
Environment=HOME=/home/ops
ExecStart=/opt/minipc/scripts/code-sessions.sh restore
ExecStop=/opt/minipc/scripts/code-sessions.sh snapshot
# 一部のセッションだけ復元に失敗しても active に留め、シャットダウン時の snapshot を走らせる。
SuccessExitStatus=1
# restore が立てた tmux サーバーと claude をユニット停止で巻き込まない
KillMode=process

oneshot は ExecStart が終わると inactive に戻り、そのままだと ExecStop は走らない。RemainAfterExit=yes で active に留め、シャットダウン時の停止を snapshot の起点にする。もう 1 つが KillMode=process で、restore が立てた tmux サーバーは unit の cgroup に入るため、既定のままだと unit が止まった時点で tmux ごと消える。

01
systemctl stop (shutdown)ExecStop が snapshot を走らせ、tmux 名 / cwd / 会話 ID を TSV に控える。
02
reboottmux サーバーも claude も消える。残るのは控えと会話ログだけ。
03
ExecStart restore控えの行ごとに tmux new-session で同じ名前・同じ cwd・--resume 付きで起動する。
04
8 秒待って生存確認tmux は中のコマンドが即死しても成功を返すので、has-session で確かめる。
05
fallback死んでいたら --resume を外して同名で立て直す。
unit が再起動をまたいでやっていること

boot で 1 本だけ落ちた

再起動後の journal がこれだ。8 本のうち 7 本が ok、1 本が FAIL で、unit は exit 1 で failed になった。

journalctl -b -u nishioka-code-sessions(名前は伏せている)
Starting nishioka-code-sessions.service - nishioka code sessions (snapshot at shutdown, restore at boot)...
ok     web-a1b2 (…)
ok     web-c3d4 (…)
FAIL   web-e5f6 (…): 起動直後に終了した。手で実行して原因を見る: cd /home/ops/git/web && /home/ops/.local/bin/claude --dangerously-skip-permissions --remote-control web-e5f6 --resume xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ok     app-g7h8 (…)
ok     app-i9j0 (…)
ok     app-k1l2 (…)
ok     app-m3n4 (…)
ok     app-o5p6 (…)
失敗 1 件
nishioka-code-sessions.service: Main process exited, code=exited, status=1/FAILURE
nishioka-code-sessions.service: Failed with result 'exit-code'.
nishioka-code-sessions.service: Unit process 1076 (tmux: server) remains running after unit stopped.
nishioka-code-sessions.service: Unit process 1078 (claude) remains running after unit stopped.

末尾の 2 行は KillMode=process の実証でもある。unit が failed に落ちても、tmux サーバーと claude は残っている。

落ちた 1 本は、再起動前の調査では transcript が見つからなかった、いちばん古いセッションだった。再起動後に見ると同じパスにファイルがある。更新時刻は今回の boot より前、つまりシャットダウン中に古い claude が終了処理で書いたものだ。中身はメタデータの行だけで、会話の本文が 1 行もない。

空に近い transcript の各行の type
last-prompt      ['lastPrompt', 'leafUuid', 'sessionId', 'type']
ai-title         ['aiTitle', 'sessionId', 'type']
mode             ['mode', 'sessionId', 'type']
permission-mode  ['permissionMode', 'sessionId', 'type']
pr-link          ['prNumber', 'prRepository', 'prUrl', 'sessionId', 'timestamp', 'type']
bridge-session   ['bridgeSessionId', 'lastSequenceNum', 'ownerAccountUuid', 'ownerOrganizationUuid', 'sessionId', 'type']

restore 側は transcript を [[ -s … ]] で見ていた。サイズが 0 でないことは確かめたが、会話が入っていることは確かめていない。そのため --resume を付けて起動し、claude は起動直後に終了した。tmux は中のコマンドが即死しても new-session に成功を返すので、8 秒待ってから has-session で生存を見る仕組みが拾った。

「ファイルがある」と「中身がある」は別の条件だ。復元の前提にするなら、確かめるのは後者のほうだった。

直し方は、transcript の中身を判定することではなく、resume で死んだら同じ名前で --resume なしに立て直す fallback にした。会話が戻らなくても、スマホから見える名前と cwd は戻る。この boot の時点では fallback はまだ無く、原因を見てから足し、同じセッションで手で実行して「会話の復元に失敗したので新規で起動した」と出ることを確認した。

合わせて SuccessExitStatus=1 を unit に足した。部分失敗で failed に落ちると active に留まらず、次のシャットダウンで snapshot が走らないからだ。

まとめ

「再起動でセッションが消える」は、プロセスと状態を分けると半分は消えない問題になる。状態がディスクにあり、開き直す入口(ここでは --resume)があるなら、復元すべきはプロセスの形ではなく状態のほうだ。tmux を復元する道具を探すより、何が状態で、どこに紐付けがあるかを先に確かめたほうが早かった。

もう 1 つ。復元系は部分失敗を前提に組む。1 本が落ちたときに unit 全体を failed にすると、次の控えが取れなくなる。落ちたものは別経路で立て直し、失敗は記録に残して先へ進む。

Claude CodetmuxsystemdHome Server