cmux の再起動に自動で claude /resume するようにしてみた

cmux の再起動に自動で claude /resume するようにしてみた

cmux の標準機能でサポートしてくれるまでの気休め暫定対応として
2026.03.30

cmux を再度起動する際に、前回のClaude Codeのセッションを自動で再開させたい

こんにちは、のんピ(@non____97)です。

皆さんはcmux を再度起動する際に、前回のClaude Codeのセッションを自動で再開させたいなと思ったことはありますか? 私はあります。

cmuxは再度起動をする際に、終了前のワークスペースやレイアウト、セッションを自動的に復元してくれます。

しかし、Claude Codeのセッションについては自動で復元してくれません。

複数ワークスペース、複数タブでClaude Codeを使用している状況で cmux を再起動してしまった場合、人力で claude /resumeをしていくのは中々辛いところがあります。

ということで、Claude Code Hooksとシェルスクリプトで実装してみました。

仕組み

概要

やっていることを箇条書きで簡潔に説明すると、以下になります。

  • Cluade Code Hooksでcmuxのワークスペースとタブ情報を取得、保存
  • zsh起動時に cmux かつ マーカーファイルが存在していれば、マーカーファイルの情報に基づいて、claude --resume <セッションID>を実行

フロー図で表現すると以下のようになります。

この仕組みの関連ファイルは以下のとおりです。

ファイル 役割
~/.claude/settings.json SessionStart / SessionEnd hooks の登録
~/.claude/cmux_session_hook.sh hook スクリプト本体
~/.config/sheldon/plugins/cmux-claude-resume/ sheldon プラグイン
~/.config/sheldon/plugins.toml sheldon プラグイン管理
$cwd/.claude/cmux-tab-info/$session_id タブ情報キャッシュファイル
$cwd/.claude/cmux-session-resume/$session_id マーカーファイル

sheldonを使っているのは.zshrcを汚したくなかっただけです。

~/.config/sheldon/plugins/cmux-claude-resume/cmux-claude-resume.plugin.zshにスクリプトを配置して、読み込ませています。

~/.config/sheldon/plugins.toml
# `sheldon` configuration file
# ----------------------------
#
# You can modify this file directly or you can use one of the following
# `sheldon` commands which are provided to assist in editing the config file:
#
# - `sheldon add` to add a new plugin to the config file
# - `sheldon edit` to open up the config file in the default editor
# - `sheldon remove` to remove a plugin from the config file
#
# See the documentation for more https://github.com/rossmacarthur/sheldon#readme

shell = "zsh"

.
.
(中略)
.
.
[plugins.cmux-claude-resume]
local = "~/.config/sheldon/plugins/cmux-claude-resume"

Claude Code Hooks

Claude Code Hooksでは以下のようにセッション開始時と終了時にシェルスクリプトを呼び出しています。

{
.
.
(中略)
.
.
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/cmux_session_hook.sh"
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/cmux_session_hook.sh"
          }
        ]
      }
    ],
.
.
(中略)
.
.
  },
.
.
(中略)
.
.
}

セッション開始時にはワークスペース名やタブインデックスなどの情報を取得しています。

本当はセッション終了時に取得できれば非常に良かったのですが、残念ながら終了時にはcmux treeなどcmuxコマンドを叩くことはできませんでした。

叩いたとしてもERROR: Access denied — only processes started inside cmux can connectと、情報を取得できませんでした。

この影響で、Claude Code起動後にワークスペース名を変更すると、自動で resume はされません。

CMUX_SURFACE_IDとタブごとに割り当てられるUUIDで判定できれば一番簡単なのですが、cmux再起動前後で変化してしまうため、苦肉の策でこのような対応となっています。

機能のリクエストは行われているため期待して待ちましょう。

https://github.com/manaflow-ai/cmux/issues/480#issuecomment-4055089802

Claude Code Hooksで実行する実際のシェルスクリプトは以下のとおりです。

~/.claude/cmux_session_hook.sh
#!/bin/bash
#
# Claude Code SessionStart/SessionEnd hook for cmux session resume.
#
# cmux のセッション復元時に Claude Code を自動 resume するための hook スクリプト。
# settings.json の SessionStart / SessionEnd 両方に登録して使用する。
#
# ---- 動作概要 ----
#
# SessionStart:
#   cmux CLI から workspace name / tab index / tab name を取得し、
#   セッション ID ごとのキャッシュファイルに保存する。
#   SessionEnd 時には cmux CLI が使えないことが検証済みのため、ここで事前にキャッシュしておく。
#   session_source が "resume" の場合、対応するマーカーを削除する
#
# SessionEnd:
#   reason が "other" の場合にマーカーファイルを作成する。
#   "other" は cmux ⌘+Q または⌘+Wで発火する。
#   reason が "prompt_input_exit" の場合はキャッシュを削除する。
#
# ---- ファイル構成 ----
#
# キャッシュ: $cwd/.claude/cmux-tab-info/$session_id
#   SessionStart 時に作成。key=value 形式で workspace_name, tab_index, tab_name を保存。
#
# マーカー: $cwd/.claude/cmux-session-resume/$session_id
#   SessionEnd (reason: "other") 時に作成。キャッシュの内容をコピーしたもの。
#   zsh プラグインがこれを検出して resume を提案する。
#
# ---- マッチングに workspace name を使う理由 ----
#
# workspace ref (例: "workspace:1") はワークスペースの追加/削除や、Claude Codeの通知によって順番が入れ替わり、再起動後の番号がずれるため不安定。
# workspace name (例: "workspace") はユーザーが明示的にリネームしない限り変わらないため、
# マッチングの識別子として適している。
#
# 既知の制限: セッション中にワークスペース名をリネームした場合、
# キャッシュが古い名前のままとなりマッチングに失敗する。
#
# ---- stdout に関する注意 ----
#
# Claude Code hooks の仕様上、stdout への出力は Claude のコンテキストに注入される。
# コンテキスト消費を避けるため、このスクリプトは stdout に一切出力しない。
# ファイル操作はリダイレクト (> file) で行い、stdout を経由しない。

