
WSL2環境Claude Codeのセッション状態でターミナルの背景色を塗り分ける方法
はじめに
データ事業本部の荒木です。
Claude Codeを複数のセッションで並行して使うとき、それぞれ別のターミナルで開いて作業しています。その状態でデスクトップ通知だけに頼ると、通知が鳴っても「どのターミナルのセッションが終わったのか」が分かりにくいという悩みがありました。
以前WSL2 + Windows Terminalでトースト通知を出す仕組みは組んだのですが、通知は一瞬で消えますし、どのセッションのものかまでは判別できません。結局どれが完了したのか、タブを順番に覗きに行くことになります。
そこで、Claude Codeのセッションの状態に応じてターミナルの背景色そのものを塗り分けるようにしてみました。背景色ならタブを切り替えた瞬間に分かりますし、通知のように消えることもありません。今回は次の状態を色分けしています。
- 処理中・入力待ち: 黒 (デフォルト)
- 選択待ち (AskUserQuestion 表示中): オレンジ
- ターン完了: 緑
- ターン完了だがバックグラウンドのworkflowが実行中: 青
- セッション終了: 黒に戻す
環境はWSL2 + Windows Terminalで、Claude Codeはフルスクリーンのターミナルモードで動かしています。
本題
ターミナルの背景色はOSC 11というエスケープシーケンスで動的に変えられます。Windows Terminalも対応していて、\033]11;#RRGGBB\007を書き込めば背景色が変わり、\033]111\007 (OSC 111) でデフォルトに戻せます。
やることは2つです。まず背景色を切り替えるスクリプトを1つ用意し、次にsettings.jsonで状態ごとのhookからそのスクリプトを呼び分けます。
スクリプトは~/.claude/scripts/term-bg.shとして用意しました。引数に色 (hex) を渡すとその色に、resetでデフォルトに、stopを渡すとバックグラウンドのworkflow実行中かどうかを見て青か緑を選びます。
#!/usr/bin/env bash
# Claude Code の状態に応じて端末背景色を OSC 11 で切り替える。
# 使い方:
# term-bg.sh "#14401c" … その色にする
# term-bg.sh reset … デフォルトに戻す
# term-bg.sh stop … Stop hook 用。バックグラウンド workflow 実行中なら青、なければ緑
set -u
GREEN="#14401c"
BLUE="#14304a"
# Claude Code 本体が紐づく pts を親プロセスを辿って検出する。
# hook には制御端末 (/dev/tty) が無いため、ps で辿って書き込み先を特定する。
find_pts() {
local pid=$$ t ppid
while [ "${pid:-0}" -gt 1 ]; do
t=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ "$t" == pts/* ]]; then echo "/dev/$t"; return 0; fi
ppid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
[ -z "$ppid" ] && break
pid="$ppid"
done
return 1
}
# 背景色を適用する ($1 = reset または #RRGGBB)
apply() {
local dev; dev=$(find_pts) || return 0
[ -w "$dev" ] || return 0
if [ "$1" = "reset" ]; then
printf '\033]111\007' > "$dev"
else
printf '\033]11;%s\007' "$1" > "$dev"
fi
}
arg="${1:-reset}"
if [ "$arg" != "stop" ]; then
apply "$arg"
exit 0
fi
# stop: hook の stdin JSON から session_id を取り、workflow 実行中か判定する
sid="$(cat 2>/dev/null | python3 -c '
import sys, json
try:
print(json.load(sys.stdin).get("session_id") or "")
except Exception:
print("")
' 2>/dev/null)"
color="$GREEN"
if [ -n "$sid" ]; then
running="$(python3 - "$sid" <<'PY' 2>/dev/null
import sys, glob, json, os, time
sid = sys.argv[1]
base = os.path.join(os.path.expanduser("~"), ".claude", "projects", "*", sid, "**")
WINDOW = 90
now = time.time()
# agent ログが直近 WINDOW 秒以内に更新されているか (生存確認)
fresh = any(
now - os.path.getmtime(p) <= WINDOW
for p in glob.glob(os.path.join(base, "agent-*.jsonl"), recursive=True)
)
# started > result = 未完了エージェントがある
started = result = 0
if fresh:
for p in glob.glob(os.path.join(base, "journal.jsonl"), recursive=True):
with open(p, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
t = json.loads(line).get("type")
except Exception:
continue
if t == "started":
started += 1
elif t == "result":
result += 1
print(1 if (fresh and started > result) else 0)
PY
)"
[ "${running:-0}" -gt 0 ] 2>/dev/null && color="$BLUE"
fi
apply "$color"
書き込み先が/dev/ttyではなくfind_ptsで検出した/dev/pts/Nになっている点がポイントです。hookのプロセスには制御端末が割り当てられないので、親プロセスを辿ってClaude Code本体の端末を特定して書き込んでいます。
次にsettings.jsonで、状態ごとのhookからこのスクリプトを呼びます。
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command",
"command": "bash ~/.claude/scripts/term-bg.sh reset" }] }
],
"PreToolUse": [
{ "matcher": "AskUserQuestion",
"hooks": [{ "type": "command",
"command": "bash ~/.claude/scripts/term-bg.sh \"#8a5214\"" }] }
],
"PostToolUse": [
{ "matcher": "AskUserQuestion",
"hooks": [{ "type": "command",
"command": "bash ~/.claude/scripts/term-bg.sh reset" }] }
],
"Stop": [
{ "hooks": [{ "type": "command",
"command": "bash ~/.claude/scripts/term-bg.sh stop" }] }
],
"SessionEnd": [
{ "hooks": [{ "type": "command",
"command": "bash ~/.claude/scripts/term-bg.sh reset" }] }
]
}
それぞれの設定は次のような意味です。
UserPromptSubmit→reset: プロンプトを送信したとき。処理中は黒 (デフォルト) に戻します。PreToolUse(matcherAskUserQuestion) → オレンジ#8a5214: Claudeが選択肢を提示したとき。matcherでツール名を指定でき、AskUserQuestionのときだけ色付けされます。選択待ちであることが色で分かります。PostToolUse(matcherAskUserQuestion) →reset: 選択に答えたとき。黒に戻して処理再開を表します。Stop→stop: ターンが完了してこちらに制御が返ったとき。基本は緑ですが、バックグラウンドでworkflowが動いていれば青にします (後述)。SessionEnd→reset: セッション終了時。黒に戻します。
Stopだけ少し特別です。Stopが発火するのは「メインのターンが終わってこちらに制御が返った瞬間」ですが、dynamic workflowをバックグラウンドで起動すると、メインのターンはすぐ終わって入力待ちに戻ります。裏でエージェントが動いていてもStopは発火するので、単純に緑にすると「完了」なのか「メインは手を離したが裏で継続中」なのか区別できません。そこでstop引数では、Claude Codeがセッションごとに残すworkflowのログ (~/.claude/projects/*/<session_id>/**/) を見て、実行中なら青、そうでなければ緑を選んでいます。
判定は2つのシグナルを両方満たすときだけ「実行中」=青にしています。
journal.jsonlのstarted数とresult数を比べ、started > resultなら未完了のエージェントがあるagent-*.jsonl(各エージェントの実況ログ) が直近90秒以内に更新されていれば、本当に生きている
どちらか片方だと不都合があります。startedとresultの数だけで見ると、エージェントがresultを書かずに落ちたときに差が永久に残ってずっと青になります。逆にjournal.jsonlの更新時刻だけで見ると、実行中なのに緑と誤判定することがあります。journal.jsonlはエージェントの開始・完了時にしか行が追記されないので、1つのエージェントが長時間動いている間は更新時刻が古いままになり、止まっているように見えてしまうためです。その点agent-*.jsonlは動いている間ずっと書き込まれ続けるので、生存確認に向いています。完了するとstarted == resultになるので、待ち時間なくすぐ緑に戻ります。
設定すると、ターン完了で背景が緑 (裏でworkflowが動いていれば青)、選択肢が出ている間は暗めオレンジ、入力すると黒、という具合に切り替わります。複数のターミナルを並べていても、緑になっているタブがあれば「あのセッションが終わったな」とすぐ分かるようになりました。
動作サンプルは以下の画像のようになります。









まとめ
複数のClaude Codeセッションを別々のターミナルで動かしていると、トースト通知だけではどのセッションが完了したのか分かりにくかったので、状態に応じてターミナルの背景色を塗り分けるようにしてみました。処理中は黒、完了は緑、選択待ちはオレンジ、バックグラウンドでworkflowが動いている間は青と色で分かるようになり、タブを覗きに行かなくても状態を把握できるようになりました。
複数セッションを並行で回していて状態が分かりにくいと感じている方の参考になれば幸いです。







