コンテンツにスキップ

tmux ベースの Claude セッションマネージャ — 設計調査

対象: ハブ 1 本から、tmux 上に複数の Claude Code セッションを立て・回収し・畳む仕組み。 調査日: 2026-08-17 / claude 2.1.226 (CLI) / 2.1.229 (デスクトップ) / tmux 3.7b。

この文書は「いまどうなっているか」を書く場所。 覆った決定は追記で上書きせず、 ADR に 1 決定 1 ファイルで置き、ここからは Superseded として送る。 この使い分けを始めた経緯は ADR とは。


1. 結論

作るものは薄い。 想定していた「セッション管理」の大半は、Claude Code 2.1.x が既に持っている。 実測で確認できた既存機能は次のとおり。

やりたいこと 既にある 自作が要るか
全セッションの一覧・状態 claude agents --json 不要
ハブ → 子セッションへの指示 ListAgents / SendMessage ツール 不要
tmux ペインとの対応づけ Claude 自身が tmux フィールドに記録する 不要
セッション ID の固定 --session-id <uuid> 不要
表示名 -n <name> 不要
Remote Control 登録 remoteControlAtStartup: true(設定済み) 不要
skills が見えること ~/.claude/skills はユーザレベル。cwd に依らず 57 件見える 不要
セッションの新規作成 無い(SendMessage は既存宛のみ) 要る
workspace trust ダイアログの回避 無い 要る
プロジェクト解決(ghq / 一時ディレクトリ) 無い 要る
命名規約と後片付け 無い 要る

したがって自作分は「足りない 4 つだけを埋める薄いラッパー」に収まる。既存の tmux オーケストレータ(後述)を持ってくると、いま組み込みになった部分まで二重に抱えることになる。


2. 実測でわかったこと(証拠つき)

2.1 生きているセッションのレジストリが既にある

Claude Code は起動時に ~/.claude/sessions/<pid>.json を書く。tmux 内で claude を起動して 実際に採取したもの:

{"pid":82344,"sessionId":"b85e68f4-…","cwd":"/…/scratchpad/probe",
 "kind":"interactive","entrypoint":"claude-desktop",
 "tmux":"probe1:@0.%0",
 "messagingSocketPath":"/tmp/cc-socks/82344.sock",
 "name":"probe-tmux","status":"idle",
 "bridgeSessionId":"session_01HavSXkisL7kZBmaz6jkEFu"}

読み出しは claude agents --json(TTY 不要、スクリプトから叩ける)。デスクトップ・VS Code 拡張・tmux 内 CLI が同じ配列に並ぶことを確認した。

効いてくる点: 状態監視のためのデーモンもポーリングも要らない。参考にした既存実装 (devas.life 版)は tmux のユーザオプション @claude_state に hook で状態を書き込んでいたが、 その役割は今 Claude 本体が持っている。

訂正(S7、2026-08-17): claude agents --json は tmux フィールドを持たない。 返るのは cwd / kind / name / pid / sessionId / startedAt だけで、tmux は ~/.claude/sessions/<pid>.json の側にしか無い。したがって あの出力から「cc/ で 始まるセッション」を選び出すことはできない。ccs ls は tmux 側を起点にし (cc/ が付いているのは ccs が立てたものだけなので管轄として正しい)、 各セッションの中身はレジストリのファイルから引く。

2.2 ハブ → tmux セッションの往復が通る(実証済み)

/tmp 配下の空ディレクトリで tmux セッションを立て、このハブから SendMessage を投げ、返答を得た。

  • ListAgents に probe-tmux [68a618] · interactive · idle · tmux probe1:@0.%0 として出た
  • SendMessage が届き、返答が tmux capture-pane で読めた
  • 返答内容: cwd は /tmp/…/probe、$TMUX は設定済み、ls ~/.claude/skills | wc -l = 57

