iTerm2のタブを左に2行表示し、色でClaude Codeの状態まで一目で見分けたい
はじめに
こんにちは、Androidエンジニアの@syarihuです。
皆さんはターミナルは何を使っていますか?Warp、Ghostty、Alacrittyなどさまざまな選択肢がありますが、筆者は結局iTerm2が一番使いやすいと感じています。Split Panes、プロファイル、Shell Integrationなど機能が豊富で、長年の安定した開発が続いているのが魅力です。
ただ、Claude Codeで複数のタスクを同時に動かすようになってから一つ困ったことがあります。複数のタスクを並行して作業していると、どのタブで何をしているのか分からなくなるのです。Claude Codeが自動でタブタイトルを書き換えてしまうこともあり、気付いたら全タブが似たようなタイトルになっていることも珍しくありません。
本記事では、iTerm2のタブカスタマイズ機能とPythonプラグインを組み合わせて、タブを一目で識別できるようにした方法を紹介します。

ThemeをMinimalにする
まず、ThemeをMinimalまたはCompactにします。タブタイトルを複数行にしたとき、この2つ以外だと表示が変になるのでどちらかが良いと思います。

個人的にはMinimalの方がタブの境界線がなくなりかなり見やすくなると思っています。
タブを左に配置する
次に、タブを画面上部ではなく左側に配置します。

Settings → Appearance → Tab bar location を Left に変更するだけです。

ただし、これだけでは不十分でした。iTerm2標準で設定できるタイトル(Session Name、Profile Nameなど)では、複数タスクを区別するのに十分な情報が表示されません。
タブの高さとフォントサイズを変える
タブを左に配置しても、デフォルトの状態ではタブの高さが狭く、テキストも小さくて見づらいです。
実はiTerm2では、設定でタブの高さやフォントサイズを変更できます。
タブの高さ
Settings → Advanced で「height」などで検索すると、次の設定項目が見つかります。

| 設定項目 | 説明 |
|---|---|
| Default tab bar height | タブの高さ |
筆者は 80 に設定しています。これで2行のテキストが余裕を持って表示されます。
タブのフォントサイズ
同じくAdvanced設定にフォントサイズの項目があります。

