Claude が動いてるって、ccmux はどう気付いてるの?

ペインで claude と打つと枠がオレンジになる。種は、昔のターミナルが使っていた小さな「ささやき」でした。

ccmux のペインで claude と打つと、少ししてから枠がオレンジになります。

┌─ ● claude [1] ─────────────────┐ ← オレンジ
│ Claude Code v2.1.104 │
│ What can I help with? │
└────────────────────────────────┘

ここで種明かしを一つ。Claude Code 側には何も仕込んでません。ccmux がただ、Claude Code のおしゃべりを盗み聞きしているだけです。

昔のターミナルには「タイトルバー」があった

今の iTerm や Windows Terminal は、タブに小さく現在のディレクトリが出たりしますね。昔の xterm / gnome-terminal はウィンドウ全体のタイトルバーにそれを出していました。

そこに表示する文字列は、プログラム側から特殊なエスケープシーケンスで送ります:

\x1b]0;タイトル文字列\x07

これが OSC 0(Operating System Command 0、ターミナルへの命令の一種)。\x1b]0; で始まって \x07 で終わる、という約束事です。

Claude Code は起動時に、自分の名前をこの OSC で投げています:

\x1b]0;Claude Code v2.1.104\x07

ふつうのターミナルならタイトルバーに表示されるだけ。ccmux は、ここを盗み聞きしています。

横取りは短い関数ひとつ

PTY(シェルと会話するための擬似端末)から届くバイトを vt100 パーサ に流す途中で、OSC 0/2 だけ取り出して覚えておく:

src/pane.rs
fn extract_osc_title(data: &[u8]) -> Option<String> {
let s = std::str::from_utf8(data).ok()?;
for marker in &["\x1b]0;", "\x1b]2;"] {
if let Some(start) = s.find(marker) {
let rest = &s[start + marker.len()..];
let end = rest.find('\x07')?;
return Some(rest[..end].to_string());
}
}
None
}

判定は 1 行

覚えておいたタイトルに claude が含まれるか、見るだけ:

pub fn is_claude_running(&self) -> bool {
self.title.lock().ok()
.map(|t| t.to_lowercase().contains("claude"))
.unwrap_or(false)
}

枠の色を決める関数はこれを呼んで、オレンジかグレーかを返す。本当にこれだけ

気持ちいいところ

この仕組みが好きな理由は、

  • Claude Code に一切触ってない — プラグインも設定もゼロ
  • ユーザーが何もしなくても動く — インストールすれば終わり
  • 他のツールでも応用できるnodepython も同じように自分の名前を流している

「一次情報(Claude Code 自身のシグナル)を素直に拾う」のが、結局いちばん安定する。この姿勢は ccmux 全体に通底しています。

落とし穴

状況結果
vim でファイル編集中タイトルが pane.rs などに変わる(Claude 判定が外れる)
bash で PS1 にタイトル命令を仕込んでる人Claude 起動中は Claude Code が上書きするので問題なし
PowerShell ネイティブOSC 0 を流さないので判定できない(Git Bash 経由ならOK)

「動いてるか判定する API が欲しい」と考えがちですが、実はターミナルの古い習慣の上に乗るだけで十分だった、という話でした。次は、ペーストで事故る「Enter が勝手に走る」問題の話を書きます。