つまり「一時ディレクトリで作業させても skills は全部見える」が実測で確定した。 ~/.claude/skills の 57 件は全て ~/.agents/skills/* への symlink で、ユーザレベルなので cwd に依存しない。ここに手当ては要らない。

2.3 cross-session-hub スキルは tmux セッションを見られない(要更新)

既存スキルが使う mcp__ccd_session_mgmt__* は デスクトップアプリ専用のストアを見ている。 セッション ID が local_<uuid> 形式で、実測では VS Code のセッションも tmux のセッションも 一覧に出なかった。

一方、組み込みの ListAgents / SendMessage は全エントリポイントを横断する。

ccd_session_mgmt ListAgents / SendMessage
デスクトップのセッション ○ ○
VS Code のセッション × ○
tmux 内の CLI セッション × ○
停止中セッションへの送信(自動起動) ○ ×(プロセスが生きている必要あり)
トランスクリプトの読み出し ○ (list_events) ×(capture-pane か jsonl 直読み)

両方要る。 新方式では ListAgents/SendMessage が主、ccd_session_mgmt は デスクトップ側セッションの履歴を読むときに残る。スキルはこの二層構造に書き直す必要がある。

2.4 --session-id は尊重される

claude -p --session-id <uuid> が、要求した UUID をそのまま session_id として返した。 → 立てる前に ID を決められる。 トランスクリプトの場所 (~/.claude/projects/<cwd-slug>/<uuid>.jsonl)も起動前に確定するので、 起動直後のポーリングによる名寄せが不要になる。

2.5 tmux 連携は Claude 本体にも一部ある

claude --worktree <name> --tmux が組み込みで存在する(iTerm2 のネイティブペイン、 --tmux=classic で従来の tmux)。ただし --tmux は --worktree 必須なので、 「既存リポジトリのまま」「一時ディレクトリ」のケースは自前で立てることになる。

2.6 詰まりどころ: workspace trust ダイアログ

~/.claude.json に trust エントリを持たないディレクトリで対話モードの claude を起動すると、 最初に信頼確認で止まる。tmux 越しに send-keys "1" Enter で抜けるまで、セッションは レジストリにすら現れなかった(~/.claude/sessions/82344.json が存在しない状態が続いた)。

  • -p(print モード)はこのダイアログを飛ばす
  • 対話モードでは、~/.claude.json の projects["<abs path>"].hasTrustDialogAccepted が真なら出ない

訂正(2026-08-20): 止まる条件は「ディレクトリが新しいこと」ではない。 当初ここに「一時ディレクトリを毎回新規に作る方式は、毎回ここで固まる」と書いていたが、 実測し直した結果、条件は「起動時点で trust エントリが無いこと」だった。作りたての ディレクトリでも、起動前に hasTrustDialogAccepted を書いておけばダイアログは出ない (ccs は ensure_trust で実際にそうしている)。ディレクトリをいつ作ったかは関係しない。 実験と再現手順は ADR-0001。

対策は §4.4。

2.7 その他

  • 稼働中セッションの name は tmux のステータス行にも出る(── probe-tmux ──)
  • 実測中、子セッションに ⚠ Your login expires in 1 day · run /login to renew が出ていた。 無人セッションを長期に走らせる構成では、認証切れが最初に壊れる箇所になる
  • /tmp/cc-socks/<pid>.sock がセッション間 IPC の実体。プロセスが死ねば消える → tmux ペインが生きていても claude プロセスが終了していれば通信不能(§4.5)

3. 参考にした既存実装

実装 方式 採否
devas.life の tmux セッションマネージャ tmux セッション名を cwd のハッシュで決定、状態は @claude_state に hook で書く、fzf + capture-pane でプレビュー。デーモン無し 考え方を採る。 ただし状態管理は claude agents --json に置き換え(本体が持つようになったため)
nielsgroen/claude-tmux tmux ポップアップの TUI。worktree / PR 連携 人間が操作する TUI が主眼。ハブから AI が叩く用途には過剰
Tmux-Orchestrator 系 「マネージャ役の Claude」が send-keys で子を叩き、自分でチェックインを予約する 不採用。 send-keys によるプロンプト注入は SendMessage の下位互換で、脆い

共通する教訓: デーモンを作らない。tmux のプリミティブと、その場で読める状態だけで構成する。


4. 設計案

4.1 全体像

┌─ ハブセッション(デスクトップ or tmux。Remote Control 常時 ON)─────────┐
│  ・スマホ / デスクトップからここだけを触る                              │
│  ・ListAgents で全体を見る、SendMessage で個別に指示する                │
│  ・立てる / 畳むときだけ ccs を叩く                                     │
└───────────────┬────────────────────────────────────────────────────────┘
                │ Bash: ccs new / ls / kill / gc
                ▼
        ┌───────────────┐   薄いラッパー(自作分はここだけ)
        │   ccs (CLI)   │   ・tmux new-session -d
        └───────┬───────┘   ・ghq / 一時ディレクトリの解決
                │           ・trust の事前承認
                │           ・命名規約と後片付け
                ▼
   tmux server(ターミナルを閉じても残る)
   ├─ cc/x01          → claude -n x01 --session-id … (~/ghq/…/x01)
   ├─ cc/catan        → claude -n catan …
   └─ cc/tmp-abc123   → claude -n tmp-abc123 …      (使い捨てディレクトリ)
                │
                └─ 各セッションが自力で:
                   ・~/.claude/sessions/<pid>.json に登録(tmux ペイン ID 込み)
                   ・/tmp/cc-socks/<pid>.sock を開く  → SendMessage が届く
                   ・CCR ブリッジに登録              → 単体でも Remote Control 可
                   ・~/.claude/skills の 57 件を見る  → cwd に依らない

自作するのは ccs の 1 本だけ。 一覧・通信・状態は全て組み込みに委譲する。

4.2 命名規約

一意な <slug> を 1 つ決め、3 箇所で同じものを使う。これが全ての名寄せの鍵になる。

場所 値
tmux セッション名 cc/<slug>
Claude の表示名 (-n) 渡さない(hub だけ例外。#108、下記)
Claude の session id 起動時に生成した UUID を --session-id で固定

-n を渡すと自動命名が止まる。 レジストリが nameSource: "user" になり、 会話の内容から名前が付かなくなる。実測(2026-09-04):

起動 会話ログの 1 行目
claude --session-id <uuid> {"type":"ai-title","aiTitle":"Suicaカード移行手順"}
claude -n <slug> --session-id <uuid> {"type":"custom-title","customTitle":"<slug>"}
すると Claude Desktop 上の表示名が slug の
ままになり、話題では検索できない ── tmp-f610a8e6 で立てた Suica の相談が、
suica で見つからなかった(#108)。

痕跡の代わりは記録が持つ。 ghq 配下だけは、会話ログ 1 行目の custom-title が 「ccs が立てた」ことの唯一の痕跡だった(§10.4 の restore_started_by_ccs)。 ADR-0002 決定 5 が ghq 配下への印を禁じているので代わりが無く、 --append-system-prompt は会話ログに何も残さない(実測)。

そこで ccs new は立てた会話を CCS_LAUNCHED_FILE に記録する。 これは索引ではない ── 決定 6 が禁じたのは存在を二重に持つことで、ここが持つのは 「この会話は ccs が立てた」という、他のどこにも無い事実(CCS_DISMISSED_FILE と同じ理屈)。

痕跡の判定も残してある。 記録を作る前に立てたセッションと、記録を rm したときは、 そちらでしか拾えない。

作業枠は印(.ccs.json)で、worktree は置き場所の規約(.worktrees/)で列挙するので、 どちらも最初から痕跡に依存していない。

<slug> の決め方:

  • リポジトリ: ghq list の末尾要素(x01、catan)。同名衝突時は <owner>-<repo>
  • worktree: <repo>--<branch-slug>(C1、2026-08-20 に実装。§9.6)。 どこを指しても同じ slug になる — 実パスで指されたときは git に訊いて本体と ブランチを引く(W1、2026-08-26。ADR-0003 決定 3)
  • 一時: tmp-<workspaceId>(tmp-3f9a2c1b)。発行のたびに一意(I2b、2026-08-31)。 ADR-0001 で見直しが決まっている

tmp-<6桁> から tmp-<枠番号> に変えた。 §4.4 で使い捨て作業枠を固定枠にすると 決めた時点で、ランダムな 6 桁は枠番号と二重の識別子になる。枠が有限である以上、 番号で足りる。実装は tmp-<枠番号>(S3、2026-08-17)。

見直し(2026-08-20、ADR-0001): 番号で足りるという根拠が崩れた。 枠が有限だから番号で識別できる、というのが上の理屈 だったが、固定 8 枠をやめる決定によりその前提が無くなる。

より効いたのは名前が変わることのほうだった。立てたセッションは、あとから Remote Control 上で分かりやすい名前に付け替えて長時間使うことがある。いまの ccs は 枠の占有を「cc/tmp-N という名前の tmux があるか」で推論しているので、名前を変えた 瞬間に、稼働中の枠が「空いている」と見えてしまう。ディレクトリの同一性を セッションの名前で表さない ── slug は表示のための名前、ディレクトリの同一性は ディレクトリ自身が持つ、と分ける。新しい名前の形は実装時に決める(ADR-0001「未決」)。

同一性の表現は ADR-0002 で決まった(2026-08-21)。 場所の同一性は ccs が発行する workspaceId(ディレクトリ名そのもの)、会話の同一性は Claude の sessionId。tmux 名も claude の name も表示用のラベルで、同一性の根拠には 使わない。占有も冪等性も、名前ではなく作業ディレクトリ(レジストリの cwd)で照合する。 発行したディレクトリには ccs が印(.ccs.json)を刻み、ディレクトリ側からも素性を引ける ようにする。実装は未着手なので、上の tmp-<枠番号> がいまの挙動。

衝突判定は「利用者がどう打ったか」に依らない。 x01 と ken-ty/x01 と github.com/ken-ty/x01 は同じ slug に落ちる。打ち方で別セッションが立つと、 下の冪等性が成立しない。

同じ <slug> の tmux セッションが既にあれば新規に立てず、それを返す(冪等)。

追記(2026-08-30、I1 実装): 照合は名前ではなく cwd。 tmux 名も claude の name も 表示用のラベルなので、同一性の根拠にしない(ADR-0002 決定 3)。名前で当てていると、改名されたセッションが「居ない」ことになる:

どこ 何が起きていたか
resolve_as_scratch 稼働中の作業枠を横取りする
cmd_new の冪等性 同じ作業ツリーに2 本目の claude を立てる
orphan_slots 使われている枠を「空き」として消す
ccs gc の「止まったペイン」 生きているセッションを畳む(実測: gc --yes で消えた)
ccs ls / ccs agents の表示 動いているセッションを stopped と出す(N2、2026-08-30 に修正)
ccs restore の生死判定 生きているペインを畳んで立て直す(R2 と一緒に修正)
hub の生死判定 改名すると absent と読み、2 本目の hub を立てる(I4、2026-08-31 に修正)

レジストリの tmux 欄は「起動時のスナップショット」。 claude が起動時に 1 度書くだけで 改名に追随しないので、素性を引くのには使えるが、いま届く宛先にはならない。宛先が 要るときはペインの起動コマンドに残る uuid から引く(tmux_session_for_uuid)── これは --resume で立て直したペインでも同じ形で残る。

占有は tmux セッションがある または その cwd に生きている ccs のセッションがある の or で見る(今までより厳しい側にしか倒れない)。生死は、ペインの起動コマンドに残る uuid をレジストリと突き合わせて決める ── 改名しても uuid は変わらない。表示も同じ 経路(registry_file_for_session)を通す。stopped と出ると、見た人は復帰の手を 打とうとする ── ccs restore は生きている会話をもう 1 本立てることになる。

数えるのは管轄下だけ。 アプリや VS Code でその場所を開いているだけで ccs new が 断られると使えない道具になる(引き取るなら ccs adopt、§11)。

比較の前に abs_dir を通す。 レジストリに入っているのは claude が見た文字列で、 綴りが違いうる(/tmp と /private/tmp で 2 度踏んでいる)。 cc/ 接頭辞により、手動で開いた tmux セッションを巻き込まない。

slug からは危険な文字を落とす。 使えるのは [A-Za-z0-9_-] で、それ以外は - に潰す。 落とす理由は 2 つあって、どちらも「後から名前で狙えなくなる」という同じ形をしている。

  • tmux の : — tmux 3.7 はセッション名に受け付けてしまうが、: は session:window の 区切りなので、後から -t で狙えなくなる(実測)
  • SendMessage の @(N1、2026-08-31)— 組み込みのツールにとって @ は name@team のチーム区切りなので、ccs@ls-board は「チーム ls-board の ccs」と 解釈され、宛先として弾かれる(to must be a bare teammate name)。送る側からは 見えているのに届かないので、worktree のセッションだけハブから指示を受け取れなかった

実測 2026-08-31(ListAgents の 37 行): ref を持たないのは @ を含む 1 行だけで、 空白・日本語・#・/・[ ] を含む名前はすべて通っていた。効いているのは文字集合では なく @ そのもの。

だから worktree の区切りを -- に変えた。併せて @ を許可集合から外している ── 区切りを変えるだけでは足りず、foo@2 という名前のディレクトリを指されたら同じ壊れ方をする。

打ち方(ccs new <repo>@<branch>)は変えていない。 あれは SendMessage を通らないので、 変える理由が無い。変わったのは組み立てる slug のほう。

既に立っているセッションの名前は変わらない(ccs は改名しない)。cc/ccs@ls-board は そのまま attach も kill もできるが、ハブから届かないのも変わらない ── 立て直せば直る。

4.3 コマンド

ccs new <target> [-- <初期プロンプト>]   # target: リポジトリ名 | ghq パス
ccs new --tmp [-- <初期プロンプト>]       # 使い捨ての作業枠("tmp" は短い綴り)
ccs ls                                    # claude agents --json を cc/ のものに絞って整形
ccs attach <slug>                         # 人間が乗り込む
ccs adopt <target>                        # 管轄外のセッションを引き取る(2026-08-30 追加)
ccs send <slug> <message>                 # 補助。通常はハブが SendMessage を使う
ccs kill <slug>                           # ペインごと畳む
ccs gc                                    # 死んだペイン・空の一時ディレクトリ・不要な worktree を掃除

追記(2026-08-18): 枠は --tmp で指す。 当初は <target> に tmp と書く形だけを 用意していたが、<target> の名前空間はリポジトリ名と共有している。ghq get someone/tmp をした瞬間、ccs new tmp がどちらを指すのかは打った人にしか分からない。オプションなら 名前空間の外なので、この曖昧さが起きない。

tmp も短い綴りとして残す(既存のドキュメントとハブのエージェントを壊さないため)。 ただし ghq に tmp というリポジトリがあるときだけは、同名リポジトリの衝突と同じく 候補を出して止める(§4.2)。

ccs new の中身(ここが設計の実体):

  1. <target> を解決 → 絶対パス(ghq list --full-path で照合。--tmp なら作業枠を確保)
  2. cc/<slug> が既にあれば、その slug を返して終了(冪等)
  3. trust を事前承認(§4.4)
  4. UUID を生成
  5. tmux new-session -d -s "cc/<slug>" -c <path> "claude --session-id <uuid>; exec $SHELL" (-n は渡さない。hub だけ例外。§4.2)
  6. レジストリに <uuid> が現れるまで待つ(数秒。タイムアウトしたらペインの内容を出して失敗)
  7. slug / uuid / tmux ターゲット / transcript パス を JSON で返す

末尾の exec $SHELL が重要: claude が終了してもペインが残るので、ccs resume で 同じ場所に claude --resume <uuid> を流し込める(§4.5)。

4.4 trust ダイアログの扱い

この節の結論は ADR-0001 で更新された。 いまの実装は案 A(ccs が起動前に hasTrustDialogAccepted を書き込む)で、 自動承認するのは ghq 配下と空の使い捨て作業枠だけ。案 B の「固定枠を人が一度だけ 信頼する」は、そもそも成立しなかった(下の 2026-08-18 の追記)ので採られていない。 固定 8 枠そのものも ADR-0001 で見直しが決まっている。以下は経緯として残す。

対話モードは trust エントリの無いディレクトリで止まる(§2.6)。取れる手は 3 つ:

案 中身 評価
A. 事前書き込み ccs new が ~/.claude.json の projects["<path>"].hasTrustDialogAccepted = true を立ててから起動 確実。ただし安全確認をツールが自動で潰すことになる
B. 一時ディレクトリを固定枠にする ~/.cc-scratch/{1..8} を最初の 1 回だけ人が信頼し、以後は使い回す ダイアログが一度も出ない。一時作業の分離は「使う前に中身を消す」で担保
C. send-keys で答える 起動後にペインを見て 1 Enter を送る 実測で動いたが、画面の見た目に依存する。脆い

(2026-08-17 時点の推奨)B + A の限定運用。 ghq 配下のリポジトリは既に人が信頼済みのものが 大半なので A はほぼ発火しない。一時作業は B の固定枠に閉じ込め、A を使うのは「ghq 配下の 未信頼リポジトリを明示的に指定したとき」だけに絞る。C は採らない。

B なら「一時ディレクトリを毎回作る」より良い副作用がある。枠が 8 本しかないので、 使い捨てセッションが無限に増えない。

追記(2026-08-18): 空の枠は ccs が自動で信頼する。 上の「最初の 1 回だけ人が信頼する」を 実機で回したところ、枠 1 本ごとに承認が要ることが分かった(枠 2 を取った ccs new tmp が 信頼ダイアログで固まり、30 秒待って「登録されませんでした」で終わる)。枠を何本も立てる 運用が実質できない。

枠は ccs 自身が作ったディレクトリで、渡すのは空だと確かめたときだけ (resolve_as_scratch)。つまり信頼確認が守ろうとしている「知らないコード」がそこには 存在せず、確認は情報を増やさない。ghq 配下と同じ理由で A を適用する (#6 の回答)。

中身のある枠を実パスで指されたときは自動承認しない。 そこにあるのは誰かが置いたコード なので、確認する意味がある。判定は「枠の直下」かつ「いま空」の両方を毎回見る。

追記(2026-08-20): 枠を固定にする理由から trust が抜けた。 上の 2026-08-18 の決定に よって、trust は起動前に ccs が書き込むものになった。枠が事前に作ってあることは、もう trust とは無関係である ── ensure_trust は tmux new-session より前に走るので、 渡すディレクトリが「1 分前に作ったもの」でも「1 週間前からあるもの」でも同じように通る (実測。ADR-0001)。

案 B の残る根拠は「枠を有限にして使い捨てセッションを増やしすぎない」だけになったが、 有限性は枠数ではなく確実な後片付けで担保するほうが良い、というのが ADR-0001 の決定。 判定条件(「枠の直下」かつ「いま空」)は変えない。作りたてのディレクトリは空である ことが保証されるので、自動承認の根拠はむしろ強くなる。

追記(2026-08-21、ADR-0002): 判定条件を言い換える。 作業ディレクトリに ccs が印(.ccs.json)を刻むと決めたので、「いま空」がそのままでは 成り立たなくなる。守っていたのは「ccs が作ったもので、まだ誰も何も置いていない」ことで、 「空」はそれを外から確かめるための代理指標だった。条件を 「枠の直下 かつ ccs の印がある かつ その印以外に何も無い」に置き換える。 印の有無を見るぶん判定は今より厳しくなる(人が手で作った空ディレクトリは自動承認 されなくなる)ので、安全側への変更である。

追記(2026-08-31、I2a): 先に「印を数えない」ほうだけ入れた。 置き換えは 2 段になる。

段 何 状態
I2a 「いま空」が正しい印 1 つだけなら空と数える 入った
I2b 発行を一意なディレクトリにし、印を書く 未着手
I2c 「印が無ければ承認しない」へ条件を締める 入った

3 段とも入った(2026-08-31)。 条件は「枠の直下」かつ「ccs の印がある」かつ 「その印以外に何も無い」。scratch root の直下に人が手で作った空ディレクトリは、 もう自動承認されない ── ccs が発行したものではないので、そこに何が置かれるかを ccs は知らない。

レガシー枠({1..8})にも印は無いので、ここでは承認されない。ただし実害はほぼ無い ── 一度でも使った枠は ~/.claude.json に信頼済みとして残っているので、ensure_trust は そもそもこの判定へ来ない。

順序に必然がある。 I2c を先に入れると、印を持たない新規枠が信頼されず起動時に必ず 止まる。逆に I2a だけなら、印を書く者がまだ居ないので挙動は 1 つも変わらない (緩めた側の条件が発火しない)。

正しい印だけを緩める。 kind が scratch、workspaceId がディレクトリ名と一致、 schema が整数。一致まで見るのは、素性が「そこに書いてあること」ではなく「そこと 結びついていること」で決まるから ── 印ごとコピーされたディレクトリが、元のものを 名乗れてはいけない。

ccs gc の空枠の掃除は rmdir なので、印が入ると失敗する。検証を通った印そのもの 1 つだけを先に消してから rmdir に渡す(rmdir は「そのどちらでもなかったときに 止まる」門として残す)。

追記(2026-08-31、I3a): ccs gc は作業枠を素性で 4 つに分ける (ADR-0002 決定 7)。

ディレクトリ 扱い
印だけ 消す
印と中身 報告だけ(issuedAt / issuedSlug を添える)
稼働中 触らない。報告もしない
印が無い / 壊れている 触らない。「素性の分からないディレクトリ」として報告

「空なら消す」をやめた。 以前は人が手で作った空ディレクトリまで消していた ── ccs が発行したものではないので、そこに何が置かれるかを ccs は知らない。 印を失っても安全側に倒れる。

レガシー枠({1..8})は印を持たないので、空でも消されない(報告はする)。 ADR の決定 8 は「今までどおり空なら rmdir」と書いており矛盾するので、 #91 に上げてある。

追記(2026-08-31、ADR-0004): 「tmp」が 2 つある。

パス 寿命
ccs の作業枠 ~/.cc-scratch/<workspaceId> ホーム配下。OS は消さない
ハーネスの scratchpad /private/tmp/claude-…/…/scratchpad/ /tmp 配下。macOS が数日で消す

Claude Code は一時ファイルを後者へ置くよう指示するので、「一時的な作業」と言われた エージェントはそちらを選ぶ。作業枠は消えないのに、そこに立てたセッションの成果物が 消える(実例は #63)。

穴の本体はここで、昇格コマンドは事後の救済にすぎない。枠に CLAUDE.md を置いて 「成果物は cwd に置く」と伝えるのが先(ADR-0004 決定 1、ROADMAP の P0)。

4.5 生死の扱い

  • 生きている = claude agents --json に載っている = ソケットがある = SendMessage が届く
  • ペインだけ生きている = claude が終了した状態。ccs ls は stopped と表示する
  • tmux サーバごと落ちた(再起動など) = 全滅。~/.claude/projects/**.jsonl は残るので 会話は失われない。tmux 自体の永続化(resurrect 等)は入れない — 復帰の権限は claude 側の resume が持っている

追記(2026-08-22): どちらも ccs restore が扱う(§10)。 当初案の「同ペインに claude --resume を送り込む」は採らなかった ── ペインを 使い回すと前の claude の出力が残ったまま新しいものが動く。hub と同じく 畳んでから立て直す。

4.6 ハブの役割とスキル

ccs はダムなツールにして、判断はスキル側に置く。

cross-session-hub スキルを書き直す(新設せず、既存を拡張)。理由は、いま書いてある 「新規セッションは開けない」という制約がこの設計で崩れるため、放置すると矛盾する。

追記・変更する内容:

  • 二層になったことを書く — ListAgents/SendMessage(全横断・要プロセス生存)と ccd_session_mgmt(デスクトップ限定・停止中も可・履歴が読める)の使い分け(§2.3 の表)
  • ccs new で立てられるようになったことを書く — ただし立てるのはユーザーが頼んだときだけ。 自発的に増やさない
  • 読み取り専用の原則は維持する。 副作用のある作業を他セッションに投げない現行ルールは、 新しく立てたセッションにも同じく効かせる。新規セッションに投げるのも「別セッションに 投げる」ことに変わりはない — 権限判断の主体がずれる問題(現行スキルの「安全策」節)は そのまま残る

4.7 置き場所

  • ccs の実体 → 新規リポジトリ ken-ty/ccs(ghq create で作る。ghq スキルの規約に従う)
  • スキル本体(cross-session-hub の改訂)→ agent-skills-store
  • agent-skills には置かない。 ROADMAP の確定方針に「管轄範囲は skills 限定。 dotfiles / harness には広げない」とあり、これはハーネス側のツール

5. 段取り案

# 内容 サイズ
1 ccs new / ls / attach / kill(ghq 解決 + 冪等性 + 命名規約) M
2 一時ディレクトリの固定枠(§4.4 案 B)と ccs gc S
3 cross-session-hub スキルの改訂(二層の使い分け + ccs の呼び方) S
4 ccs resume(停止中セッションの復帰) S
5 worktree 対応(ccs new <repo>@<branch>)。git-worktree スキルの規約に合わせる M

1 → 3 まで通れば、ハブから「新しいセッションを立てて仕事を頼み、結果を回収する」が回る。


6. 決定事項(2026-08-17)

項目 決定
ccs の置き場所 新規リポジトリ ken-ty/ccs(ghq create)
実装言語 POSIX sh / zsh。依存は tmux と jq のみ
v1 の範囲 段取り 1〜3。resume(4)と worktree(5)は v1 に含めない
trust の扱い ~~§4.4 案 B + 限定的な A。固定枠 ~/.cc-scratch/{1..8} は人が一度だけ信頼し、hasTrustDialogAccepted の自動書き込みは「ghq 配下の未信頼リポジトリを明示指定したとき」に限る~~ → Superseded by ADR-0001(2026-08-20)。「人が一度だけ信頼する」は実機で成立せず、2026-08-18 に案 A(ccs が起動前に書き込む)へ切り替え済み。固定 8 枠もやめる。案 C(send-keys で答える)を採らない判断だけは変わらない

決定から派生する実装上の縛り

  • jq を必須依存に加える。 claude agents --json を読むため。tmux ともども無ければ ccs は起動時に落として案内を出す
  • ~~v1 に resume が無い = claude が終了したペインは ccs ls に stopped と出るだけで、 復帰は人が ccs attach して手で claude --resume <uuid> を打つ。 uuid は ccs ls が表示する(表示していないと詰むので、これは v1 の要件)~~ → §10(2026-08-22)で ccs restore を入れた。 uuid を ccs ls が出す要件は そのまま残る(restore が拾えない相手は手で打つしかない)
  • ~~v1 に worktree が無い = 同一リポジトリに 2 本立てると同じ作業ツリーを共有する~~ → C1(2026-08-20)で解消。 ccs new <repo>@<branch> が作業ツリーごと分ける(§9.6)

残る判断(実装中に確認する)

  • ~/.cc-scratch の枠数 8 が妥当か。使ってみて増減する
  • ccs ls の出力形式(人が読む表 / ハブが食う JSON)。両方出す(--json で切替)方針で進める

8. hub と設定(2026-08-19 決定)

v1 は「ハブから子を立てる」までを作り、ハブ自身をどう保つかは範囲外にしていた。 スマホから使うと、ここが最初に壊れる ── アプリから届くのは生きているセッション だけなので、ハブが死ぬと ccs を叩く経路ごと消える。実装は hub。

8.1 決めたこと

項目 決定 理由
hub の識別 tmux <prefix><hub-slug>、-n <hub-slug>、--remote-control <hub-slug> も明示 -n で付けた名前は起動後に変わる(実測: レジストリの formerNames に tmp-1 → 朝会夕会 の遷移)。アプリ上で見失わないよう RC 名も固定する
hub の cwd 専用ディレクトリ(既定 ~/.cc-hub) 使い捨て枠に置くと tmp-N の名前空間と gc に巻き込まれる
「立った」の判定 レジストリ登録 かつ bridgeSessionId が非 null RC が付かないセッションはアプリに出ない=スマホからは死んでいるのと同じ。実測で null のまま残る個体があった
再起動時の会話 既定はまっさら。--resume で継続 落ちた原因が会話側なら、復元すると同じ理由で落ち続ける。直前の uuid は state.json に残す
自動起動 launchd / systemd の設定を 出すだけ。設置は人 インストールをツールにやらせない。ccs hub agent --print
自動起動の PATH 生成時に解決できた依存の在処をユニットに焼き込む launchd / systemd は対話シェルの設定を読まない。-lc を付けても ~/.zshrc は読まれず、Homebrew 前提の環境では tmux も claude も見つからない(実測。hands-on §11.1)
認証切れ 検知して止まる。自動で直さない /login は対話が要る。立て直しても同じ画面で止まり、API を叩き続けるだけ
暴走の歯止め 窓(既定 600 秒)で規定回数(既定 3)を超えたら止めて人を待つ 無限再起動は課金とレート制限に直結する
保護 ccs kill <hub> は --force でも拒否。gc の対象外。自セッションの kill は --force 必須(自分で終わると宣言する ccs kill --self は別口で、--force は要らない) ハブのエージェントが自分を落とすと、誰も起こせない
通知 ~/.cc-hub/hub.log(JSONL)のみ。外部送信はしない 外部送信は承認が要る操作。hub 自身に読ませられれば足りる

8.2 設定を外に出した理由

既定値は作者の環境に合わせてある。他人の環境ではぶつかる。 とくに hub はリポジトリ名としてありふれているので、予約語が固定だと詰む。

  • 優先順位は env > 設定ファイル > 既定値(ccs config が出どころを表示する)
  • 設定ファイルは ${XDG_CONFIG_HOME:-~/.config}/ccs/config。source しない ── 設定ファイルに任意のコマンドを書けると、設定と実行の境界が無くなる
  • 知らないキーは黙って無視せず警告する(綴り間違いが「設定したのに効かない」で終わらないように)
  • CCS_REMOTE_CONTROL=off を用意した。RC を使わない環境で no-rc 判定のまま 永久に立て直し続けるのを避けるため
  • ghq 依存はそのまま。 リポジトリ名の解決を任せている部分だけなので、 使わない人はパスで渡せばよい

8.3 名前の衝突

hub は <target> の名前空間をリポジトリ名と共有している(tmp と同じ問題)。

  • ccs new <hub-slug> は勝手に選ばず止め、3 つの逃げ道を出す (ccs hub up / <owner>/<repo> / CCS_HUB_SLUG を変える)
  • <owner>/hub を開いたときの slug は <owner>-hub。cc/hub を他のリポジトリに 使わせると、立てた瞬間に hub の生死判定が壊れるため

8.4 まだ実測できていないこと

fake claude では検証できず、本物で確かめる必要がある(hub)。 残っているのはアプリ側の見え方(名前が維持されるか、offline が積み上がらないか、 --resume で RC が張り直されるか)。

launchd から起動した ccs が手元と同じ tmux サーバに入るかは 2026-08-22 に 実測して入ることを確認した(hands-on §11.1)。 崩れると設計ごと組み直しになる項目だったので、ここは閉じた。


9. ccb のためのインターフェース(2026-08-20 決定)

ccb(private リポジトリ)は、タスクに Claude Code セッションを割り当てる ボード。設計の正本はあちらのリポジトリにある。 ここに書くのは 「そのために ccs へ何が要るか」だけ。

9.1 なぜ ccs に変更が要るのか

ccb の中心の等式は 1 タスク = 1 セッション = 1 worktree = 1 PR、終わったら畳む。 1 リポジトリに同時に複数のセッションが立つことになる。

いまの ccs は slug = リポジトリ名で冪等なので、ccs new x01 を何度打っても 1 本しか 立たない。1 リポジトリ 1 セッションの前提では正しい設計だが、タスク単位にすると 2 本目が立たない。ここが唯一の構造的なブロッカー。

9.2 必要な変更(P0)

すべて追加のみ。終了コード(0/1/2/3/4)・-- の意味・既定の出力・hub の互換は 一切変えない。

# 変更 なぜ要る
C1 worktree 対応 ccs new <repo>@<branch> §9.1。これが無いと成立しない(実装済み。§9.6)
C2 --label k=v(反復可)。tmux のユーザオプションに保存し、ls --json が返す タスクとセッションの紐付け
C3 ls --json の情報追加 — idle/busy・startedAt・pid・transcript・labels・worktree 盤面に出す。すべて既存の情報源から引ける
C4 --session-id の外部指定と--prompt-file 起動前に行を作れる。タスク仕様は数 KB の複数行になり、argv では引用と長さで壊れる
C5 new --json で失敗理由も機械可読に いまは人間向けの文が stderr、終了コード 1 だけ。盤面に理由を出せない

新しい env を足すときは CCS_CONFIG_KEYS に載せて ccs config に出す(§8.2)。

9.3 ラベルを tmux のユーザオプションに置く

新しいファイルが増えず、セッションと同時に消えるので後片付けが要らない。

§3 で devas.life 版の @claude_state を退けたのは「状態を hook で書く」ことに 対してで、状態は Claude 本体が持つ。ここで置くのは ccs 自身が作った出自情報なので、 退けた対象とは別物。混同しないこと。

9.4 ccs はタスクを知らないままにする

C2 のラベルは ccs にとってただの文字列。解釈しない。 知らせると「GUI 無しでも ccs が使える」が崩れ、§1 の 4 責務が 5 つ目に膨らむ。

9.6 worktree の実装(C1、2026-08-20)

決めたこと 内容 理由
置き場所 <repo>/.worktrees/<branch-slug>(リポジトリ配下) W2、2026-08-26(ADR-0003 決定 1・2)。~~ghq root の外に置く~~ の根拠は兄弟配置(<repo>.worktrees/<name>)の観測で、配下には当てはまらなかった ── ghq は .git を見つけた時点で降りない(実測)。親の作業ツリーは .git/info/exclude の /.worktrees/ で塞ぐ。ドット始まりは飾りではない — gitignore を読まない道具(go build ./...・pytest・analyzer・rg の hidden 既定)への独立した防御
ブランチの slug / を - に潰す。同じ場所に別のブランチが座っていれば落とす slug とディレクトリ名を一致させるため。~~feat と feat/login が同居できない~~ はgit 自身が禁じるので起きない(実測)。潰したことで生まれる feat-foo と feat/foo の衝突のほうが実在するので、ensure_worktree が既存の HEAD を見る(ADR-0003 決定 5)
分割 <target> の 最後の @ で割る リポジトリ名に @ が入ることはあるが、ブランチ名の @ より後ろには来ない
実在するディレクトリ パス指定が優先 foo@2 のようなディレクトリ名は珍しくない。実物があるならそれを指している
作るタイミング ccs new だけ。ccs resolve は作らない 作業枠(--tmp)は「空いている枠はどれか」で識別子が決まるので解決時に枠を取る必要があるが、worktree は repo と branch から一意に決まる。解決に副作用を持たせない
ブランチが無いとき 作る(worktree add -b)。あるときは開く タスクごとにブランチを切る運用が主な用途。既存を -b で作ろうとすると落ちるので、先に show-ref で見る
trust 元のリポジトリが ghq 配下なら、その worktree も自動で信頼する そこにあるのは人が意図して clone したリポジトリの中身そのもので、確認が守ろうとしている「知らないコード」ではない(§4.4 の ghq と同じ理由)。これが無いとタスクごとに 30 秒の信頼確認で固まる
素性 git に訊く(--git-dir と --git-common-dir の食い違い)。パスの形からは判断しない W1、2026-08-26(ADR-0003 決定 3)。置き場所の前方一致で当てていた頃は、根の外に置かれた worktree を素通しし、実パスで指されると素性が分からなかった(ADR-0002 表 #9)。git worktree list で判定してはいけない — submodule の中から呼ぶと作業ツリーではなく git-dir を先頭に並べる(実測)
入れ子 worktree を指されたら本体に読み替える(拒まない) ADR-0003 決定 4。拒否は人が打ち直す前提で、入れ子を作れる形が残る。読み替えれば原理的に生まれず、slug も本体から決まるので自動で正しくなる。主経路(ghq list 経由)はそもそもここに来ない
後片付け ccs kill は worktree を消さない。撤去は ccs gc が 4 分類で扱う W3、2026-08-26(ADR-0003 決定 7)。そこに未コミットの作業が残っていることがあり、消す判断は人がする。kill は片付け先を 1 行案内するだけ。gc は git worktree remove → git branch -d を force 無しで直列に通す ── この 2 つが素で拒むことが安全性の本体で、「未 push かつ未マージ」は自動では消えない

CCS_WORKTREE_ROOT は無い(W2 で削除。ADR-0003 決定 6)。 置き場所がリポジトリのパスから導出されるので、設定で動かす余地も、テストの差し替え点も要らない ── テストが作るリポジトリはサンドボックスの中にあり、worktree もそこに落ちる。

git は必須依存に入れない。 @ を使ったときだけ要る。CCS_GIT_BIN で差し替えられる。

9.5 デーモンは ccs 側に作らない

盤面の更新は ccb が ccs ls --json をローカルでポーリングし、差分を push する。 ポーリングするのは ccb であって ccs ではない。 §3 の「デーモンを作らない」はそのまま維持される。

tmux を直接触るのも ccs だけ。ccb は ccs ... --json を子プロセスで叩く (真実の源を 2 つにしないため)。


10. restore(止まったセッションの立て直し、2026-08-22 決定)

v1 は resume を範囲外にした(§6)。実機で詰まったのは PC の再起動。 hub は自動起動で戻るが、他のセッションは誰も立て直さない ── アプリの一覧から 消えたままになる。手で 8 本戻せることは確認した(tmux new-session -d -s cc/tmp-N -c <枠> "claude --resume <uuid>")ので、それを ccs の側へ持ってくる。

運用の説明は restore.md。ここにはなぜその形なのかだけを残す。

10.1 列挙をどこから作るか

ccs は状態ファイルを持たず tmux から導出する(§2.1)。だから「さっきまで何が 立っていたか」を誰も覚えていない。

レジストリ(~/.claude/sessions/<pid>.json)は当てにならない。再起動を跨いで 残っていなかった(実測。落ちた 8 本のぶんは 1 つも残っていなかった)。 pid をキーにしたファイルなので、仮に残っても再起動後は pid の再利用と衝突する。

覚える代わりに、ccs 自身が場所を決めているところを舐める。

# 何 根拠
1 止まったペイン(cc/ はあるが claude が死んでいる) tmux 名が cc/ であること自体が「ccs が立てた」証拠
2 CCS_SCRATCH_ROOT/<n> の会話ログ 枠は ccs が作って ccs しか渡さない
3 ghq 配下の <repo>/.worktrees/<branch> の会話ログ 同上(置き場所を ccs が決めている。§9.6)。舐めるべき「根」が無いので、ghq list の各リポジトリに .worktrees/ があるかを見て回る
4 ghq 配下で、印のある会話ログ 下記(2026-08-23 に追加)

追記(2026-08-23): ghq 配下も列挙するようにした。

当初は「ghq 配下は列挙しない」と決めていた。そこの会話ログは ccs が立てたものとは 限らないため ── レジストリ全件で entrypoint が cli / claude-desktop / claude-vscode に分かれることは確認済み(2026-08-19)。

実機で足りなかった。 再起動のあと、hub と作業枠は戻ったがリポジトリ単位の セッションだけが戻らない。名指しすれば戻せるが、何が落ちたかを人が覚えていないと 名指しできない ── ccs が状態を持たないことの裏返しがここで効いた。

区別する印が会話ログの中にあった。 当時 ccs new は claude -n <slug> を渡していたので、 本物は会話ログの1 行目に custom-title を書く。アプリや VS Code から開いた セッションは名前が自動で決まるため、この行が先頭に来ない。

実測(2026-08-23、会話ログ 247 本): 立っている ccs セッション 12/12 に印があり、 デスクトップから開いたセッションには無かった。

アプリ側の実装に依存した判定である。 外れたときに倒れる側を選んである ── 印が付かなくなれば「候補に出ない」(名指しは従来どおり効く)。黙って知らない会話を 立てる側には倒れない。 加えて会話ログの cwd による答え合わせ(§10.2)も通る。

ghq の外に立てたセッションは列挙できない(ghq list -p を舐めるため)。 これは名指しで戻す。

状態ファイルを足して「最後に生きていた slug」を記録する案は採らない。 記録を正にすると、記録と実体がずれたときにどちらが正か決められなくなる (§2.1 で「真実の源を 2 つにしない」と決めた理由がそのまま効く)。

10.2 エンコード規則への依存

会話ログの置き場所は cwd の英数字以外を - に潰したもの(§2.7 で実測)。 この規則は逆算できない — / も . も _ も同じ - に潰れる。

claude --resume の一覧に頼る手も無い。値なしの --resume は対話ピッカーを 開くだけで、機械可読な一覧を出すサブコマンドは存在しない(claude --help で確認、 2.1.239)。

そこで規則は前向きにだけ使い、当たった会話ログの中の cwd で答え合わせをする。 食い違ったら黙って別の会話を戻す代わりに、その 1 本を飛ばして理由を出す。 規則が変わったらここで気づける。

10.3 自走の再開を止める手段は無い

戻した会話は、落ちる直前に続きがあればその場で走り出す(実測で 1 本が busy に なった)。何をするかは会話の側が持っているので、ccs から抑えられるのは 「戻す本数を人が選べること」だけ。したがって:

  • 既定は dry-run(ccs gc と同じ作法)。--yes を付けるまで何も起きない
  • 名指しで 1 本ずつ戻せる

10.4 ccs hub up に混ぜない

hub up は launchd / systemd から 5 分おきに走る(§8)。そこで他のセッションまで 戻すと、人が畳んだものが 5 分後に生き返る。 ccs は状態を持たないので、durable な 情報からは「畳んだ」と「落ちた」を区別できない ── 区別できるのは打った本人だけ。

代わりに、hub を実際に立て直したときだけ(=落ちていたとき。再起動の直後が まさにこれ)「他に止まっているものがある」旨を 1 行出す。ログイン時だけ復元する案は 採れない ── 自動起動のユニットはログイン時も定期実行も同じコマンドを叩くので、 ccs 側から両者を区別できない。

10.5 会話の名前を上書きしない

ccs new は名前を渡さず(hub だけ例外)、restore も渡さない。戻す会話には 既に名前が付いていて、アプリの一覧に出ているのはその名前(実測。手で戻した 8 本は 元の名前のまま戻った)。slug を被せると、戻した瞬間に tmp-2 へ改名され、探している 名前のほうが消える。

hub だけは逆で、hub restart --resume は -n hub を渡す ── hub は名前が動くと 見失うので、会話側の名前より固定名が優先される(§8.1)。

10.6 派生する実装上の縛り

  • pane_start_command から uuid を拾う正規表現が --resume も見る必要がある。 restore と hub restart --resume は --session-id ではなく --resume で立てるので、 片方しか見ないと 一度戻したセッションの uuid が二度目から引けない (ccs ls の stopped 行が - になり、復帰の手掛かりが消える)
  • 古さの上限が要る。 会話ログは消えないので、上限が無いと数か月前に使い終えた枠まで 候補に並ぶ(CCS_RESTORE_MAX_AGE、既定 7 日)。列挙にだけ効かせる ── 名前で 指したものは古くても戻す

11. adopt(管轄外のセッションの引き取り、2026-08-30 決定)

ccs ls は cc/ のものしか出さない(§4.2)。では管轄外のセッションを管轄下へ 移せるか、という問い。

11.1 プロセスは移せない

tmux セッションを rename しても、レジストリの tmux 欄は追随しない(実測 2026-08-30: cc/ccs → cc/ccs-probe にしたところ、busy で動いているセッションが ccs ls に stopped と出た)。あの欄は起動時に 1 度書かれるだけで、claude 自身が 書く(§2.1)。つまり cc/<slug> を名乗るには、claude がそのペインの中で起動する しかない。

だから引き取りは会話の引き取りになる ── 元を閉じ、同じ sessionId を --resume で cc/<slug> のペインに開き直す。立てるところは §10 の restore と 同じ手順で足りる。違うのは相手が生きていることだけ。

11.2 元が生きている間は絶対に立てない

claude --resume は、その会話を別プロセスが握っていても拒まない(実測 2026-08-30)。同じ sessionId のレジストリが 2 件並び、しかも引き取った側は bridgeSessionId が null になる ── 先に居るほうが Remote Control を譲らず、 ペインにこう出る:

another Claude Code on this machine (started 14s ago) already has
Remote Control for this conversation

RC が付かないことは「アプリからもハブからも見えない」と同じ(§8)。つまり 成功したように見えて、見えないセッションができる。門が要るのはこのため。

閉じ方は SIGTERM を送り、レジストリファイルが消えるのを待つ。claude は正常終了 のときだけ自分の json を消すので、消えたこと自体が「会話ログを書き切って終わった」 の合図になる。SIGKILL には昇格しない ── 書き切られていない会話が残ると、いま 戻そうとしている先が壊れる。待ちきれなければ何もせず終わる(元は生きたまま)。

頼まれた側が自分で閉じる口は ccs kill --self(管轄外でも使える)。送るのは ccs の仕事ではない ── セッション間通信は組み込みに委譲する(§3)ので、 「引き取るので閉じて」と頼むのは呼ぶ側(hub)。

11.3 緩く見つけて、相手の綴りで動く

照合は cwd を正規化して当てる(ccs adopt /tmp/x が /private/tmp/x の セッションを見つけられないと、名指しできない相手ができる)。そのあとは相手が 名乗る cwd の文字列をそのまま使う ── 会話ログの置き場所は cwd の文字列から 決まる(§10.2)ので、正規化した綴りで探すと相手が実際に書いた場所を見に行かない。 立てるときも同じ綴りで -c する。違う綴りで立てると、同じ会話が 2 か所に割れる。


12. agents(俯瞰、2026-08-30 決定)

12.1 ccs ls には混ぜない

管轄外を ccs ls に出す案(--all を含む)は採らない。ls の行が kill / gc の対象と一致していること自体が、いまの安全性の根拠だから(§4.2・§10.1)。 混ぜると、人の判断も ccs ls --json | jq 系も、他人のセッションを巻き込む方向に 倒れる。俯瞰は別の動詞にする。

claude agents と同じ綴りにしてあるのは、これが「ccs の管轄の一覧」ではなく 「claude の母集団を ccs の目で見た表」だと名前で分かるようにするため。

12.2 区別は SLUG の - だけ

専用の MANAGED 列を足さない。- は「取れない値」の綴りとして既に使っている (collect_session_row)し、SLUG が無い = attach も kill も宛先が無いが そのまま読める。

管轄外の name を SLUG 欄に置くのはしない。実測で 8/26 ヨドバシエアコン tmp-7 のように空白・日本語・/ を含む名前が付くので、打てない文字列が打てる 位置に座ることになる。

並びは管轄ぶんが先。上半分が ccs ls と行単位で一致するので、2 つの表の 関係が説明なしで読める。

JSON では管轄外の slug と tmux を null にする。-(値が取れなかった)と null(そもそも無い)を分ける ── 管轄外のセッションに ccs の handle は 存在しない。

12.3 見えるのはこのマシンの claude だけ

ListAgents はサブエージェントもクラウドも他マシンも並べるので母集団が違う。 実測 2026-08-30 ── レジストリ 20 / claude agents --json 20 に対し、 ListAgents は 39。ヘルプにこれを書く。「全部見えている」と思って居ないものを 探すのが、いちばん高くつく誤解になる。

12.4 この join を ccb に持たせない

「どれが管轄か」の判定に使う接頭辞は CCS_PREFIX という設定キーなので、 ccb 側にハードコードさせると設定変更で黙って壊れる。そもそも claude agents --json は tmux 欄を持たない(§2.1 の訂正)ので、ccb 単独では 書けない。


13. アプリで閉じられたセッション(2026-09-11 決定)

症状: claude.ai / スマホアプリで「アーカイブ」「削除」しても、tmux の claude は 生き続ける。ccs ls に idle で並び、再起動のあとは ccs restore --last が 「一緒に落ちた組」として戻す。人はアプリで畳んだつもりなので、「終わったのに残る」 「畳んだのに戻る」として現れる(2026-09-11 実測: 生きている 24 本のうち 4 本がこの状態。 畳んだ 5 本のうち 4 本にも同じ痕跡があった)。

13.1 何が起きているか(実測)

Remote Control サーバーはアーカイブされたセッションへの接続に 4090 session_not_active を返す。claude はそれを session_archived と分類し、Remote Control だけを切る。 プロセスは残る。痕跡は 2 つ:

どこ 何
会話ログ {"type":"system","subtype":"informational","content":"Remote Control disconnected — this session was ended or archived from another device or app (code 4090)", …}。削除は … the server no longer reports this session — it may have been deleted …
レジストリ bridgeSessionId が null になる

13.2 判定

会話ログの行を読み、生きているならレジストリを重ねる。

  • 会話ログに disconnect の行があり、それより後に人が打った依頼が無い → 閉じられている
  • 生きているセッションは、さらに bridgeSessionId が null であること

片方では足りない理由が 2 つある。(a) 立て直すと同じ id で付き直る ── 一度アーカイブ した会話を ccs restore で開くと、Remote Control は bridge_session_unarchive で 同じ id のまま復活する(会話ログの bridge-session 行の id は変わらない)。 だから「別の id が付いたら付き直り」では見分けられず、人が続けたかを見る。 (b) null になる経路は他にもある ── 同じ会話を 2 本開いたときの取り合い(§11.2)。 会話ログの行が無ければ、閉じたのではない。

hub からのメッセージは「人が打った」に数えない。 閉じられたあとも棚卸しは届き、 claude は答える。それを「続けている」と読むと、閉じたものが永久に閉じていないことになる。 これに合わせて、ccs ls -l の REQUEST も hub からのメッセージを定型文として落とす (定義は CCS_LS_HUMAN_JQ に 1 本化)。

13.3 どこで使うか

どこ 何をするか
ccs ls -l STATUS に archived / deleted。--json に closed。既定の ccs ls は変えない
ccs gc 「アプリで閉じられたセッション(畳む対象)」に並べ、--yes で畳む。畳んだら「終わった」の記録(R6)に入れる
ccs restore 列挙では戻さず「戻せません」に理由つきで出す。名指しと --all は戻す

4 責務の外に出ていない。 読んでいるのは claude が書く定型の system 行で、 custom-title の痕跡(§10)と同じ種類。人の依頼の中身は解釈しない。

7. 実測に使ったコマンド(再現用)

claude agents --json | jq -r '.[] | "\(.pid) \(.name) \(.cwd)"'
for f in ~/.claude/sessions/*.json; do jq -r '"\(.name)\ttmux=\(.tmux // "-")\tstatus=\(.status // "-")"' "$f"; done
tmux new-session -d -s probe1 -c "$PWD" "claude -n probe-tmux --permission-mode plan; exec zsh"
tmux capture-pane -p -t probe1 | tail -20
claude -p --session-id "$(uuidgen | tr 'A-Z' 'a-z')" --output-format json "Reply with exactly: OK"