Tech BlogAWSツール & 技術ブログ

cmux再起動でClaude Codeのセッションが自動復元されない。壊れた案内文の正体と一括復元スクリプト

はじめに

Claude Codeを複数のペインで並行して走らせる運用をしていると、Macの再起動やターミナルアプリの再起動のたびに「あのセッション、どのペインで何をやっていたか」を復元する作業が発生します。macOS向けのターミナルアプリ cmux は、再起動時にClaude Codeのセッションを自動で復元してくれる機能を持っています。

ところがある日、Macを再起動してcmuxが自動起動すると、ほとんどのペインでセッションが戻らず、見慣れない文字列がプロンプトに残っていました。この記事は、その原因を調べて一括復元できるようにした記録です。

  • 症状と、壊れて見える案内文の正体
  • 自動復元が失敗する原因(ソースを読んで確認したもの)
  • 手作業で全ペインを復元する手順と、それをスクリプト化した際の判定ロジック
  • 実際にハマったポイントと、再発を減らす運用

なお、ここに書いた挙動は cmux 0.64.22〜0.64.25(2026年9月時点)で確認したものです。バージョンが上がれば変わる可能性があります。

症状

cmuxを再起動すると、各ターミナルに次のような長い文字列が流れ込み、zshのプロンプトに入力途中のまま残ります。

/usr/bin/printf '%s\n' "$(printf '\343\201\223...

八進エスケープなので一見ただの文字化けですが、デコードすると中身は日本語の案内文でした。

このエージェントセッションがすでに実行中かどうかを cmux で確認できなかったため、セッションを自動的に再開しませんでした。このターミナルで再開するには『cmux restore --surface』を実行してください。

つまり、cmuxは「復元していいか確認できなかったので、手動で復元してください」と伝えようとしていたのです。案内の中身は正しく、壊れているのは表示だけでした。

表示が壊れる理由

案内文が切れて見える原因は、非ASCII文字(日本語)を $(printf '\343...') という八進エスケープに変換してシェルへ流し込む実装にあります。コマンドが1,024バイトを超えると、シェルが起動する前に投入された入力がttyのcanonicalモードの行長上限で切り詰められ、さらに二重にエコーされます。これは表示の問題で、復元そのものの失敗とは別の話です。

原因:エージェント確認スキャンの10秒タイムアウト

自動復元が走らなかった本当の理由は、cmux 0.64.25のソースを読んで確認できました。

  1. 自動復元は、保存済みのセッションが別プロセスで既に動いていないかを確かめるライブエージェントスキャンの完了を待ってから実行される
  2. このスキャンの期限は10秒固定で、設定では変えられない
  3. 期限内に終わらないとタイムアウト扱いになり、復元の代わりに「確認できなかった」案内文を全ペインへ流し込む
cmux起動時の自動復元フロー cmux起動 保存済みセッションあり ライブエージェント スキャン(上限10秒) 期限内に完了 → 自動復元 タイムアウト → 案内文を 全ペインへ流し込む 図1: 負荷が高いとスキャンが10秒に間に合わず、右下の経路に入る

今回は、Macの再起動直後にcmuxが自動起動し、十数個のエージェントペインを一斉に復元しようとしていました。ターミナル生成の処理でメインスレッドが数秒止まり(cmuxの ~/Library/Logs/cmux/hangs/ にログが残ります)、再起動直後のログイン項目の負荷も重なって、スキャンが10秒に間に合わなかったと見ています。

再発しやすい条件は次の2つが重なったときです。

  • 復元するエージェントペインが多い(体感で10個を超えるあたり)
  • 起動直後でマシンの負荷が高い(Mac再起動後は特に起きやすい)

cmuxだけをCmd+Qで終了して起動し直す場合は負荷が低いので起きにくいものの、ペイン数が多ければ起こりえます。

手作業で全ペインを復元する

各ペイン(cmuxの用語では surface)には、cmux surface resume show で確認できる復元レコードが残っています。中身は claude --resume <session-id> --permission-mode auto と作業ディレクトリです。ペインのシェルで cmux restore --surface を実行すると、このレコードの内容でプロセスが置き換わります。これを外側から cmux send で全ペインに送れば、まとめて復元できます。

export CMUX_QUIET=1
cmux tree --all                                  # surface の一覧と状態を把握
cmux surface resume show --surface surface:2     # 復元レコードを確認
for i in $(seq 2 13); do                         # 範囲は tree の結果に合わせる
  cmux send-key --surface surface:$i ctrl+c      # 壊れた案内文の行をクリア
  cmux send --surface surface:$i "cmux restore --surface\n"
done
pgrep -fl 'claude.*--resume' | grep -oE -- '--resume [0-9a-f-]+'   # 復元済みのIDを確認

先に Ctrl+C を送るのは、プロンプトに残っている壊れた案内文の行をクリアしないと、その続きに cmux restore が連結されてしまうためです。

そもそも cmux コマンドが見つからない場合

この手順の前提は cmux コマンドがPATHにあることですが、アプリを入れただけでは通っていないことがあります。cmuxはClaude CodeやCodexの起動時に一時ディレクトリをPATHの先頭に差し込みますが、そこに置かれるのは claude と codex のラッパーだけで、cmux 本体は含まれません。そのため zsh: command not found: cmux になります。

