Backgrounding a named session duplicates its name in the session list

Status Open
Reported on v2.1.235
Maintainer reply None cached
Activity 1 comment · opened Aug 19, 2026

Backgrounding a named session duplicates its name in the session list

Version: 2.1.235 · Platform: macOS 15 (Darwin 25.6.0), arm64 · Install: native (~/.local/share/claude/versions/2.1.235)

Summary

Backgrounding a session that has a user-assigned name (/rename) forks it and copies the name verbatim to the fork. The parent's job record keeps the same name, so the session list shows two entries with identical names and no way to tell them apart. It recurs on every background, so collisions accumulate — I ended up with four sessions in one project of which three carried the same name.

The name-uniqueness machinery that normally prevents this cannot see the collision, because it compares only live pid-registry names while the list displays names from two other stores.

Reproduction

  1. Start a session and /rename my-session.
  2. Send a prompt so the session has content to fork.
  3. Press <kbd>←</kbd> on an empty input line to open the agents view (default leftArrowOpensAgents). Confirm "Background this session?" if a turn is in flight. The /exit → "Move to background and exit" path does the same thing.
  4. claude agents --json --all.

Expected: one my-session, or the fork disambiguated (the fork writer already has a " (fork)" suffix for this).

Actual: two rows named my-session — the frozen parent (state: done) and the live fork (state: working).

Observed

my-session   background  done     <parent-id>   ← frozen parent
my-session   background  working  <fork-id>     ← fork
myproj-42    interactive busy     <parent-id>   ← same session as row 1, derived name

Three generations of the same bug in one project, reconstructed from ~/.claude/jobs/*/state.json:

| Background | Parent | → Fork | Result |
|---|---|---|---|
| 1st | A | B | both named my-session |
| 2nd | B | C | both named my-session |
| 3rd | C | D | both named my-session |

Cause

Three stores hold the displayed name and none of them is the one that gets checked for uniqueness:

| Surface | Reads from |
|---|---|
| claude agents background rows | ~/.claude/jobs/<id>/state.jsonname |
| claude agents interactive rows | ~/.claude/sessions/<pid>.jsonname |
| /resume picker | custom-title record in the transcript .jsonl (last-wins) |
| uniqueness check | ~/.claude/sessions/*.json live pid registry only |

Two things then combine:

  1. The fork inherits the name. The background seed is built as name: customTitle ?? aiTitle with nameSource: "user", and the job-state writer stores it straight through with no collision resolution. The fork's transcript writer does have a fallback — let d = r.title?.trim(); if (!d) d = \${n() || "Forked session"} (fork)\` — but the background path always supplies r.title, so the (fork)` suffix never fires.
  1. The parent is invisible to the check. The backgrounded parent stays in the live registry under a derived name (nameSource: "derived"), not its custom title. When the fork registers the custom title, listLive() reports no holder, so the collision resolver returns settle: "kept" and the name stands. The frozen parent's job record — which is what the list actually renders — is not part of the live registry at all and can never be collided against.

Two independently-named live sessions do collide-check correctly and one auto-yields with nameSource: "collision"; this path bypasses that entirely.

Suggested fix

Any one of these would close it:

  • Let the (fork) suffix fire when the requested title is already held by a non-live job record, rather than only when no title is passed.
  • Run the collision resolver over job-state names as well as live pid-registry names when seeding a background job.
  • Clear or suffix the parent's custom-title when its work is handed to a fork, since the parent transcript is frozen from that moment.

Workaround

Rename the leftover in both stores: edit name in ~/.claude/jobs/<jobid>/state.json and append a custom-title record to the frozen parent .jsonl. Both are safe only because the parent stops being written at the moment of the fork — confirm with mtime. ctrl+r on the row in the agents view covers it if done before the transcript is re-touched.

Note that /rename in the still-open window is not a workaround: seconds after the background, that window re-attaches from the fleet surface to the fork (~/.claude/daemon/attach-journal/*.json, surface: "fleet"), so it renames the live session rather than the leftover.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