Background launch provisions a second job identity with no spec or prompt, and publishes it in ListAgents as an addressable idle peer

Status Fixed / completed
Reported on v2.1.234
Maintainer reply None cached
Activity 1 comment · opened Aug 18, 2026 · closed Aug 19, 2026

What's Wrong?

Launching a single background job provisions two job identities about 33 ms apart. Only one of them ever receives a spec and a prompt. The other gets a job directory and a pre-warmed claude bg-spare worker, and then sits with respawnFlags: [], no state.json, no timeline.jsonl and no transcript. It has never been given any task at all.

That empty identity is nevertheless published to other sessions: ListAgents lists it as bg · idle, formatted identically to sessions that are doing real work, and it is addressable by SendMessage.

When a peer session addresses it, the empty identity wakes up, materialises a transcript, auto-names itself from the incoming message, and answers as a normal session.

This is a correctness problem for cross-session coordination, not just list noise. Sessions here use ListAgents + SendMessage as a lock-announcement protocol ("I am about to write to file X: are you holding it?"). A never-used slot answers that question truthfully about itself ("I am not holding it") while being structurally incapable of holding anything, and the answer is indistinguishable from a real all-clear given by the session that actually works in that directory. The sender cannot tell the two apart from the ListAgents output, because the empty slot and a genuinely idle worker render the same.

What Should Happen?

Either of these would fix it:

  1. Do not publish job identities that have never been given a spec or a prompt. Keep them out of ListAgents until they are claimed for real work.
  2. Or distinguish them in the output, e.g. unclaimed / no task instead of idle, so a peer can tell "idle because it finished its work" from "idle because it never had any".

Secondary, lower severity, same episode. When a session's very first prompt is an inbound cross-session message, the auto-generated display name is derived from that message. ListAgents then shows the sender a name that paraphrases the sender's own announcement, which reads as confirmation that the right session picked the message up. Auto-naming from the first prompt is reasonable in general (a session started from a human prompt gets its name the same way, and that is fine), but combined with defect 1 it makes a session that has never touched the topic advertise that exact topic back to the peer that asked about it.

Error Messages/Logs

No error is emitted; everything below is normal-path state on disk. CLI 2.1.234, Linux, daemon backend.

One background job was launched from an interactive session at 09:51:49. Two job dirs were created, 33 ms apart:

$ stat -c 'birth=%w  %n' ~/.claude/jobs/*
birth=2026-08-18 09:51:49.563443372 -0300  /home/<user>/.claude/jobs/<A>
birth=2026-08-18 09:51:49.596443441 -0300  /home/<user>/.claude/jobs/<B>    # never used

48 minutes later, <B> still had nothing but an empty tmp/:

$ ls -la --time-style=full-iso ~/.claude/jobs/<B>/
drwxrwxr-x  tmp/     2026-08-18 09:51:49.596   # created at job birth, never modified again
# no state.json, no timeline.jsonl, no transcript under ~/.claude/projects/

The two identities differ exactly as you would expect from "one was launched, one was not":

<A>: respawnFlags=['--agent','claude','--permission-mode','auto','--model','opus[1m]']   intent="<the human prompt>"
<B>: respawnFlags=[]                                                                     intent=<absent until a peer wrote to it>

Despite that, a third session's ListAgents at 10:39:08, 48 seconds before any message reached <B>, already listed it:

Peer sessions (4):
  <user>-62 [b6b441]   · interactive · idle · started 3h ago
  <A-name> [888a84]    · bg          · busy · started 47m ago
  <B> [f573d8]         · bg          · idle · started 47m ago
  ...

<B> is listed by its raw job id because it has no name yet: it has never had a prompt. The peer had no way to see that, picked it as a coordination target, and sent it a lock announcement at 10:39:56. What followed:

10:39:56.444   transcript file for <B> created  (exact second the peer's SendMessage landed)
10:40:09.947   ~/.claude/jobs/<B>/timeline.jsonl created; first entry state="working"
               state.json now carries "nameSource": "auto" with a name paraphrasing the peer's message
10:41:00       <B> answers the peer, truthfully, that it is not holding the file

Minor detail that looks like a timeline inconsistency but is not: the transcript file's birth is 10:39:56 while its earliest content timestamp is 09:51:53. The session context is assembled at job-creation time and only flushed to disk when a first prompt arrives.

Steps to Reproduce

  1. On Linux, with the background/agents daemon backend, launch a single background job from an interactive session (any prompt).
  2. ls -la --time-style=full-iso ~/.claude/jobs/. Two job directories appear within ~35 ms of each other. Only one gets state.json and timeline.jsonl.
  3. From a different session, call ListAgents. The unused id is listed as bg · idle, addressable, and rendered the same as a real worker.
  4. SendMessage to that id. It wakes, creates a transcript, auto-names itself from the text you sent, and replies as a normal session.

Claude Code Version

2.1.234

Is this a regression?

Not sure. This host has not observed the previous behaviour, so no last-working version can be named.

Platform / OS / Terminal

Claude subscription; Linux (Ubuntu 24.04.4, kernel 7.0.0); Ghostty.

Related issues (not duplicates)

  • #82107: background daemon spare roster advertising an already-reaped spare. Same daemon/spare area, opposite condition: that one is a dead worker being claimed, this one is a live worker that was never given a task.
  • #85497, #84945: cross-session peer socket failing to bind. Also about ListAgents/SendMessage reachability, but those are sessions that should be reachable and are not; this is an identity that should not be offered and is.

View original on GitHub ↗

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