Desktop sidebar ignores UserPromptSubmit hook `sessionTitle` output

Status Open
Reported on v2.1.121
Maintainer reply None cached
Activity 10 comments · opened May 4, 2026

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

  1. Add a UserPromptSubmit hook in ~/.claude/settings.json:

``json
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "command", "command": "/path/to/hook.sh" } ] }
]
}
}
``

  1. Have the hook return:

``json
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"sessionTitle": "[ProjectA] Test Title"
}
}
``

  1. 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 .jsonl transcript → custom-title records contain \"[ProjectA] Test Title\"
  • ~/Library/Application Support/Claude/claude-code-sessions/.../local_*.jsontitle = \"<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.

View original on GitHub ↗

9 Comments

github-actions[bot] · 3 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/37628
  2. https://github.com/anthropics/claude-code/issues/40781
  3. https://github.com/anthropics/claude-code/issues/49712

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

LeoGestetner · 3 months ago

Reviewed the three flagged issues — this is a distinct bug, not a duplicate of any of them:

  • #37628 — VS Code extension's sidebar pencil-rename not syncing with terminal tab title. Different surface (VS Code), different mechanism (manual UI rename), different symptom.
  • #40781 — VS Code extension's manual rename reverting to auto-title. Same scope as #37628 — manual rename, VS Code-specific.
  • #49712 — Closest, but about manual user-renames being overwritten by auto-titles in the desktop. That reporter's suggested fix even mentions hook/env-var support as a separate future path: "Expose a way for agents to self-name at startup (e.g., CLAUDE_SESSION_NAME env var or a hook)".

This issue is specifically about the already-shipped, documented UserPromptSubmit hookSpecificOutput.sessionTitle output (added in v2.1.94) being honored by the CLI session record and .jsonl transcript but not by the desktop sidebar's local_*.json. Different layer, different bug.

Please keep open.

luisdlugo · 3 months ago

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 UserPromptSubmit returns hookSpecificOutput.sessionTitle:

  • CLI synthesizes a /rename slash command from the hook output. The session JSONL records a type:"system" subtype:"local_command" line containing <command-name>/rename</command-name> plus a type:"user" isMeta:true system-reminder ("The user named this session ..."). This invokes the backend rename API, which cloud-syncs to Desktop's sidebar.
  • Desktop only appends type:"custom-title" lines to the JSONL. Tracing app.asar strings: the JSONL-append helpers (Ubi/EPi in 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-title records accumulate in the JSONL. Workaround in our setup: always start sessions in iTerm via claude so the CLI's /rename synthesis 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.

minami-s-ji · 3 months ago

Adding a confirmation data point from 2026-05-22 (Mac Desktop):

  • Reproduced on: Mac Claude Code Desktop app v1.8089.1 and v1.8555.0 (both versions tested back-to-back), macOS Darwin 24.6.0
  • Hook side: UserPromptSubmit hook with hookSpecificOutput.sessionTitle fires correctly, and internal session_title state is saved (verified via Claude Code internal logs)
  • UI side: Desktop sidebar entry never reflects the hook output, regardless of how many prompts are submitted in the session
  • After confirming the divergence, we rolled back our settings.json hook entry — the documented sessionTitle mechanism is effectively unusable from a Mac Desktop-started session

Behavior matches @luisdlugo's reverse-engineering trace exactly: the Desktop app appends custom-title lines to the session JSONL but does not invoke the backend rename function, so the sidebar's local_*.json is never updated.

Workaround we adopted: manual session naming (no automation). The CLI-start workaround (start claude in a terminal so /rename synthesis 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.

duckin-alabcoat · 2 months ago

Confirming this also affects the SessionStart hook (not just UserPromptSubmit) on v2.1.163.

Setup: a SessionStart hook with matcher: "startup" emits one pure-JSON object on stdout, exit 0:

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "sessionTitle": "POGA·S28·Studio·SatJun6",
    "additionalContext": "=== SESSION START ===\n..."
  }
}

Observed:

  • additionalContext from this object is consumed every session (the orientation text reliably reaches the model).
  • sessionTitle from the same object is silently ignored — the picker keeps the auto-generated title.
  • A manual /rename on the same session does update the picker.

So within one hookSpecificOutput object, additionalContext is honored and sessionTitle is dropped — which lines up exactly with the root cause in this issue (the picker reads titleSource: "auto" from local_*.json; the hook never sets titleSource: "user", so only the UI /rename path is honored).

