AI session titler overwrites user-set custom titles in long sessions (64 KB head/tail guard blind spot)
AI session titler overwrites user-set custom titles in long sessions (64 KB head/tail guard blind spot)
Component: Claude Code VS Code extension
Extension version reproduced on: 2.1.220 (darwin-arm64); offending code confirmed still present in 2.1.224
Platform: macOS (Darwin 25.2.0)
Severity: Low impact per event, but continuous — the title flips back dozens of times per session
Summary
When a user renames a conversation in the sidebar, the rename is persisted as a custom-title
record appended to the session .jsonl. The automatic AI titler is supposed to stand down once a
custom title exists, but its check only scans the first 64 KB and last 64 KB of the transcript.
In long sessions the custom-title record falls outside both windows, the guard misses it, and anai-title record is appended — overwriting the user's title, since the last title record wins.
The result is a persistent tug-of-war: the user renames, the titler reverts it, repeat.
Root cause
extension.js, storage-layer renameSession(sessionId, title, onlyIfNoCustomTitle):
async renameSession(e, t, r) {
let n = Hc(this.projectRoot), i = Vn.join(n, `${e}.jsonl`);
if (r) { // r = onlyIfNoCustomTitle (AI titler path)
let s = await nB(i);
if (!s) return true;
if (ia(s.tail, "customTitle") || ia(s.head, "customTitle")) return true; // <-- guard
let a = { type: "ai-title", sessionId: e, aiTitle: t };
await dn.promises.appendFile(i, JSON.stringify(a) + "\n");
this.customTitles.set(e, t);
return false;
}
let o = { type: "custom-title", sessionId: e, customTitle: t }; // user rename path
await dn.promises.appendFile(i, JSON.stringify(o) + "\n");
this.customTitles.set(e, t);
return false;
}
The guard depends on nB(), which reads only two fixed-size windows — not the whole file:
const uu = 65536; // 64 KB
async function nB(e, t) {
// ...
let n = await r.stat(),
i = Buffer.allocUnsafe(uu),
o = await r.read(i, 0, uu, 0); // head: first 64 KB
let s = i.toString("utf8", 0, o.bytesRead),
a = Math.max(0, n.size - uu),
c = s;
if (a > 0) {
let l = await r.read(i, 0, uu, a); // tail: last 64 KB
c = i.toString("utf8", 0, l.bytesRead);
}
return { mtime: ..., size: n.size, head: s, tail: c };
}
ia(str, "customTitle") is a plain substring scan over those windows. Two consequences:
- Blind spot (primary): in a multi-megabyte transcript, everything between the first and last
64 KB is invisible to the guard. Sessions with large tool results can push a custom-title
record out of the tail window after only a handful of subsequent turns.
- TOCTOU (secondary):
nB()samples the file before the append. A rename that lands between
the read and the write is not seen either.
Because resolution is last-record-wins, a single missed guard is enough to revert the title.
Evidence
Four sessions in one project, all 2.1.220. In every case the user set a JIRA-key title and the
final title record in the file is an ai-title:
| session | size | ai-title records | custom-title records | user's title | final record |
|---|---|---|---|---|---|
| 962f9301… | 4.1 MB | 108 | 67 | tri-3551/tri-3562/saas-3999 | ai-title |
| 187bc3c3… | 3.9 MB | 47 | 43 | tri-3547/saas-3994 | ai-title |
| 5b14c626… | 2.8 MB | 19 | 13 | tri-3531 | ai-title |
| faf39d00… | 1.5 MB | 21 | 7 | tri-3572 | ai-title |
Title records in file order from 962f9301…jsonl (2026-08-04 → 2026-08-10) — strict alternation,
64 consecutive clobbers:
line 1424 custom-title tri-3551/tri-3562/saas-3999
line 1425 ai-title Unpost original readings without deletion
line 1447 custom-title tri-3551/tri-3562/saas-3999
line 1448 ai-title Unpost original readings without deletion
line 1466 custom-title tri-3551/tri-3562/saas-3999
line 1467 ai-title Unpost original readings without deletion
... (pattern repeats through line 1631)
The custom-title at line 1424 sits ~200 lines from EOF — far outside the 64 KB tail window — so
every later titler run re-appends its own title.
Reproduction
- Open a conversation in the VS Code extension and rename it in the sidebar (e.g.
TRI-1234). - Continue working until the transcript exceeds a few hundred KB, with large tool results so that
more than 64 KB of content accumulates after the rename.
- Observe the sidebar title revert to an auto-generated one.
- Confirm in
~/.claude/projects/<project>/<sessionId>.jsonl: an{"type":"ai-title",…}record
has been appended after the {"type":"custom-title",…} record.
Expected behavior
A user-set title is sticky. Once a custom-title record exists for a session, the AI titler must
never overwrite it, regardless of transcript size.
Suggested fixes
- Track custom-title state outside the transcript scan — e.g. consult the already-maintained
this.customTitles map, or a per-session sidecar/index — instead of substring-scanning a sampled
window of the .jsonl.
- Make resolution precedence-based rather than last-write-wins: when reading back, let any
custom-title beat any ai-title no matter the order.
- If the transcript must remain the source of truth, scan the full file (or maintain a marker in the
head, which is always read) rather than head+tail sampling.
- Stop re-appending an identical
ai-titleon every run — the alternation above wrote ~175 title
records into a single session.
Additional note
There appears to be no user-facing escape hatch: claude-code-settings.schema.json in 2.1.224
exposes no setting to disable AI session titling (terminalTitleFromRename is unrelated). Users who
rely on stable, self-chosen conversation titles currently have no way to opt out.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