Dynamic terminal tab title showing current task context
Feature Request
Current behavior: Terminal tab title shows static "Claude Code (node)" regardless of what's being worked on.
Desired behavior: Claude Code dynamically updates the terminal tab title to reflect the current working context — e.g., the project name, active file, or a short description of the current task.
Why this matters
When running multiple Claude Code sessions across different projects/terminals, they all look identical in the tab bar. You have to click into each one to figure out which is which. A dynamic title like Claude Code — qinnovate/blog or Claude Code — fixing BCI toggle would make multitasking significantly easier.
Security-hardened implementation proposal
Terminal title updates are a known attack surface. Malicious escape sequences injected into terminal titles have led to CVEs in terminal emulators (arbitrary code execution via crafted OSC sequences). The implementation must treat this as a security boundary.
Architecture
- Internal-only rendering. Title updates must go through Claude Code's own Node.js process via
process.stdout.write()with a compile-time constant OSC prefix and suffix. Never shell out. Never useexec(),spawn({ shell: true }), or template literals with dynamic content near escape sequences.
- Strict allowlist sanitization. All title content — regardless of source — must pass through a sanitizer that:
- Strips ALL control characters (U+0000–U+001F, U+007F, U+0080–U+009F) — prevents nested escape sequence injection
- Strips ALL OSC/CSI/DCS/APC sequences (regex for
\x1bfollowed by sequence initiators) - Allowlists only printable ASCII + common Unicode (letters, numbers, spaces, hyphens, dots, slashes)
- Enforces a hard max length (e.g., 80 chars) to prevent buffer-based attacks on older terminal emulators
- Returns a static fallback string (
"Claude Code") if sanitization produces an empty result
- No user-controlled or file-derived content without sanitization. Even project directory names from the filesystem are untrusted — a malicious repo name could inject sequences if passed raw. Use
path.basename()then sanitize.
- No model-generated content in title by default. If task summary is implemented:
- Opt-in via settings (disabled by default)
- Rate-limited (max 1 update per 5 seconds to prevent flickering attacks)
- Treated as fully untrusted input (same sanitizer)
- Truncated before sanitization to prevent DoS via large strings
- Terminal capability detection. Not all terminals support OSC. Detect via
$TERM/$TERM_PROGRAMand only emit for known-safe terminals. Provide a setting to disable entirely.
Reference implementation (TypeScript)
import { basename } from 'node:path';
// Compile-time constants — never interpolated from user input
const OSC_SET_TITLE = '\x1b]0;';
const OSC_END = '\x07';
const MAX_TITLE_LENGTH = 80;
const FALLBACK_TITLE = 'Claude Code';
// Allowlist: printable ASCII + common Unicode letters/numbers
const ALLOWED_CHARS = /[^\p{L}\p{N}\s\-._\/\[\]()—:]/gu;
/**
* Sanitize a string for safe use in terminal title.
* Defense-in-depth: strip control chars, strip escape sequences,
* allowlist remaining chars, enforce length.
*/
function sanitizeTitle(raw: string): string {
let s = raw;
// Layer 1: Strip all control characters (C0, DEL, C1)
s = s.replace(/[\x00-\x1f\x7f\x80-\x9f]/g, '');
// Layer 2: Strip any remaining ANSI/OSC/CSI sequences
// (belt-and-suspenders after Layer 1 already removed \x1b)
s = s.replace(/\x1b[\[\]PX^_].*?[\x07\x1b\\]/g, '');
// Layer 3: Allowlist — remove anything not in the safe set
s = s.replace(ALLOWED_CHARS, '');
// Layer 4: Collapse whitespace, trim
s = s.replace(/\s+/g, ' ').trim();
// Layer 5: Enforce max length
if (s.length > MAX_TITLE_LENGTH) {
s = s.slice(0, MAX_TITLE_LENGTH);
}
return s || FALLBACK_TITLE;
}
/**
* Set terminal title. Only called internally by Claude Code's
* rendering layer — never exposed to plugins or user code.
*/
function setTerminalTitle(projectDir: string, branch?: string): void {
const project = sanitizeTitle(basename(projectDir));
const branchSafe = branch ? sanitizeTitle(branch) : '';
let title = `Claude Code — ${project}`;
if (branchSafe) {
title += ` [${branchSafe}]`;
}
// Final sanitization of the composed string
title = sanitizeTitle(title);
// Write directly to stdout — no shell, no exec
process.stdout.write(`${OSC_SET_TITLE}${title}${OSC_END}`);
}
Key security properties
| Layer | Threat | Mitigation |
|-------|--------|------------|
| Input | Malicious repo/branch names containing escape sequences | sanitizeTitle() strips all control chars and ANSI sequences |
| Input | Model-generated content with injection attempts | Same sanitizer, opt-in only, rate-limited |
| Composition | Nested sequences surviving individual sanitization | Final sanitization pass on composed title string |
| Output | Shell injection via exec()/spawn() | No shell involvement — direct process.stdout.write() only |
| Output | Buffer overflow on older terminals | Hard 80-char max length |
| Fallback | Empty/fully-stripped title | Returns static "Claude Code" |
| Scope | Function never exposed to plugins/hooks/user code | Internal rendering layer only |
Title content sources (in priority order)
- Project directory basename (always available, e.g.,
Claude Code — qinnovate) - Git branch (if in a repo, e.g.,
Claude Code — qinnovate [main]) - Task summary (opt-in, model-generated, e.g.,
Claude Code — fixing BCI toggle)
Even just option 1 with the sanitization layer above would be a significant improvement over the static title.
Environment
- Claude Code 2.1.74
- macOS (iTerm2 + Terminal.app)
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