[FEATURE] Agent-maintained session state in the terminal tab title (name + closed state vocabulary, e.g. "proj-42 - blocked")
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Running several Claude Code sessions in VS Code terminal tabs (or tmux windows), the thing I need at a glance is which session is blocked waiting on me — a decision, a permission, a review — versus which is happily working. Today no title signal can express that:
- The auto-generated topic title is set early and goes stale (mine still described the first investigation hours after that work had shipped).
claude -n "<name>"labels the tab but is static for the whole session./renameupdates the title live, but only from literal user keystrokes.- #71369 asks for activity state (generating / unread / idle) — valuable, but it's a different axis. "Actively generating" doesn't tell me the session is semantically blocked on my input; only the agent knows it's in that state.
- #67858 is the closest prior request: same motivation (parallel sessions, stale names), asking for mechanisms to keep the session name current (model-settable name, live auto-naming, a hook output field, a CLI rename subcommand). It was labeled duplicate and went stale with no engagement. This proposal is complementary rather than the same ask: keep the name stable, and add a validated state dimension on top — plus a working recipe on today's documented primitives.
Proposed Feature
First-class support for an agent-maintained state suffix on the session name in the terminal title:
proj-42 - investigating
proj-42 - verifying
proj-42 - blocked ← the one that matters at a glance
with the state drawn from a closed, user-configurable vocabulary (e.g. investigating | fixing | verifying | blocked | waiting-review | done in a settings key). Closed matters: free-text states drift ("verifying" / "checking" / "testing…") and titles stop being scannable across tabs.
Possible shapes (any would work):
- A supported way for the agent/hooks to update the session's title state — related to #76414 (programmatic
/rename) and #67386 (hooks setting the session name), but with a validated state vocabulary rather than free text. - A
${state}variable in a script-based title (#17951's model), fed by an agent-writable, schema-validated field.
It already works today on documented primitives — proof of demand and feasibility
We built this with existing documented pieces; sharing the recipe both as a workaround for others and as evidence the feature wants first-class support:
1. VS Code workspace settings — read titles from escape sequences, and stop Claude Code re-asserting its own title (the documented env var):
"terminal.integrated.tabs.title": "${sequence}",
"terminal.integrated.env.osx": { "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1" }
2. A closed state vocabulary in ~/.claude/tab-states.conf:
investigating fixing verifying blocked waiting-review done
3. A cc-title helper the agent calls at state transitions — validates the state against the vocabulary (rejects drift), persists the title per-TTY, writes OSC 0. It walks up the process tree to the first ancestor owning a TTY, since tool-call shells have none:
#!/usr/bin/env sh
# cc-title "<name> - <state>" — validated against ~/.claude/tab-states.conf
conf="$HOME/.claude/tab-states.conf"
title="$*"
[ -f "$conf" ] || { echo "missing $conf" >&2; exit 2; }
allowed=$(grep -v '^[[:space:]]*#' "$conf" | tr -s '[:space:]' ' ')
case "$title" in
*' - '*) state="${title##* - }" ;;
*) echo "need '<name> - <state>'; allowed:$allowed" >&2; exit 2 ;;
esac
ok=0; for s in $allowed; do [ "$state" = "$s" ] && ok=1 && break; done
[ "$ok" -eq 1 ] || { echo "'$state' not allowed:$allowed" >&2; exit 2; }
pid=$$
while [ "$pid" -gt 1 ] 2>/dev/null; do
t=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
case "$t" in
''|'??'|'-') pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ') ;;
*)
printf '%s' "$title" > "$HOME/.claude/tab-state/$(printf '%s' "$t" | tr '/' '_')"
printf '\033]0;%s\007' "$title" > "/dev/$t" 2>/dev/null && exit 0 || exit 1
;;
esac
done
exit 1
4. A Stop hook that self-corrects the title at every turn boundary via the documented terminalSequence hook-output field — a sibling script reads the per-TTY state file and emits {"terminalSequence": "<ESC>]0;<title><BEL>", "suppressOutput": true} (bytes built at runtime with printf '\033' passed to jq --arg).
5. A CLAUDE.md/memory instruction telling the agent to call cc-title at session start and on genuine state transitions. The tool enforces the vocabulary, so a drifting agent is corrected rather than trusted.
Launch with claude -n "proj-42" and the agent keeps the suffix current. Verified end-to-end: mid-turn updates render immediately, and the Stop hook re-asserts the saved title when the turn ends.
Observed behaviors that shaped the workaround (useful data points)
- Claude Code re-asserts its own terminal title on render, so any external OSC title write is reverted almost immediately (a single write, a write-as-last-action, and repeated writes all lose the race). This is why the env var is required today.
CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1silences Claude Code's own title writes but does not suppress hook-emittedterminalSequenceoutput — the two compose exactly as needed.- The
terminalSequenceallowlist (OSC 0/1/2/9/99/777 + BEL) already permits both titles and desktop notifications, which makes hooks a natural home for this.
Why native support would still be better
- The workaround needs three separate pieces of user setup (env var, VS Code setting, hook) and only covers terminals honoring OSC via
${sequence}— the VS Code extension tab reads the internal session name instead (see #67386), which none of this reaches. - The state vocabulary should be a first-class, schema-validated setting rather than a homegrown conf file.
- With the env var set, Claude Code's own (useful) auto-titling is lost entirely; a native state suffix could compose with it instead of replacing it.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