# python3 の JSON パース用の共通プレフィックス。
# macOS には jq がインストールされていないため python3 を使用。
readonly _EXTRACT_PY='import json,sys; print(json.load(sys.stdin)'

#######################################
# cmux list-workspaces から現在のワークスペース名を取得する。
# Globals:
#   None
# Arguments:
#   None
# Outputs:
#   Workspace name to STDOUT.
# Returns:
#   0 on success, 1 if unavailable.
#######################################
get_workspace_name() {
  cmux list-workspaces 2>/dev/null \
    | grep '\[selected\]' \
    | sed 's/^\* workspace:[0-9]*  //' \
    | sed 's/  \[selected\]//'
}

#######################################
# cmux tree から呼び出し元サーフェスのタブインデックスとタブ名を取得する。
#
# cmux tree の出力で surface 行を順番にカウントし、
# 指定された surface ref に一致する行のインデックスを返す。
# このインデックスは GUI 上のタブの並び順に対応しており、
# cmux 復元後も安定している (surface ref 自体は変わる)。
# Globals:
#   None
# Arguments:
#   $1 - workspace ref (e.g. "workspace:1")
#   $2 - surface ref (e.g. "surface:4")
# Outputs:
#   "tab_index tab_name" to STDOUT (space separated).
# Returns:
#   0 on success, 1 if not found.
#######################################
get_tab_info() {
  local ws_ref="$1"
  local surface_ref="$2"
  # cmux tree の surface 行を順にカウントし、target に一致する行のインデックスとタブ名を抽出。
  # macOS 標準の awk (非 GNU) を使用するため、
  # 正規表現キャプチャグループは使わず index() でダブルクォート内の文字列を手動抽出する。
  cmux tree --workspace "${ws_ref}" 2>/dev/null \
    | grep 'surface ' \
    | awk -v target="${surface_ref} " '{
      idx++
      if ($0 ~ target) {
        s = $0
        i = index(s, "\"")
        if (i > 0) {
          s = substr(s, i+1)
          j = index(s, "\"")
          if (j > 0) name = substr(s, 1, j-1)
        }
        print idx " " name
        exit
      }
    }'
}

