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 だけ取り出して覚えておく:
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 に一切触ってない — プラグインも設定もゼロ
- ユーザーが何もしなくても動く — インストールすれば終わり
- 他のツールでも応用できる —
nodeやpythonも同じように自分の名前を流している
「一次情報(Claude Code 自身のシグナル)を素直に拾う」のが、結局いちばん安定する。この姿勢は ccmux 全体に通底しています。
落とし穴
| 状況 | 結果 |
|---|---|
| vim でファイル編集中 | タイトルが pane.rs などに変わる(Claude 判定が外れる) |
bash で PS1 にタイトル命令を仕込んでる人 | Claude 起動中は Claude Code が上書きするので問題なし |
| PowerShell ネイティブ | OSC 0 を流さないので判定できない(Git Bash 経由ならOK) |
「動いてるか判定する API が欲しい」と考えがちですが、実はターミナルの古い習慣の上に乗るだけで十分だった、という話でした。次は、ペーストで事故る「Enter が勝手に走る」問題の話を書きます。