[BUG] Remote Control leaves bridge-session-only .jsonl stubs that show up in /resume as empty "(session)" entries

Status Open
Maintainer reply None cached
Activity 0 comments · opened Oct 15, 2026

What's Wrong?

With Remote Control enabled, a 267-byte <uuid>.jsonl gets left in ~/.claude/projects/<project>/ containing only a single line:

{"type":"bridge-session","sessionId":"<uuid>","bridgeSessionId":"cse_…","lastSequenceNum":0,"ownerAccountUuid":"…","ownerOrganizationUuid":"…"}

It has no user or assistant messages. The /resume picker lists these as an untitled (session) entry showing "267 bytes" and no branch, usually at the top since it's the newest file. With remoteControlAtStartup: true, 68 of them built up across 14 projects.

Root cause (from reading the v2.1.300 bundle; minified names)

saveBridgeSession (Zit) is meant to never create a transcript for a session that doesn't have one yet. It only writes if the guard passes:

function Qwt(e,n,r){return Boolean(e.sessionFile)||r?.targetExists===!0&&n!==void 0}

The REPL bridge hook captures the session at bridge init: Ta=K(), Ql=xu() (the computed <Ta>.jsonl path), and ya=()=>Ta===K()?Ld()??Ql:Ql.

  1. Launch a fresh session with Remote Control on. Ta = the fresh, not-yet-materialized session; nothing is written (guard fails, link kept in memory only). Correct.
  2. /resume another session. The session-change handler only sets the detached flag (case"resume": ho.current=ci!==Ta); Ta/Ql keep pointing at the fresh session.
  3. On exit (hook teardown), the non-clear branch calls Zit(Ta, wi.bridgeSessionId, …, ya(), …, void 0, it). Since Ta!==K(), ya() returns Ql, the never-materialized fresh session's path. The guard checks Di().sessionFile, which is now the resumed session's file (truthy), so it passes and XT creates <Ta>.jsonl with only the bridge-session line.

So the guard checks the current session's file but the write targets a different session's path.

The /clear path in the same hook already handles this correctly, using the old session's real file and skipping when it doesn't exist:

let fi=wi===Ta?Ld():Ri; if(fi!==null){ ... F7(Ta,fi,{targetExists:!0},it) ... }

This also explains why each stub's mtime matches the resumed session's exit-time cost-state/last-prompt writes to the millisecond.

What Should Happen?

The teardown path should resolve the old session's real file the same way the /clear path does (or append with onlyIfExists), so no transcript is created for a session that never had one. Optionally, the /resume picker could also skip transcripts that contain no conversation messages, which would hide the stubs already on disk.

Steps to Reproduce

  1. Set "remoteControlAtStartup": true (or run /rc immediately in a fresh session).
  2. Without sending a message, /resume an existing session.
  3. Exit Claude Code.
  4. Run /resume again: an empty (session) entry appears for the original fresh session ID; its .jsonl contains only the bridge-session line.

(Steps derived from the code path above and the on-disk evidence.)

Environment

  • Claude Code v2.1.300
  • Windows 11

View original on GitHub ↗