技術 約6分で読めます

Ghosttyの全タブがClaude Codeになる問題をzshフックで直す

いけさん目次

Ghosttyで複数プロジェクトを開いて、各タブに2〜3ペインずつ切ってClaude Codeを走らせると、タブバーが全部 Claude Code になる。
元記事の Liran Baba 氏は、4つのGhosttyタブを開いていた朝に claudoscope のテスト実行中タブを探して、タブ切り替えとペイン確認を何度も繰り返していた。

tmuxでClaude CodeとCodexを連携させた自動ループや、かなチャット v3のように、複数のAI CLIを同時に立てる運用では、いま見ているペインがどのリポジトリで、AIエージェントが走っているのかをタブバーで読めないと普通に迷う。

タイトルを書き換える相手が二重にいる

Liran Baba の記事で整理されていた原因は2つある。
Ghosttyのshell integrationが、フォアグラウンドプロセス名をターミナルタイトルに入れる。
プロンプトなら zsh、Claude Code実行中なら claude、Node実行中なら node という具合だ。

もう1つがClaude Code側のタイトル更新。
Claude Codeはターミナルタイトルを自動更新し、会話コンテキストに基づいたタイトルを出す。
公式の環境変数一覧にも CLAUDE_CODE_DISABLE_TERMINAL_TITLE があり、1 にするとこの自動更新を止められる。

Ghostty側だけを止めても、Claude CodeがOSC 2で上書きする。
Claude Code側だけを止めても、Ghosttyのshell integrationがフォアグラウンドプロセス名へ戻す。
両方を退かせてから、自分のzshフックでタイトルを決める、という順番になる。

Ghosttyにはタイトルだけ触らせない

Ghostty側は shell-integration-features = no-title を入れる。
shell integration全部を切るのではなく、タイトル更新だけ外す設定だ。

shell-integration-features = no-title

Ghosttyのshell integrationには、プロンプト境界の把握、作業ディレクトリ引き継ぎ、jump_to_prompt、SSH関連など、タイトル以外の便利機能もある。
全部切る shell-integration = none ではなく no-title にするほうが副作用が小さい。

macOSでは設定ファイルの置き場所も確認する。
元記事では ~/.config/ghostty/config~/Library/Application Support/com.mitchellh.ghostty/config のどちらをGhosttyが見ているかに触れていた。
dotfiles管理したいならXDG側へ寄せ、Application Support側からシンボリックリンクする運用が扱いやすい。

Claude Codeには環境変数で黙ってもらう

Claude Code側は ~/.claude/settings.jsonenv に入れる。

{
  "env": {
    "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1"
  }
}

シェルでexportしてから claude を起動してもいいが、毎回の起動で忘れないようにするならsettings側が楽だ。
この変数は公式ドキュメント上では「会話コンテキストに基づく自動ターミナルタイトル更新を無効化する」設定として載っている。

CTXでClaude Codeに動くメモリを足すでも書いた通り、Claude Codeのフックや設定は便利な一方で、既存設定との衝突が見えにくい。
settings.json をすでに共有しているなら、タイトル用のenvだけを雑に足すのではなく、ユーザー設定、プロジェクト設定、ローカル設定のどこで効かせたいかを分けたほうがいい。

zshのpreexecで起動中だけ印を付ける

ここから先はzsh側でタイトルを自分で書く。
元記事の30行フックは、gitリポジトリ名を通常タイトルにし、claude 起動時だけ印を付ける構成だった。
絵文字を使うと環境によって幅が揺れるので、ここでは CC にしている。

autoload -U add-zsh-hook

function _ghostty_context() {
  local ctx
  ctx=$(git rev-parse --show-toplevel 2>/dev/null) && echo "${ctx:t}" || echo "${PWD:t}"
}

function _ghostty_set_title() {
  printf '\033]2;%s\007' "$(_ghostty_context)"
}

function _ghostty_set_title_for_cmd() {
  local cmd="$1"
  local ctx="$(_ghostty_context)"

  case "$cmd" in
    claude|claude\ *|clauded|clauded\ *)
      printf '\033]2;CC %s\007' "$ctx"
      ;;
  esac
}

add-zsh-hook chpwd   _ghostty_set_title
add-zsh-hook precmd  _ghostty_set_title
add-zsh-hook preexec _ghostty_set_title_for_cmd

chpwdcd した直後に走る。
別リポジトリへ移動した時点でタブ名が変わる。

preexec はコマンド実行直前に走る。
ここで claudeclauded を拾い、リポジトリ名の前に CC を付ける。

precmd は次のプロンプト表示前に走る。
Claude Codeが終了したあと、タブ名をリポジトリ名へ戻す役割になる。

zshの preexec はalias展開後のコマンド行を見る。
clauded='claude --dangerously-skip-permissions' のようなaliasなら claude * で拾える。
関数やスクリプトとして clauded を作っている場合はalias展開ではないので、clauded|clauded\ * を残しておく意味がある。

split単位の限界は残る

元記事で一番大事だった制限は、GhosttyのOSC 2が永続的なタブ単位タイトルではなく、surface、つまりsplit側のタイトルに寄る点だ。
タブに3つのsplitがあり、1つだけClaude Codeを走らせている場合、タブバーの表示はフォーカス中のsplitに引っ張られる。

これはzshフックでは直せない。
Ghostty側で永続的なタブレベルタイトルを持つか、タブ色をOSCで変えられるようになるまでは、1タブ1プロジェクト、1split1役割くらいまでが扱いやすい線になる。
Ghostty discussion #7581 と issue #12235 がこのあたりの未解決点として挙げられている。

AI CLIを増やすなら、case文に geminicursor-agentcodex を足せば同じ考え方で動く。
ただ、タブタイトルに詰め込みすぎるとまた読めなくなる。
自分なら、通常時はリポジトリ名、Claude Code実行中だけ CC repo、危険な権限モードは CC! repo くらいに留める。

セッション管理より手前の視認性

Claude Codeのコンテキスト劣化では、長時間セッションで同じファイルを読み直したり、前に捨てた案へ戻ったりする兆候を書いた。
タブタイトルが全部同じになる問題は、モデル側ではなく人間側のコンテキスト劣化に近い。
目の前のタブがどのリポジトリか分からないまま、別のセッションへ指示を投げる事故が起きる。

Claude CodeやCodexを複数走らせるなら、タブ名、tmuxセッション名、ログファイル名、git worktree名を同じ粒度で揃えたほうがいい。
フックでタブ名だけ直しても、ログや作業ディレクトリの命名がばらばらだと、結局どこかで迷う。

参考