本体はアプリバンドルの中にあるので、PATH済みのディレクトリへシンボリックリンクを張れば解決します。sudoは不要です。

ln -sfn /Applications/cmux.app/Contents/Resources/bin/cmux ~/.local/bin/cmux
cmux version   # バージョンが出ればOK
cmux ping      # アプリと通信できれば PONG

リンクなのでアプリを更新しても追従します。

ハマりポイント

一括復元を実際にやってみて、手順に書いておかないと踏む点がいくつかありました。

1. cmux restore はClaude CodeのBashツールから実行しない

cmux restore は「呼び出し元のCLIプロセスを置き換える」動作です。Claude Codeのセッションの中からBashツールで実行すると、自分自身のプロセスを差し替えることになってしまいます。必ず対象ペインのシェルに cmux send で流してください。

2. 表示されていないワークスペースでも send は届く

別のワークスペースにあって画面に出ていないペインは、状態確認で in_window=false になり、read-screen が失敗します。ただし send は「queued」として受け付けられ、ターミナルの準備ができた時点で送信されるので、そのまま復元されます。画面が読めないことを理由にスキップする必要はありません。

3. 終了済みのペインは respawn すると復元レコードが消える

シェルのプロセスが終了済み(process_exited)のペインには、cmux respawn-pane でシェルを作り直します。ところが作り直した時点で復元レコードも消えます。先にレコードの内容を控えておき、claude --resume <session-id> --permission-mode auto を直接 send する必要があります。順序を逆にすると、どのセッションに戻すべきか分からなくなります。

4. 会話が1件もないセッションは復元できない

起動しただけで何も話していないセッションは、No conversation found で復元できません。これはClaude Code側の仕様です。

スクリプト化:判定ロジック

手順を毎回打つのは面倒なので、ワンコマンドのスクリプトにしました。cmux内の任意のターミナルで実行する前提です。

cmux-restore-all --dry-run   # 復元対象の確認だけ(何も送信しない)
cmux-restore-all             # 復元を実行し、8秒後に起動確認(--wait N で変更)

肝は「どのペインに復元を送るか」の判定です。全ペインに送ると、元気に動いているセッションにまで Ctrl+C を送ってしまいます。そこで、ペインごとに次の順で判定しています。

  1. 実行しているペイン自身ならスキップする
  2. 復元レコードが無いペインはスキップする
  3. レコードからセッションIDを取り出し、そのIDを持つ --resume / --session-id 付きのプロセスが生きていればスキップする。cmuxが新規起動したセッションは引数にIDを持たないため、cmuxのフック台帳(~/.cmuxterm/claude-hook-sessions.json)に記録されたpidが claude として生きているかも確認する
  4. 画面の最終行がシェルプロンプトでなく、エージェントの対話UIらしき表示があればスキップする
  5. 残りに Ctrl+C → cmux restore --surface を送る。送信結果が process_exited なら respawn してから、レコードから組み立てた claude --resume を送る

セッションIDの抽出は、レコードに混じるクォートを取り除いてから正規表現で拾っています。

uuid_after() {
    # $1: テキスト, $2: 直前の語(例: --resume)。最初に見つかった UUID を返す
    printf '%s' "$1" | tr -d "'\"\\\\" | grep -oE -- "$2 +[0-9a-fA-F-]{36}" | head -1 | awk '{print $2}'
}

ループの書き方にも罠がありました。while read の中で cmux コマンドを呼ぶと、そのコマンドが標準入力を読み尽くして1件目でループが終わってしまいます。ペインの一覧は別のファイルディスクリプタ(fd 3)から読み、ループ内のcmux呼び出しには </dev/null を付けて回避しています。

while IFS=$'\t' read -r -u 3 win ws s_ref s_uuid here title; do
    record="$(cmux surface resume show --surface "$s_ref" 2>&1 </dev/null)"
    # ...判定と送信...
done 3< "$SURFACES_FILE"

自分の環境で確認したこと

スクリプトは、実際にクラッシュを再現して動作を確認しました。Claude Codeを動かしたペインのプロセスを kill -9 で落とし、スクリプトを実行して、復元と起動確認([OK] 表示)まで通ることを確かめています。--dry-run を付けると、何も送信せず復元対象だけを一覧できます。

また、公開されている開発中のmainブランチには、復元の再試行(admission retry)の処理が入っているのをソースで見かけました。リリース版にいつ入るかは確認できていないので、当面は次の運用で凌いでいます。

  • 終わったセッションのワークスペースをこまめに閉じ、同時に復元するペイン数を減らす
  • 再発したらスクリプトで戻す(ペイン数が多くても短時間で済む)

まとめ

  • 再起動後に壊れた八進エスケープの案内文が出たら、中身は「確認できなかったので手動で復元して」という正常な案内だった
  • 原因はライブエージェントスキャンの10秒固定タイムアウト。復元するペインが多く、起動直後で負荷が高いと起きやすい
  • cmux send で全ペインに cmux restore --surface を送れば一括復元できる。Claude CodeのBashツールから直接 cmux restore を実行してはいけない
  • 全ペインへ盲目的に送らず、稼働中・復元レコード無し・自分自身を判定で除外するとスクリプト化しても安全
  • respawn はレコードを消すので、レコードを先に控えてから行う

エージェントを多数並行させる運用では、「再起動後にどう戻すか」までを手順にしておくと、再起動への心理的なコストがかなり下がります。