Worth noting #59267 (closed) tracked enabling sessionTitle on SessionStart — but on 2.1.163 the emitted value doesn't reach the picker, same as the UserPromptSubmit case here.

BryceEWatson · 2 months ago

Confirming on Windows (this is labeled platform:macos, but it's not macOS-specific): Claude Desktop 1.15962.1.0, same behavior. A SessionStart/UserPromptSubmit hook that returns sessionTitle writes a custom-title record, but the model's auto-titler writes an ai-title afterward and that's what the sidebar shows. Across the 18 sessions here that have both record types, all 18 end on the ai-title. /rename sticks; a hook's title doesn't.

Root cause, read from app.asar (.vite/build/index.js), the title-update guard:

const s = incoming.titleSource ?? "user";
s === "auto" && current.titleSource === "user"
  ? log("Ignoring auto-generated title: user already renamed")
  : (current.title = incoming.title, current.titleSource = s);

An auto title is dropped only once the session is already titleSource: "user". /rename coerces to "user", so it sticks. A hook's sessionTitle is evidently applied as auto (I can't read that path directly: the ai-title writer is in the CLI sidecar and aiTitle is 0× in app.asar, but #67755 reports the metadata stays titleSource: "auto" and the behavior matches), and a hook only fires before the model's turn, so the later auto write always wins.

Fix would be either to apply a hook-set sessionTitle as titleSource: "user" (treat it like /rename), or add a titleSource field to the hook output (there's none today). Related Windows issue: #64304.

LeoGestetner · 1 month ago

Still reproduces on the current Desktop build — 1.19367.0 (2026-07-07), macOS. Fresh trace from today, plus experimental confirmation that the titleSource guard described in earlier comments is the entire root cause:

  1. A UserPromptSubmit hook emits hookSpecificOutput.sessionTitle ([Project] Short Title). Verified the hook fired for a Desktop-started session and produced the expected title.
  2. The Desktop sidebar discarded it and kept its own AI auto-title. The session's record in ~/Library/Application Support/Claude/claude-code-sessions/<acct>/<group>/local_*.json stayed titleSource: "auto" with the auto-title — the hook output never reaches that store.
  3. Confirming the guard: externally editing that JSON record to the hook's title with 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 hook sessionTitle output through the same path /rename uses (set titleSource: "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.

LeoGestetner · 1 month ago

Follow-up with a sharper root-cause, from further testing on 1.19367.0 today. The titleSource guard in the app is real but it is not what blocks hook titles — the title update never reaches the app at all:

  • When a UserPromptSubmit hook emits hookSpecificOutput.sessionTitle in a Desktop-started session, the CLI does apply the rename locally: it appends a custom-title record to the session's .jsonl transcript each time. Verified with multiple emissions across several prompts — the transcript accumulates the records.
  • But the rename is never forwarded over the stdio stream-json bridge to the desktop app, so the app's in-memory session store (and therefore the sidebar) never hears about it. The app's own updateSession guard would actually accept the hook title in the common case — decompiled logic only rejects an auto-sourced title when the session was already manually renamed (titleSource: "user"); auto freely overwrites auto.

So the fix is plumbing, not policy: when the CLI applies a hook-driven rename (the same path that writes the custom-title transcript record), emit the corresponding session-update event over the bridge — the code paths on both ends already exist, since manual /rename and 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.

axyojp · 1 month ago

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 UserPromptSubmit hook returning sessionTitle updates every local store correctly, but the cloud-side title never moves:

| Store | Value |
|---|---|
| transcript custom-title | 🔧 watcher 実装確認 (current) |
| pid file ~/.claude/sessions/<pid>.jsonname | 🔧 watcher 実装確認 (current) |
| transcript ai-title | セッション名の自動更新機能の実装 (turn 1) |
| claude.ai/code sidebar + desktop sidebar | matches ai-title, i.e. stuck at turn 1 |

So /resume and 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 the ai-title generation path does. And in a bridge worker neither of the two push routes is usable: CLAUDE_CODE_REMOTE_SESSION_ID is not present in the process environment, and no bridgeSessionId is recorded (absent from both the pid file and the transcript), so updateBridgeSessionTitle is 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 the SessionStart side.

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.

Showing cached comments. Read the full discussion on GitHub ↗