#######################################
# stdin JSON を解析し、SessionStart/SessionEnd に応じて
# キャッシュ/マーカーファイルの作成/削除を行う。
# Globals:
#   CMUX_WORKSPACE_ID
#   _EXTRACT_PY
# Arguments:
#   None
# Outputs:
#   None (stdout への出力はコンテキスト消費を避けるため行わない)
# Returns:
#   0
#######################################
main() {
  # cmux 外では何もしない (環境変数が未設定)
  if [[ -z "${CMUX_WORKSPACE_ID:-}" ]]; then
    return 0
  fi

  # stdin から hook input JSON を全て読み取る。
  local input
  input="$(cat)"

  # stdin JSON から必要なフィールドを python3 で抽出。
  # 各フィールドの意味:
  #   session_id: セッションの UUID
  #   cwd: Claude Code の作業ディレクトリ
  #   hook_event: "SessionStart" or "SessionEnd"
  #   reason: SessionEnd の終了理由
  #     - "prompt_input_exit": /exit や Ctrl+D による正常終了
  #     - "other": ⌘+Q や⌘+Qによる強制終了
  #   session_source: SessionStart の開始種別 (JSON フィールド名は "source" だが、
  #     bash ビルトインの source と紛らわしいため変数名を変更)
  #     - "startup": 新規セッション
  #     - "resume": --resume による再開
  local session_id cwd hook_event reason session_source
  session_id="$(printf '%s' "${input}" \
    | python3 -c "${_EXTRACT_PY}.get('session_id',''))" \
    2>/dev/null)"
  cwd="$(printf '%s' "${input}" \
    | python3 -c "${_EXTRACT_PY}.get('cwd',''))" \
    2>/dev/null)"
  hook_event="$(printf '%s' "${input}" \
    | python3 -c "${_EXTRACT_PY}.get('hook_event_name',''))" \
    2>/dev/null)"
  reason="$(printf '%s' "${input}" \
    | python3 -c "${_EXTRACT_PY}.get('reason',''))" \
    2>/dev/null)"
  session_source="$(printf '%s' "${input}" \
    | python3 -c "${_EXTRACT_PY}.get('source',''))" \
    2>/dev/null)"

  # session_id と cwd は必須。なければ終了。
  if [[ -z "${session_id}" ]] || [[ -z "${cwd}" ]]; then
    return 0
  fi

  # パス定義:
  #   cache_dir: タブ情報のキャッシュ (セッション ID ごとに1ファイル)
  #   cache_file: このセッションのキャッシュファイル
  #   resume_dir: マーカーファイルの格納先
  local cache_dir="${cwd}/.claude/cmux-tab-info"
  local cache_file="${cache_dir}/${session_id}"
  local resume_dir="${cwd}/.claude/cmux-session-resume"

  case "${hook_event}" in
    SessionStart)
      # cmux identify で現在のワークスペース/サーフェス情報を取得。
      # SessionEnd 時には cmux CLI が応答しないことが検証済みのため、
      # SessionStart のタイミングでキャッシュしておく。
      local identify_json
      identify_json="$(cmux identify 2>/dev/null)"
      # cmux CLI が使えない場合は終了
      if [[ -z "${identify_json}" ]]; then
        return 0
      fi

      # identify JSON から workspace_ref と surface_ref を抽出
      local ws_ref surface_ref
      ws_ref="$(printf '%s' "${identify_json}" \
        | python3 -c \
        "${_EXTRACT_PY}['caller']['workspace_ref'])" \
        2>/dev/null)"
      surface_ref="$(printf '%s' "${identify_json}" \
        | python3 -c \
        "${_EXTRACT_PY}['caller']['surface_ref'])" \
        2>/dev/null)"

      # ワークスペース名を取得 (マッチングに使用)
      local ws_name
      ws_name="$(get_workspace_name)"

      # cmux tree からタブインデックス (GUI 上の並び順) とタブ名を取得
      local tab_info tab_index tab_name
      tab_info="$(get_tab_info "${ws_ref}" "${surface_ref}")"
      tab_index="${tab_info%% *}"
      tab_name="${tab_info#* }"

      # key=value 形式でキャッシュファイルに保存
      if [[ -n "${ws_name}" ]] && [[ -n "${tab_index}" ]]; then
        mkdir -p "${cache_dir}"
        {
          printf 'workspace_name=%s\n' "${ws_name}"
          printf 'tab_index=%s\n' "${tab_index}"
          printf 'tab_name=%s\n' "${tab_name}"
        } > "${cache_file}"
      fi

      # resume 成功時: マーカーのみ削除。
      # キャッシュは削除しない。resume したセッションが再度 ⌘+Q で終了した際に、
      # キャッシュからタブ情報を読んでマーカーを作成する必要があるため。
      if [[ "${session_source}" == "resume" ]]; then
        rm -f "${resume_dir}/${session_id}"
      fi
      ;;

    SessionEnd)
      if [[ "${reason}" == "prompt_input_exit" ]]; then
        # 正常終了 (/exit, Ctrl+D): キャッシュを削除。
        # このセッションは意図的に終了されたため、次回の resume 対象にしない。
        rm -f "${cache_file}"
      elif [[ "${reason}" == "other" ]]; then
        mkdir -p "${resume_dir}"
        if [[ -f "${cache_file}" ]]; then
          # キャッシュの内容 (workspace_name, tab_index, tab_name) をマーカーにコピー
          cp "${cache_file}" "${resume_dir}/${session_id}"
        else
          # キャッシュがない場合 (SessionStart hook が失敗した等) は空マーカーを作成。
          # zsh プラグイン側で workspace/tab のマッチングはできないが、
          # ディレクトリの一致だけで resume を提案する。
          touch "${resume_dir}/${session_id}"
        fi

        # transcript_path からセッション名を取得してマーカーに追記。
        # JSONL ファイル内の最後の custom-title エントリの customTitle フィールドが
        # /rename で設定された (または自動生成された) セッション名。
        local transcript_path session_name
        transcript_path="$(printf '%s' "${input}" \
          | python3 -c "${_EXTRACT_PY}.get('transcript_path',''))" \
          2>/dev/null)"
        if [[ -n "${transcript_path}" ]] && [[ -f "${transcript_path}" ]]; then
          session_name="$(python3 -c "
import json, sys
name = ''
for line in open(sys.argv[1]):
    line = line.strip()
    if not line:
        continue
    try:
        data = json.loads(line)
        if data.get('type') == 'custom-title':
            name = data.get('customTitle', '')
    except:
        pass
print(name)
" "${transcript_path}" 2>/dev/null)"
          if [[ -n "${session_name}" ]]; then
            printf 'session_name=%s\n' "${session_name}" \
              >> "${resume_dir}/${session_id}"
          fi
        fi
      fi
      ;;
  esac

}

main "$@"

zsh起動時のシェルスクリプト

zsh起動時のシェルスクリプトでは、ワークスペース名やタブインデックスの情報を元にclaude /resumeの提案および実行を行います。

/exitではなく、タブを閉じたセッションについても自動で再開されるのは嫌だなと感じたため、5秒以内に何かキーを押下すれば再開をキャンセルするようにしてみました。

実際のzsh起動時のシェルスクリプトは以下のとおりです。

~/.config/sheldon/plugins/cmux-claude-resume/cmux-claude-resume.plugin.zsh
#!/usr/bin/env zsh
#
# cmux-claude-resume: cmux セッション復元時に Claude Code を自動 resume する
#
# ---- 概要 ----
#
# cmux はセッション復元時にレイアウトと作業ディレクトリを復元するが、
# ペイン内のプロセス (Claude Code) は復元しない。
# このプラグインは、cmux 復元後の zsh 起動時にマーカーファイルを検出し、
# 対応する Claude Code セッションの resume を提案する。
#
# ---- マーカーファイル ----
#
# マーカーは Claude Code の SessionEnd hook (reason: "other") で作成される。
# 場所: $cwd/.claude/cmux-session-resume/$session_id
# 内容: key=value 形式 (workspace_name, tab_index, tab_name, session_name)
#
# ---- マッチング条件 ----
#
# resume を提案するには以下の3条件が全て一致する必要がある:
#   1. ディレクトリ: マーカーが $PWD/.claude/cmux-session-resume/ に存在
#   2. workspace name: マーカーの workspace_name が現在と一致
#   3. tab index: マーカーの tab_index が現在と一致
#
# workspace ref ではなく workspace name を使用する理由:
#   workspace ref はワークスペースの追加/削除で番号がずれるため不安定。
#   workspace name はユーザーが明示的にリネームしない限り変わらない。
#
# ---- 同一ディレクトリの複数セッション ----
#
# mv でマーカーをアトミックに取得するため、複数の zsh が同時に起動しても競合しない。
# macOS に flock が標準搭載されていないため、mv を使用している。
# mv は同一ファイルシステム上で POSIX 仕様としてアトミックが保証されている。
# 先に mv した方がそのマーカーを取得し、失敗した方は次のマーカーを試行する。
#
# ---- マーカーの削除 ----
#
# マーカーは以下のタイミングで削除される:
#   - resume 成功時: SessionStart hook (source: "resume") が削除
#   - キャンセル時: このプラグインが削除 (キャッシュも同時に削除)
#   - 期限切れ時: このプラグインが削除 (1h 超のマーカー)
#
# ---- 既知の制限 ----
#
# セッション中にワークスペース名をリネームした場合、
# キャッシュが古い名前のままとなりマッチングに失敗する。