| 設定項目 | 説明 |
|---|---|
| Custom tab label font size | タブのフォントサイズ |
筆者は 18 に設定しています。
タブタイトルに改行を入れる
iTerm2のタブタイトルは、実は改行に対応しています。タイトル文字列に \n を含めると、そのまま改行して表示されます。
これにより、たとえば次のような2行表示ができます。
1行目: 作業内容の概要
2行目: ブランチ名 / リポジトリ名
1行目で「何をしているか」、2行目で「どのプロジェクトのどのブランチか」が分かるので、複数プロジェクトを並行していても一目で区別できます。
Pythonプラグインでタブタイトルを自由に設定する
iTerm2にはPython APIが用意されており、Title Provider というカスタムプラグインを書くことで、タブタイトルの表示内容を自由にコントロールできます。
仕組み
Title Providerの仕組みは次の3ステップです。
- Pythonスクリプトで「Title Provider」を登録する
- iTerm2のユーザー変数(
user.project)にタイトルをセットする - Title Providerがユーザー変数を参照してタブタイトルとして表示する
Claude Codeが書き換えるのは autoName という変数なので、ユーザー変数を参照するTitle Providerを使えば影響を受けません。
Title Providerの実装
次のPythonスクリプトを ~/.config/iterm2/Scripts/AutoLaunch/project_title.py に配置します。
import iterm2
@iterm2.TitleProviderRPC
async def project_title(
auto_name=iterm2.Reference("autoName?"),
project=iterm2.Reference("user.project?")):
if project:
return project
return auto_name if auto_name else "shell"
async def main(connection):
await project_title.async_register(
connection,
display_name="Project Name",
unique_identifier="com.syarihu.project-title")
iterm2.run_forever(main)
ポイントは次のとおりです。
-
@iterm2.TitleProviderRPCデコレータでTitle Providerとして登録 -
user.project?の?は変数が未定義でもエラーにならないための記法 -
user.projectがセットされていればその値を表示し、なければデフォルトのタイトル(autoName)にフォールバック
セットアップ手順
セットアップは次の手順で行います。
- iTerm2のメニューから Scripts → Manage → Install Python Runtime を実行
- 上記スクリプトを
~/.config/iterm2/Scripts/AutoLaunch/project_title.pyに配置 - iTerm2を再起動
- Settings → Profiles → General → Title のドロップダウンから
Project Nameを選択
ユーザー変数のセット方法
Title Providerが参照する user.project 変数は、iTerm2のエスケープシーケンスでセットします。
# 値はbase64エンコードが必要
printf "\033]1337;SetUserVar=%s=%s\007" "project" \
"$(printf "%b" "作業内容の概要\nブランチ名 / リポジトリ名" | base64)"
エスケープシーケンスの構造は次のとおりです。
-
\033]1337;— iTerm2独自のエスケープシーケンス開始 -
SetUserVar=変数名=base64エンコード値— ユーザー変数の設定 -
\007— エスケープシーケンス終了(BEL文字)
printf "%b" で \n を改行として解釈させてからbase64エンコードすることで、改行を含むタイトルをセットできます。
シェル関数でもっと便利に
毎回エスケープシーケンスを直接書くのは面倒なので、シェル関数にまとめると便利です。iTerm2のShell Integrationを導入していれば、iterm2_set_user_var 関数が使えます。
# Shell Integrationの読み込み
source "${HOME}/.iterm2_shell_integration.zsh"
# ユーザー変数をセット
iterm2_set_user_var project "my-project-name"
筆者はfzfでプロジェクトを選択してClaude Codeを起動する cl コマンドを自作し、起動時に自動でタブタイトルを設定するようにしています。ただしこの方法だと、タイトルがセットされるのはあくまで起動時の一度きりです。作業を進めるうちに扱っているブランチや内容は変わっていくので、途中でタイトルを更新したくなる場面もあります。
cl() {
local dir
dir=$(find ~/git -name .git -type d -maxdepth 4 2>/dev/null \
| sed 's|/\.git$||' \
| while read -r d; do
ts=$(git -C "$d" log -1 --format=%ct 2>/dev/null || echo 0)
echo "$ts $d"
done \
| sort -rn \
| cut -d' ' -f2- \
| sed "s|$HOME/||" \
| fzf --prompt="project> " \
--preview="git -C $HOME/{} log --oneline -5 2>/dev/null")
if [ -n "$dir" ]; then
cd "$HOME/$dir"
iterm2_set_user_var project "${dir##*/}"
claude
fi
}
Claude Codeのスラッシュコマンドで自動更新する
cl コマンドはプロジェクト名を渡すだけなので、タイトルの1行目は「プロジェクト名」で固定になります。せっかく2行表示にしたので、1行目には「いま何の作業をしているか」を入れたいところです。とはいえ作業のたびに手で要約を打ち込むのは面倒です。
そこで筆者は、Claude Codeのカスタムスラッシュコマンドとしてタイトル更新用のコマンドを用意しました。~/.claude/commands/update-tab-title.md に次のような指示を書いておくと、/update-tab-title を実行するだけで、いまの作業内容からタイトルを組み立ててくれます。
---
description: iTerm2のタブタイトルを差分やタスクから自動更新
allowed-tools: Bash(git:*), Bash(gh:*), Bash(printf:*), Bash(ps:*), Bash(pid=*), Bash(basename:*)
---
現在の作業内容からiTerm2のタブタイトルを自動更新してください。
## タイトルフォーマット
- Line 1: 現在の作業内容の概要(全角15文字/半角30文字以内)
- Line 2: `{branch} / {repo_name}`
## Line 1 の決定方法
以下の情報源から作業内容を判定し、短い日本語で要約する:
1. Issue情報: ブランチ名からIssue番号を抽出し `gh issue view` でタイトルを取得
2. 未コミットの差分: `git diff --stat` と `git diff --cached --stat`
3. ブランチのコミット履歴: `git log main..HEAD --oneline`
4. ブランチ名: 上記で判定できない場合はブランチ名自体を表示
Claude Codeがブランチ名からIssueのタイトルを引いたり、git diff --stat や git log を見たりして、その内容を「集約タスクの簡素化リファクタ」のような短い日本語に要約し、1行目にセットしてくれます。作業の区切りで /update-tab-title を叩けば、そのときの作業に合ったタイトルへ更新できるというわけです。
Claude Codeの中からTTYへ書き込む
先ほどのスラッシュコマンドの定義には、タイトルの決め方だけでなく「実際にタイトルを更新するコマンド」も書いておき、Claude Codeにそれを実行させます。frontmatterの allowed-tools で Bash(ps:*) や Bash(printf:*) を許可しているのはこのためで、毎回の確認を挟まずにコマンドを走らせられます。
その更新コマンドには、ひとつ工夫が必要です。前述の iterm2_set_user_var はいま自分がいるシェルの標準出力にエスケープシーケンスを流していますが、Claude Codeが実行するコマンドの標準出力はClaude Code側に取り込まれてしまい、そのままではiTerm2の画面には届きません。エスケープシーケンスをターミナルに解釈させるには、iTerm2につながっているTTYデバイスへ直接書き込む必要があります。
そこで、自分のプロセスから親をたどっていき、書き込み可能なTTYを持つ祖先を見つけて、そのデバイスファイルにエスケープシーケンスを書き込みます。実際にスラッシュコマンドへ書いているのは次のスクリプトです。
pid=$$
TTY_DEV=""
while [ "$pid" != "1" ] && [ -n "$pid" ] && [ "$pid" != "0" ]; do
tty=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
if [ -n "$tty" ] && [ "$tty" != "??" ] && [ -w "/dev/$tty" ]; then
TTY_DEV="/dev/$tty"
break
fi
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
done
[ -n "$TTY_DEV" ] && printf "\033]1337;SetUserVar=%s=%s\007" "project" \
"$(printf "%b" "作業内容の概要\nブランチ名 / リポジトリ名" | base64)" > "$TTY_DEV"
ps -o tty= で各プロセスの制御TTYを調べ、/dev/ttysXXX のような書き込み可能なデバイスが見つかったらそこにエスケープシーケンスを書き込みます。書き込み先が現在のシェルの標準出力から実際のTTYデバイスに変わっただけで、セットしているのは記事の前半と同じ user.project 変数です。そのため、これまでに設定したTitle Providerの仕組みがそのまま活きます。
タブ色でClaude Codeの状態を知らせる
タブタイトルで「何をしているか」は分かるようになりましたが、複数タブを回していると今度は「どのタブが自分の操作を待っているか」も知りたくなります。Claude Codeは実行中・権限の確認待ち・完了と状態が変わるので、これをタブの色で表せると、タイトルを読まなくても色だけで「このタブが確認待ちだ」と気付けます。
Claude Codeにはフック機能があり、プロンプト送信やツール実行、停止などのタイミングで任意のスクリプトを実行できます。このフックからタブ色を変えるスクリプトを呼べば、状態に応じてタブの色が自動で切り替わります。
タブ色を変えるスクリプト
タブの色はOSC 6というエスケープシーケンスで変更できます。次のスクリプトを ~/.claude/hooks/iterm2-claude-status.sh に置きます。書き込み先のTTYを探す部分は前述の /update-tab-title とまったく同じで、フックもClaude Codeの子プロセスとして起動されTTYを持たないため、親をたどってTTYデバイスを見つけます。
status="${1:-clear}"
# 親プロセスを遡ってTTYデバイスを探す(前述と同じ)
pid=$$
TTY_DEV=""
while [ -n "$pid" ] && [ "$pid" != "1" ] && [ "$pid" != "0" ]; do
tty=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
if [ -n "$tty" ] && [ "$tty" != "??" ] && [ -w "/dev/$tty" ]; then
TTY_DEV="/dev/$tty"
break
fi
pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
done
[ -z "$TTY_DEV" ] && exit 0
# タブバーの背景色と混ぜて疑似的に半透明にする
ALPHA=35
BASE_R=40 BASE_G=40 BASE_B=45 # ダークテーマのタブバー想定。明るいテーマなら200前後に
set_tab_color() {
r=$(( (BASE_R * (100 - ALPHA) + $1 * ALPHA) / 100 ))
g=$(( (BASE_G * (100 - ALPHA) + $2 * ALPHA) / 100 ))
b=$(( (BASE_B * (100 - ALPHA) + $3 * ALPHA) / 100 ))
printf '\033]6;1;bg;red;brightness;%s\a\033]6;1;bg;green;brightness;%s\a\033]6;1;bg;blue;brightness;%s\a' \
"$r" "$g" "$b" > "$TTY_DEV"
}
reset_tab_color() {
printf '\033]6;1;bg;*;default\a' > "$TTY_DEV"
}
case "$status" in
running) reset_tab_color ;; # 実行中は色なし
waiting) set_tab_color 255 170 30 ;; # オレンジ(確認待ち)
done) set_tab_color 60 220 120 ;; # 緑(完了)
*) reset_tab_color ;;
esac
OSC 6はRGB各成分の明るさを個別に指定するエスケープシーケンスで、\033]6;1;bg;red;brightness;<値>\a のように色成分ごとに値を渡します。\033]6;1;bg;*;default\a を送ると色をリセットできます。
引き算方式で色を付ける
状態と色の対応は次のようにしています。
| 状態 | 色 | フックの引数 |
|---|---|---|
| 実行中 | 色なし | running |
| 確認待ち | オレンジ | waiting |
| 完了 | 緑 | done |
ポイントは「実行中はあえて色を付けない」ことです。すべての状態に色を付けると常にどこかのタブが光っていて、かえってどこを見ればいいか分かりません。そこで「色が付いている=自分のアクションが必要」という引き算の考え方にして、確認待ちと完了のときだけ色を付けています。
もうひとつの工夫が疑似的な半透明です。OSC 6はRGBしか指定できずアルファ値を持たないので、そのまま原色を指定するとタブがべた塗りになって目に刺さります。そこでタブバーの背景色(BASE_*)と指定色を ALPHA(不透明度)の割合で混ぜることで、うっすら色が乗ったように見せています。ALPHA を小さくするほど背景に馴染みます。
フックへの登録
あとはこのスクリプトを、状態が変わるタイミングでClaude Codeが呼ぶように ~/.claude/settings.json の hooks に登録します。
{
"hooks": {
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/iterm2-claude-status.sh running", "async": true }] }],
"PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "~/.claude/hooks/iterm2-claude-status.sh running", "async": true }] }],
"Notification": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/iterm2-claude-status.sh notification", "async": true }] }],
"Stop": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/iterm2-claude-status.sh done", "async": true }] }],
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/iterm2-claude-status.sh clear" }] }]
}
}
プロンプトを送ったりツールを実行したりしている間は running、Claude Codeが応答を終えて停止したら done、セッション終了で clear に戻します。
ひとつ注意が必要なのが Notification です。Claudeが権限の確認や質問をしてきたときに確認待ち(オレンジ)にしたいのですが、Notificationイベントは「60秒間入力がない」というアイドル通知でも発火します。これをそのまま確認待ち扱いにすると、完了後に放置しただけでオレンジになってしまいます。そこでスクリプトの冒頭で通知メッセージを見て、アイドル通知のときは色を変えないようにしています。
フックには、発火したイベントの内容がJSONで標準入力(stdin)から渡されます。そこから通知メッセージ(message)を取り出して判定します。
if [ "$status" = "notification" ]; then
msg=$(jq -r '.message // ""' 2>/dev/null) # stdinのJSONからmessageを取り出す
case "$msg" in
*"waiting for your input"*) exit 0 ;; # アイドル通知は無視
*) status="waiting" ;; # 権限確認・質問は確認待ち
esac
fi
JSONの取り出しに jq を使っています。macOSには標準で入っていないので、あらかじめ brew install jq などでインストールしておいてください。
これで、タブタイトルで作業内容、タブの色でClaude Codeの状態と、2つの情報を一目で把握できるようになりました。実際に設定すると、確認待ちのタブがオレンジ、完了したタブが緑にうっすら色付き、どのタブに対応すればよいかが色だけで分かります。

まとめ
iTerm2のタブカスタマイズについて紹介しました。タブを見るだけで各タブで何をしているか、どういう状態かが一目で分かります。2行目にはブランチ名とリポジトリ名も表示されているので、どのプロジェクトのどのブランチかも把握できます。要点は次のとおりです。
- タブを左に配置 — 一覧性が向上する
-
タブの高さを変更 — Advanced設定で調整可能(筆者は
80に設定) -
改行を含むタイトル —
\nで2行表示ができる - Python Title Provider — ユーザー変数を参照するカスタムTitle Providerで、Claude Codeのタイトル書き換えの影響を受けずにタブタイトルを自由に設定できる
-
スラッシュコマンドで自動更新 —
/update-tab-titleで、いまの作業内容からタイトルを組み立てて更新できる - タブ色で状態を表示 — Claude Codeのフックでタブ色を変え、確認待ちや完了を色で気付けるようにする
Claude Codeに限らず、複数プロジェクトを並行して作業する方にはお勧めのカスタマイズです。iTerm2を使っている方はぜひ試してみてください。
Discussion