Desktop sidebar ignores UserPromptSubmit hook `sessionTitle` output
Summary
The documented hookSpecificOutput.sessionTitle from UserPromptSubmit hooks (added in v2.1.94) is honored by the CLI but not by the desktop app's sidebar. The desktop displays its own auto-generated title and ignores the hook output.
Environment
- Claude Code 2.1.121 (desktop)
- macOS 14.x
Repro
- Add a
UserPromptSubmithook in~/.claude/settings.json:
``json``
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "command", "command": "/path/to/hook.sh" } ] }
]
}
}
- Have the hook return:
``json``
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"sessionTitle": "[ProjectA] Test Title"
}
}
- Submit a first prompt in a desktop session.
Expected
Sidebar entry shows [ProjectA] Test Title.
Actual
~/.claude/sessions/<pid>.json→.name = \"[ProjectA] Test Title\"✅- Session
.jsonltranscript →custom-titlerecords contain\"[ProjectA] Test Title\"✅ ~/Library/Application Support/Claude/claude-code-sessions/.../local_*.json→title = \"<auto-generated summary>\",titleSource = \"auto\"❌
The desktop sidebar reads from the local_*.json file. The hook output never reaches it.
Manual /rename works because it sets titleSource: \"user\", so the field is honored when set through the UI's own path.
Workaround attempted (and why it's not viable)
Patching local_*.json directly to titleSource: \"user\" works briefly but the desktop overwrites it on session-close/app-close — disk state at next app start is what wins, and the in-memory state is what writes last. So patches don't survive a close/reopen reliably without a constantly-running watcher daemon.
Ask
Have the desktop sidebar honor hookSpecificOutput.sessionTitle from UserPromptSubmit hooks — same effect as /rename (i.e. set titleSource: \"user\" in local_*.json and invalidate the in-memory cache).
Use case
Auto-formatting session titles per-project (e.g. [ProjectA] …, [ProjectB] …) for users who work across multiple repos and view sessions in date-sorted mode rather than project mode.
Showing cached comments. Read the full discussion on GitHub ↗
9 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Reviewed the three flagged issues — this is a distinct bug, not a duplicate of any of them:
This issue is specifically about the already-shipped, documented
UserPromptSubmithookSpecificOutput.sessionTitleoutput (added in v2.1.94) being honored by the CLI session record and.jsonltranscript but not by the desktop sidebar'slocal_*.json. Different layer, different bug.Please keep open.
Empirical trace from a 2026-05-13 reverse-engineering pass on macOS (Claude Code Desktop v1.7196.0 + CLI v2.1.140): confirmed divergence at the hook-handler level.
When
UserPromptSubmitreturnshookSpecificOutput.sessionTitle:/renameslash command from the hook output. The session JSONL records atype:"system" subtype:"local_command"line containing<command-name>/rename</command-name>plus atype:"user" isMeta:truesystem-reminder ("The user named this session ..."). This invokes the backend rename API, which cloud-syncs to Desktop's sidebar.type:"custom-title"lines to the JSONL. Tracingapp.asarstrings: the JSONL-append helpers (Ubi/EPiin the obfuscated source) are reachable from the hook path, but the backend rename function (RenameSession, called by the right-click "Star / Rename / Add to project" sidebar context menu) is not.Net: a Desktop-started session never gets its sidebar entry renamed from hook output, no matter how many
custom-titlerecords accumulate in the JSONL. Workaround in our setup: always start sessions in iTerm viaclaudeso the CLI's/renamesynthesis fires; the rename then propagates to Desktop's sidebar via backend sync, and you can resume in Desktop or round-trip back to CLI freely.Adding a confirmation data point from 2026-05-22 (Mac Desktop):
v1.8089.1andv1.8555.0(both versions tested back-to-back), macOS Darwin 24.6.0UserPromptSubmithook withhookSpecificOutput.sessionTitlefires correctly, and internalsession_titlestate is saved (verified via Claude Code internal logs)settings.jsonhook entry — the documentedsessionTitlemechanism is effectively unusable from a Mac Desktop-started sessionBehavior matches @luisdlugo's reverse-engineering trace exactly: the Desktop app appends
custom-titlelines to the session JSONL but does not invoke the backend rename function, so the sidebar'slocal_*.jsonis never updated.Workaround we adopted: manual session naming (no automation). The CLI-start workaround (start
claudein a terminal so/renamesynthesis fires) is viable but defeats the purpose for users whose primary surface is the Desktop app's Code tab.Keeping this open seems correct — it is a distinct, documented-feature-vs-implementation gap, not a duplicate of #37628 / #40781 / #49712.
Confirming this also affects the
SessionStarthook (not justUserPromptSubmit) on v2.1.163.Setup: a
SessionStarthook withmatcher: "startup"emits one pure-JSON object on stdout, exit 0:Observed:
additionalContextfrom this object is consumed every session (the orientation text reliably reaches the model).sessionTitlefrom the same object is silently ignored — the picker keeps the auto-generated title./renameon the same session does update the picker.So within one
hookSpecificOutputobject,additionalContextis honored andsessionTitleis dropped — which lines up exactly with the root cause in this issue (the picker readstitleSource: "auto"fromlocal_*.json; the hook never setstitleSource: "user", so only the UI/renamepath is honored).Worth noting #59267 (closed) tracked enabling
sessionTitleonSessionStart— but on 2.1.163 the emitted value doesn't reach the picker, same as theUserPromptSubmitcase here.Confirming on Windows (this is labeled
platform:macos, but it's not macOS-specific): Claude Desktop1.15962.1.0, same behavior. ASessionStart/UserPromptSubmithook that returnssessionTitlewrites acustom-titlerecord, but the model's auto-titler writes anai-titleafterward and that's what the sidebar shows. Across the 18 sessions here that have both record types, all 18 end on theai-title./renamesticks; a hook's title doesn't.Root cause, read from
app.asar(.vite/build/index.js), the title-update guard:An
autotitle is dropped only once the session is alreadytitleSource: "user"./renamecoerces to"user", so it sticks. A hook'ssessionTitleis evidently applied asauto(I can't read that path directly: theai-titlewriter is in the CLI sidecar andaiTitleis 0× inapp.asar, but #67755 reports the metadata staystitleSource: "auto"and the behavior matches), and a hook only fires before the model's turn, so the laterautowrite always wins.Fix would be either to apply a hook-set
sessionTitleastitleSource: "user"(treat it like/rename), or add atitleSourcefield to the hook output (there's none today). Related Windows issue: #64304.Still reproduces on the current Desktop build — 1.19367.0 (2026-07-07), macOS. Fresh trace from today, plus experimental confirmation that the
titleSourceguard described in earlier comments is the entire root cause:UserPromptSubmithook emitshookSpecificOutput.sessionTitle([Project] Short Title). Verified the hook fired for a Desktop-started session and produced the expected title.~/Library/Application Support/Claude/claude-code-sessions/<acct>/<group>/local_*.jsonstayedtitleSource: "auto"with the auto-title — the hook output never reaches that store.titleSource: "user"is fully honored at the next app launch — the sidebar shows it and the auto-titler permanently stops competing for that session, exactly like a manual/rename. So the fix appears to be a one-liner in spirit: route hooksessionTitleoutput through the same path/renameuses (settitleSource: "user"and invalidate the in-memory cache).Caveats of the external-edit workaround that only an in-app fix can solve: the app flushes in-memory state back over the file during the session (external edits lose the race without a file watcher), and the sidebar only re-reads the store at launch, so hook titles can never appear live. Flagging that this remains live on the latest build so it doesn't get swept as inactive.
Follow-up with a sharper root-cause, from further testing on 1.19367.0 today. The
titleSourceguard in the app is real but it is not what blocks hook titles — the title update never reaches the app at all:UserPromptSubmithook emitshookSpecificOutput.sessionTitlein a Desktop-started session, the CLI does apply the rename locally: it appends acustom-titlerecord to the session's.jsonltranscript each time. Verified with multiple emissions across several prompts — the transcript accumulates the records.stream-jsonbridge to the desktop app, so the app's in-memory session store (and therefore the sidebar) never hears about it. The app's ownupdateSessionguard would actually accept the hook title in the common case — decompiled logic only rejects anauto-sourced title when the session was already manually renamed (titleSource: "user");autofreely overwritesauto.So the fix is plumbing, not policy: when the CLI applies a hook-driven rename (the same path that writes the
custom-titletranscript record), emit the corresponding session-update event over the bridge — the code paths on both ends already exist, since manual/renameand the app's own auto-titler both go through them.This also explains the earlier observations in this thread: external edits to the per-session store JSON are honored at next app launch (the store is only read then), which confirms the store/guard side works — the missing link is CLI→app notification at hook time.
Also reproduces on claude.ai/code (web), not just the desktop sidebar — so the scope may be broader than the title suggests.
Environment: Claude Code 2.1.220, Linux, session running as a Remote Control bridge worker (
CLAUDE_CODE_ENVIRONMENT_KIND=bridge).What I measured. A
UserPromptSubmithook returningsessionTitleupdates every local store correctly, but the cloud-side title never moves:| Store | Value |
|---|---|
| transcript
custom-title|🔧 watcher 実装確認(current) || pid file
~/.claude/sessions/<pid>.json→name|🔧 watcher 実装確認(current) || transcript
ai-title|セッション名の自動更新機能の実装(turn 1) || claude.ai/code sidebar + desktop sidebar | matches
ai-title, i.e. stuck at turn 1 |So
/resumeand the terminal title are correct; only the surfaces backed by the cloud title are stale, and they hold exactly the first auto-generated title.Why it looks like it happens. The rename path does not call
syncTitleToRemoteSession— only theai-titlegeneration path does. And in a bridge worker neither of the two push routes is usable:CLAUDE_CODE_REMOTE_SESSION_IDis not present in the process environment, and nobridgeSessionIdis recorded (absent from both the pid file and the transcript), soupdateBridgeSessionTitleis skipped as well. That matches #76812, which reports the bridge assigning the title before hooks complete and never updating it afterwards — likely the same root cause seen from theSessionStartside.Impact. Hook-based auto-retitling cannot fix the surface most users actually browse. The desktop-only session tools (
list_sessions/set_session_title) are the usual workaround, but they are unavailable on ChromeOS, where the desktop app does not run — those users have no workaround at all.