# cmux 外では何もしない (環境変数が未設定)
if [[ -z "${CMUX_WORKSPACE_ID}" ]]; then
  return 0
fi

# マーカーの有効期限 (秒)
readonly _CMUX_SESSION_RESUME_STALE=3600  # 1 hour

#######################################
# 現在の workspace name と tab index を取得する。
#
# cmux list-workspaces で workspace name を取得し、
# cmux identify + cmux tree で tab index を特定する。
# Globals:
#   None
# Arguments:
#   None
# Outputs:
#   Two lines to STDOUT:
#     line 1: workspace name
#     line 2: tab index
# Returns:
#   0 on success, 1 if unavailable.
#######################################
_cmux_session_resume::get_tab_identity() {
  # ワークスペース名を取得
  local ws_name
  ws_name="$(cmux list-workspaces 2>/dev/null \
    | grep '\[selected\]' \
    | sed 's/^\* workspace:[0-9]*  //' \
    | sed 's/  \[selected\]//')"
  [[ -n "${ws_name}" ]] || return 1

  # identify から workspace_ref と surface_ref を取得
  local identify_json ws_ref surface_ref
  identify_json="$(cmux identify 2>/dev/null)" || return 1
  ws_ref="$(printf '%s' "${identify_json}" \
    | python3 -c "import json,sys; \
    print(json.load(sys.stdin)\
    ['caller']['workspace_ref'])" 2>/dev/null)" \
    || return 1
  surface_ref="$(printf '%s' "${identify_json}" \
    | python3 -c "import json,sys; \
    print(json.load(sys.stdin)\
    ['caller']['surface_ref'])" 2>/dev/null)" \
    || return 1

  # cmux tree の surface 行を順にカウントし、自身の surface_ref の位置 = tab index を求める
  local tab_index
  tab_index="$(cmux tree --workspace "${ws_ref}" 2>/dev/null \
    | grep 'surface ' \
    | awk -v target="${surface_ref} " \
    '{ idx++; if ($0 ~ target) { print idx; exit } }')"
  [[ -n "${tab_index}" ]] || return 1

  # 改行区切りで返す (workspace name にスペースが含まれる可能性があるため)
  printf '%s\n%s' "${ws_name}" "${tab_index}"
}

#######################################
# マーカーファイルから指定キーの値を読み取る。
#
# マーカーは key=value 形式 (1行1キー) で保存されている。
# 例: workspace_name=雑談
# Globals:
#   None
# Arguments:
#   $1 - marker file path
#   $2 - key name
# Outputs:
#   Value to STDOUT.
# Returns:
#   0 on success, 1 if not found.
#######################################
_cmux_session_resume::read_marker_value() {
  grep "^${2}=" "$1" 2>/dev/null | cut -d= -f2-
}

#######################################
# 残存マーカーを検出し、Claude Code の resume を実行する。
#
# cmux 復元後の zsh 起動時に呼ばれる。
# $PWD/.claude/cmux-session-resume/ 配下のマーカーを走査し、
# workspace name + tab index が一致するものを見つけたら
# 5秒間のカウントダウンを表示する。
#
# - 5秒以内にキー入力: キャンセル (マーカーとキャッシュを削除)
# - 5秒経過: claude --resume <session_id> を実行
# Globals:
#   _CMUX_SESSION_RESUME_STALE
# Arguments:
#   None
# Outputs:
#   Writes resume prompt to STDOUT.
# Returns:
#   0
#######################################
_cmux_session_resume::check_and_prompt() {
  local resume_dir="${PWD}/.claude/cmux-session-resume"

  # マーカーディレクトリがなければ何もしない
  [[ -d "${resume_dir}" ]] || return 0

  # マーカーファイルを列挙 (zsh の (N) で glob マッチなし時のエラーを抑制)
  local marker_files=("${resume_dir}"/*(N))
  (( ${#marker_files[@]} )) || return 0

  # 現在のタブの workspace name + tab index を取得
  local tab_identity current_ws current_idx
  tab_identity="$(_cmux_session_resume::get_tab_identity)" \
    || return 0
  # 改行区切りで返されるため、1行目 = workspace name, 2行目 = tab index
  current_ws="${tab_identity%%$'\n'*}"
  current_idx="${tab_identity##*$'\n'}"

  local marker_file marker_ws marker_idx marker_mod now
  for marker_file in "${marker_files[@]}"; do
    [[ -f "${marker_file}" ]] || continue

    # 古すぎるマーカー (1h 超) はキャッシュと共に削除してスキップ
    marker_mod="$(stat -f %m "${marker_file}" 2>/dev/null)" \
      || continue
    now="$(date +%s)"
    if (( now - marker_mod > _CMUX_SESSION_RESUME_STALE )); then
      rm -f "${marker_file}"
      rm -f "${PWD}/.claude/cmux-tab-info/${marker_file:t}"
      continue
    fi

    # workspace name + tab index の両方が一致するか確認。
    # 一致しないマーカーには触らない (別のタブ用の可能性があるため)。
    marker_ws="$(_cmux_session_resume::read_marker_value \
      "${marker_file}" "workspace_name")"
    marker_idx="$(_cmux_session_resume::read_marker_value \
      "${marker_file}" "tab_index")"
    if [[ "${marker_ws}" != "${current_ws}" ]] \
        || [[ "${marker_idx}" != "${current_idx}" ]]; then
      continue
    fi

    # mv でアトミックに取得。複数の zsh が同時に起動しても、
    # mv が成功した方だけがこのマーカーを処理する。
    local claimed="${marker_file}.claiming.$$"
    mv "${marker_file}" "${claimed}" 2>/dev/null || continue

    # マーカーからセッション ID / タブ名 / セッション名を取得
    local session_id tab_name session_name
    session_id="${${claimed:t}%.claiming.*}"
    tab_name="$(_cmux_session_resume::read_marker_value \
      "${claimed}" "tab_name")"
    session_name="$(_cmux_session_resume::read_marker_value \
      "${claimed}" "session_name")"

    # resume 提案を表示
    local display_dir="${PWD/#$HOME/~}"
    printf '\033[1;36m%s\033[0m\n' \
      "[cmux] Claude Code のセッションが見つかりました"
    printf '  ディレクトリ: %s\n' "${display_dir}"
    printf '  セッションID: %s\n' "${session_id}"
    if [[ -n "${session_name}" ]]; then
      printf '  セッション名: %s\n' "${session_name}"
    fi
    printf '5秒以内にキーを押すとキャンセル\n'

    if read -t 5 -k 1 2>/dev/null; then
      # キー入力あり: resume をキャンセル。マーカーとキャッシュの両方を削除する。
      rm -f "${claimed}"
      rm -f "${PWD}/.claude/cmux-tab-info/${session_id}"
      printf '\n[cmux] キャンセルしました\n'
    else
      # 5秒間入力なし: resume を実行。
      # claimed ファイルを削除し、claude --resume でセッションを再開する。
      # 元のマーカーは SessionStart hook (source: "resume") が削除する。
      rm -f "${claimed}"
      claude --resume "${session_id}"
    fi
    return 0
  done
}

# プラグイン読み込み時に即座に実行
_cmux_session_resume::check_and_prompt

注意点

この仕組みの注意点は以下のとおりです。

  • Claude Code起動後にワークスペース名を変更すると、自動で resume されない
    • ワークスペースとセッションの紐付けはClaude Code起動時のワークスペース名で行っているため
  • ワークスペース名が重複していると、自動で resume されない
  • タブの削除を行い、再起動時にタブインデックスが繰り上がってしまうと、自動で resume されない
  • Claude Codeのセッションの中でディレクトリを移動してしまうと、自動で resume されない
    • ~/.claude/配下にキャッシュファイルやマーカーファイルが作成されても良いのであれば、スクリプトの変更することで解消します
  • cmux 自体のバージョンアップを伴う再起動の場合は、ワークスペースやタブなどの各種情報は引き継がれないため、自動で resume されない

いずれも中々気になる挙動だと思います。

あくまで気休め程度の仕組みと思ってもらえればと思います。

やってみた

現在のセッション確認

実際に動作確認をします。

2つワークスペースを用意しました。

cmux-resume-workspace-1は以下のようになっています。

1.workspace-1.png

cmux-resume-workspace-2は以下のようになっています。

2.workspace-2.png

このときのcmux treeの結果は以下のとおりです。

> cmux tree
window window:1 [current] ◀ active
├── workspace workspace:2 "cmux-resume-workspace-1"
   ├── pane pane:2
   └── surface surface:8 [terminal] "✳ hello-1" [selected]
   ├── pane pane:3 [focused]
   └── surface surface:6 [terminal] "✳ hello-3" [selected]
   └── pane pane:4
       └── surface surface:7 [terminal] "✳ hello-2" [selected]
├── workspace workspace:3 "cmux-resume-workspace-2" [selected] ◀ active
   ├── pane pane:5
   └── surface surface:9 [terminal] "~/developers-io/20260329_cmux-resume-claude-code" [selected]
   └── pane pane:6 [focused] ◀ active
       └── surface surface:10 [terminal] "cmux tree" [selected] ◀ active ◀ here
├── workspace workspace:1 "雑談"
.
.
(中略)
.
.

この状態でClaude Codeセッション開始時に作成されるキャッシュファイルの確認をします。

> cmux version
cmux 0.62.2 (77) [6c203b514]

> pwd
~/developers-io

> ls -lR .claude
total 8
drwxr-xr-x@ 3 <ユーザー>  <グループ>    96  3 29 18:35 cmux-session-resume
drwxr-xr-x@ 4 <ユーザー>  <グループ>   128  3 30 10:04 cmux-tab-info
-rw-r--r--@ 1 <ユーザー>  <グループ>  3210  3 29 10:52 settings.local.json

.claude/cmux-session-resume:
total 0

.claude/cmux-tab-info:
total 8
-rw-r--r--@ 1 <ユーザー>  <グループ>  76  3 30 10:04 68a455c7-e74a-4250-a347-f9a05799f310

> cat .claude/cmux-tab-info/68a455c7-e74a-4250-a347-f9a05799f310
workspace_name=cmux-resume-workspace-1
tab_index=1
tab_name= Claude Code

キャッシュファイルが作成されていますね。

他のディレクトリも確認します。

> ls -lR 20260329_cmux-resume-claude-code/.claude/
total 0
drwxr-xr-x@ 2 <ユーザー>  <グループ>   64  3 29 18:33 cmux-session-resume
drwxr-xr-x@ 6 <ユーザー>  <グループ>  192  3 30 10:06 cmux-tab-info

20260329_cmux-resume-claude-code/.claude/cmux-session-resume:
total 0

20260329_cmux-resume-claude-code/.claude/cmux-tab-info:
total 24
-rw-r--r--@ 1 <ユーザー>  <グループ>  76  3 30 10:04 42c4765b-7e3e-453d-a8fd-867079adc85e
-rw-r--r--@ 1 <ユーザー>  <グループ>  76  3 30 10:04 4e4b1de9-9228-474f-beca-8ffdfa20b18c
-rw-r--r--@ 1 <ユーザー>  <グループ>  76  3 30 10:06 56eafa37-3d76-426b-a6f9-0b327d996252

> cat 20260329_cmux-resume-claude-code/.claude/cmux-tab-info/42c4765b-7e3e-453d-a8fd-867079adc85e
workspace_name=cmux-resume-workspace-1
tab_index=3
tab_name= Claude Code

> cat 20260329_cmux-resume-claude-code/.claude/cmux-tab-info/4e4b1de9-9228-474f-beca-8ffdfa20b18c
workspace_name=cmux-resume-workspace-1
tab_index=2
tab_name= Claude Code

> cat 20260329_cmux-resume-claude-code/.claude/cmux-tab-info/56eafa37-3d76-426b-a6f9-0b327d996252
workspace_name=cmux-resume-workspace-2
tab_index=1
tab_name= Claude Code

⌘ + Q で cmux 終了

⌘ + Q で cmux を終了させます。

3.cmuxの終了.png

このときのキャッシュとマーカーファイルの確認をします。

> ls -lR .claude/
total 8
drwxr-xr-x@ 4 <ユーザー>  <グループ>   128  3 30 10:11 cmux-session-resume
drwxr-xr-x@ 4 <ユーザー>  <グループ>   128  3 30 10:04 cmux-tab-info
-rw-r--r--@ 1 <ユーザー>  <グループ>  3210  3 29 10:52 settings.local.json

.claude/cmux-session-resume:
total 8
-rw-r--r--@ 1 <ユーザー>  <グループ>  97  3 30 10:11 68a455c7-e74a-4250-a347-f9a05799f310

.claude/cmux-tab-info:
total 8
-rw-r--r--@ 1 <ユーザー>  <グループ>  76  3 30 10:04 68a455c7-e74a-4250-a347-f9a05799f310

> cat .claude/cmux-session-resume/68a455c7-e74a-4250-a347-f9a05799f310
workspace_name=cmux-resume-workspace-1
tab_index=1
tab_name= Claude Code
session_name=hello-1
> ls -lR 20260329_cmux-resume-claude-code/.claude/
total 0
drwxr-xr-x@ 5 <ユーザー>  <グループ>  160  3 30 10:11 cmux-session-resume
drwxr-xr-x@ 6 <ユーザー>  <グループ>  192  3 30 10:06 cmux-tab-info

20260329_cmux-resume-claude-code/.claude/cmux-session-resume:
total 24
-rw-r--r--@ 1 <ユーザー>  <グループ>  97  3 30 10:11 42c4765b-7e3e-453d-a8fd-867079adc85e
-rw-r--r--@ 1 <ユーザー>  <グループ>  97  3 30 10:11 4e4b1de9-9228-474f-beca-8ffdfa20b18c
-rw-r--r--@ 1 <ユーザー>  <グループ>  97  3 30 10:11 56eafa37-3d76-426b-a6f9-0b327d996252

20260329_cmux-resume-claude-code/.claude/cmux-tab-info:
total 24
-rw-r--r--@ 1 <ユーザー>  <グループ>  76  3 30 10:04 42c4765b-7e3e-453d-a8fd-867079adc85e
-rw-r--r--@ 1 <ユーザー>  <グループ>  76  3 30 10:04 4e4b1de9-9228-474f-beca-8ffdfa20b18c
-rw-r--r--@ 1 <ユーザー>  <グループ>  76  3 30 10:06 56eafa37-3d76-426b-a6f9-0b327d996252

> cat 20260329_cmux-resume-claude-code/.claude/cmux-session-resume/42c4765b-7e3e-453d-a8fd-867079adc85e
workspace_name=cmux-resume-workspace-1
tab_index=3
tab_name= Claude Code
session_name=hello-2

> cat 20260329_cmux-resume-claude-code/.claude/cmux-session-resume/4e4b1de9-9228-474f-beca-8ffdfa20b18c
workspace_name=cmux-resume-workspace-1
tab_index=2
tab_name= Claude Code
session_name=hello-3

> cat 20260329_cmux-resume-claude-code/.claude/cmux-session-resume/56eafa37-3d76-426b-a6f9-0b327d996252
workspace_name=cmux-resume-workspace-2
tab_index=1
tab_name= Claude Code
session_name=hello-4

キャッシュファイルを元にマーカーファイルが作成されていることが分かりますね。

cmux 起動

それでは cmux を起動します。

起動後のcmux-resume-workspace-1は以下のとおりです。

4.cmux再起後のworkspace-1.png

cmux-resume-workspace-2は以下のとおりです。

5.cmux再起後のworkspace-2.png

いずれもセッション再開できていますね。

このときのcmux treeの結果は以下のとおりです。

> cmux tree
window window:1 [current] ◀ active
├── workspace workspace:1 "cmux-resume-workspace-1"
   ├── pane pane:1 [focused]
   └── surface surface:3 [terminal] "✳ hello-1" [selected]
   ├── pane pane:2
   └── surface surface:1 [terminal] "✳ hello-3" [selected]
   └── pane pane:3
       └── surface surface:2 [terminal] "✳ hello-2" [selected]
├── workspace workspace:2 "cmux-resume-workspace-2" [selected] ◀ active
   ├── pane pane:4
   └── surface surface:5 [terminal] "✳ hello-4" [selected]
   └── pane pane:5 [focused] ◀ active
       └── surface surface:4 [terminal] "cmux tree" [selected] ◀ active ◀ here
├── workspace workspace:3 "雑談"
.
.
(中略)
.
.

再起動前はworkspace:2だったcmux-resume-workspace-1workspace:1へと、起動したときのワークスペース順でIDが上から順番に割り振られていることが分かります。

このときのキャッシュファイルとマーカーファイルは以下のとおりです。

> ls -lR .claude/
total 8
drwxr-xr-x@ 3 <ユーザー>  <グループ>    96  3 30 10:13 cmux-session-resume
drwxr-xr-x@ 6 <ユーザー>  <グループ>   192  3 30 10:14 cmux-tab-info
-rw-r--r--@ 1 <ユーザー>  <グループ>  3210  3 29 10:52 settings.local.json

.claude/cmux-session-resume:
total 0

.claude/cmux-tab-info:
total 24
-rw-r--r--@ 1 <ユーザー>  <グループ>  55  3 30 10:14 31790410-fcd5-4389-b832-b3df9ce99c92
-rw-r--r--@ 1 <ユーザー>  <グループ>  72  3 30 10:13 68a455c7-e74a-4250-a347-f9a05799f310
-rw-r--r--@ 1 <ユーザー>  <グループ>  55  3 30 10:14 cda60e9e-8003-408d-b9ee-71fe8e9ce99d

> cat .claude/cmux-tab-info/31790410-fcd5-4389-b832-b3df9ce99c92
workspace_name=雑談
tab_index=1
tab_name=cmux-resume

> cat .claude/cmux-tab-info/68a455c7-e74a-4250-a347-f9a05799f310
workspace_name=cmux-resume-workspace-1
tab_index=1
tab_name= hello-1

> cat .claude/cmux-tab-info/cda60e9e-8003-408d-b9ee-71fe8e9ce99d
workspace_name=雑談
tab_index=1
tab_name=cmux-resume
> ls -lR 20260329_cmux-resume-claude-code/.claude/
total 0
drwxr-xr-x@ 2 <ユーザー>  <グループ>   64  3 30 10:13 cmux-session-resume
drwxr-xr-x@ 6 <ユーザー>  <グループ>  192  3 30 10:06 cmux-tab-info

20260329_cmux-resume-claude-code/.claude/cmux-session-resume:
total 0

20260329_cmux-resume-claude-code/.claude/cmux-tab-info:
total 24
-rw-r--r--@ 1 <ユーザー>  <グループ>  72  3 30 10:13 42c4765b-7e3e-453d-a8fd-867079adc85e
-rw-r--r--@ 1 <ユーザー>  <グループ>  72  3 30 10:13 4e4b1de9-9228-474f-beca-8ffdfa20b18c
-rw-r--r--@ 1 <ユーザー>  <グループ>  72  3 30 10:13 56eafa37-3d76-426b-a6f9-0b327d996252

> cat 20260329_cmux-resume-claude-code/.claude/cmux-tab-info/42c4765b-7e3e-453d-a8fd-867079adc85e
workspace_name=cmux-resume-workspace-1
tab_index=3
tab_name= hello-2

> cat 20260329_cmux-resume-claude-code/.claude/cmux-tab-info/4e4b1de9-9228-474f-beca-8ffdfa20b18c
workspace_name=cmux-resume-workspace-1
tab_index=2
tab_name= hello-3

> cat 20260329_cmux-resume-claude-code/.claude/cmux-tab-info/56eafa37-3d76-426b-a6f9-0b327d996252
workspace_name=cmux-resume-workspace-2
tab_index=1
tab_name= hello-4

セッションを再開したためマーカーファイルは削除されていますね。

一方でキャッシュファイルではタブ名がセッション名で更新されています。

再度 cmux を終了した後に起動

再度 cmux を終了した後に起動をします。

起動をすると、以下のように今回もセッション再開できました。

6.2回目のcmux再起後のworkspace-1.png
6.2回目のcmux再起後のworkspace-2.png

このときのキャッシュファイルとマーカーファイルは以下のとおりです。

> ls -lR .claude/
total 8
drwxr-xr-x@ 5 <ユーザー>  <グループ>   160  3 30 10:24 cmux-session-resume
drwxr-xr-x@ 6 <ユーザー>  <グループ>   192  3 30 10:14 cmux-tab-info
-rw-r--r--@ 1 <ユーザー>  <グループ>  3210  3 29 10:52 settings.local.json

.claude/cmux-session-resume:
total 16
-rw-r--r--@ 1 <ユーザー>  <グループ>   93  3 30 10:24 68a455c7-e74a-4250-a347-f9a05799f310
-rw-r--r--@ 1 <ユーザー>  <グループ>  100  3 30 10:24 cda60e9e-8003-408d-b9ee-71fe8e9ce99d

.claude/cmux-tab-info:
total 24
-rw-r--r--@ 1 <ユーザー>  <グループ>  55  3 30 10:14 31790410-fcd5-4389-b832-b3df9ce99c92
-rw-r--r--@ 1 <ユーザー>  <グループ>  72  3 30 10:13 68a455c7-e74a-4250-a347-f9a05799f310
-rw-r--r--@ 1 <ユーザー>  <グループ>  55  3 30 10:14 cda60e9e-8003-408d-b9ee-71fe8e9ce99d

> cat .claude/cmux-session-resume/68a455c7-e74a-4250-a347-f9a05799f310
workspace_name=cmux-resume-workspace-1
tab_index=1
tab_name= hello-1
session_name=hello-1

> cat .claude/cmux-session-resume/cda60e9e-8003-408d-b9ee-71fe8e9ce99d
workspace_name=雑談
tab_index=1
tab_name=cmux-resume
session_name=workspace-ref-to-name-migration
> ls -lR 20260329_cmux-resume-claude-code/.claude/
total 0
drwxr-xr-x@ 5 <ユーザー>  <グループ>  160  3 30 10:24 cmux-session-resume
drwxr-xr-x@ 6 <ユーザー>  <グループ>  192  3 30 10:06 cmux-tab-info

20260329_cmux-resume-claude-code/.claude/cmux-session-resume:
total 24
-rw-r--r--@ 1 <ユーザー>  <グループ>  93  3 30 10:24 42c4765b-7e3e-453d-a8fd-867079adc85e
-rw-r--r--@ 1 <ユーザー>  <グループ>  93  3 30 10:24 4e4b1de9-9228-474f-beca-8ffdfa20b18c
-rw-r--r--@ 1 <ユーザー>  <グループ>  93  3 30 10:24 56eafa37-3d76-426b-a6f9-0b327d996252

20260329_cmux-resume-claude-code/.claude/cmux-tab-info:
total 24
-rw-r--r--@ 1 <ユーザー>  <グループ>  72  3 30 10:13 42c4765b-7e3e-453d-a8fd-867079adc85e
-rw-r--r--@ 1 <ユーザー>  <グループ>  72  3 30 10:13 4e4b1de9-9228-474f-beca-8ffdfa20b18c
-rw-r--r--@ 1 <ユーザー>  <グループ>  72  3 30 10:13 56eafa37-3d76-426b-a6f9-0b327d996252

> cat 20260329_cmux-resume-claude-code/.claude/cmux-session-resume/42c4765b-7e3e-453d-a8fd-867079adc85e
workspace_name=cmux-resume-workspace-1
tab_index=3
tab_name= hello-2
session_name=hello-2

> cat 20260329_cmux-resume-claude-code/.claude/cmux-session-resume/4e4b1de9-9228-474f-beca-8ffdfa20b18c
workspace_name=cmux-resume-workspace-1
tab_index=2
tab_name= hello-3
session_name=hello-3

> cat 20260329_cmux-resume-claude-code/.claude/cmux-session-resume/56eafa37-3d76-426b-a6f9-0b327d996252
workspace_name=cmux-resume-workspace-2
tab_index=1
tab_name= hello-4
session_name=hello-4

Claude Codeのセッション中に⌘ + Wで閉じたタブを開いた場合

Claude Codeのセッション中に⌘ + Wで閉じたタブを開いた場合の挙動も確認します。

hello-2というセッションのタブを閉じます。

8.hello-2のセッションのタブ閉じ.png

新規ペインを開くと、たまたまhello-2の作業ディレクトリだったので再開してくれました。

9.hello-2のセッションのタブ閉じ後の復帰.png

Claude Codeのセッション中に⌘ + Wで閉じたタブを開いた際にEnterを押下

復帰キャンセルも確認します。

hello-2というセッションのタブを閉じて、新規ペインを開きます。

5秒以内にキーを押すとキャンセルに従い、Enterを押下すると、確かにキャンセルされました。

10.復帰キャンセル.png

/exit でセッションを閉じた場合

/exit でセッションを閉じた場合の挙動を確認します。

/exit でセッションを閉じたあと、新規ペインを開きました。ただし、以下のようにClaude Codeのセッション再開処理は行われませんでした。

11.exit後は復帰されないこと.png

このときのキャッシュファイルとマーカーファイルは以下のとおりです。

> ls -lR 20260329_cmux-resume-claude-code/.claude/
total 0
drwxr-xr-x@ 2 <ユーザー>  <グループ>   64  3 30 10:32 cmux-session-resume
drwxr-xr-x@ 4 <ユーザー>  <グループ>  128  3 30 10:35 cmux-tab-info

20260329_cmux-resume-claude-code/.claude/cmux-session-resume:
total 0

20260329_cmux-resume-claude-code/.claude/cmux-tab-info:
total 8
-rw-r--r--@ 1 <ユーザー>  <グループ>  72  3 30 10:29 56eafa37-3d76-426b-a6f9-0b327d996252

> cat 20260329_cmux-resume-claude-code/.claude/cmux-tab-info/56eafa37-3d76-426b-a6f9-0b327d996252
workspace_name=cmux-resume-workspace-2
tab_index=1
tab_name= hello-4

/exitに伴い、hello-2のファイルが削除されていることが分かります。

cmux の標準機能でサポートしてくれるまでの気休め暫定対応として

cmux の再起動時に自動で claude /resume するようにしてみました。

cmux の標準機能でサポートしてくれるまでの気休め暫定対応として使っていきたいと思います。

この記事が誰かの助けになれば幸いです。

以上、クラウド事業本部 コンサルティング部の のんピ(@non____97)でした!

この記事をシェアする

FacebookHatena blogX

関連記事