remote-control (ccr transport) re-delivers tool_result chunks, causing sticky context corruption
Preflight
- Searched existing issues; not reported.
- Single bug report.
- On latest version (v2.1.177).
What's Wrong?
When connecting to a persistent host via the Claude Desktop "code" path (remote-control ccr transport) and running tool-heavy work, tool_result content gets corrupted:
- (a) whole lines duplicated (same line repeated hundreds–~1000×), and
- (b) in bad cases, fabricated data that the model then treats as real and builds reasoning on.
Once it starts, it persists for the rest of the session (sticky). Doing the same work on the same host over SSH (plain local claude) does not reproduce it.
Root cause (from v2.1.177 bundle analysis): the transport capability matrix shows fanout/presence/catchupReplay are ccr-only (ssh/direct have them off):
ccr: { fanout:1, presence:1, catchupReplay:1, bashExec:1, fileRead:1 }
ssh: { fanout:0, presence:0, catchupReplay:0, bashExec:0, fileRead:1 }
direct: { fanout:0, presence:0, catchupReplay:0, bashExec:0, fileRead:0 }
The ccr bridge spawns the SDK child with --replay-user-messages and the bridge handler holds a FIFO dedup cache resolvedToolUseIds capped at 1000 (trackResolvedToolUseId evicts the oldest tool_use_id once size > 1000). Two ccr-only mechanisms fit the symptom:
- M1 (primary suspect, explains "sticky"): after >1000 tool uses the oldest
tool_use_ids are evicted; acatchupReplay/replay carrying an evicted id is no longer recognized as resolved → thetool_resultis re-delivered. Eviction is irreversible and ids keep growing → stays broken for the rest of the session. - M2:
catchupReplayon detach→reattach (or a 2nd viewer joining via fanout) replays the outbound buffer from a miscounted cursor → already-delivered chunks re-emitted; cursor stays wrong → sticky.
Host-side transcripts (~/.claude/projects/.../*.jsonl, 49 scanned) contain zero line-duplication, confirming the corruption is injected in the ccr relay/replay leg — downstream of the persisted model context. This is also why "use a single session" did not fix it: parallelism only raises the chance of triggering a replay event; it is not the root cause.
What Should Happen?
tool_result delivered over ccr should be byte-identical to what the SDK child produced — no duplication, no re-delivery — regardless of session length, tool-use count, or detach/reattach.
Error Messages/Logs
No error is surfaced; the symptom is silent data corruption. Evidence is the ccr-only capability matrix above and the resolvedToolUseIds FIFO cap of 1000 in the bridge handler.
Steps to Reproduce
- Connect to a persistent host via Claude Desktop "code" (remote-control ccr transport).
- Run tool-heavy work (many Bash/Read calls; drive the tool-use count toward ~1000). To target M2, detach→reattach Desktop partway through.
- Observe
tool_resultline duplication / fabricated data that then persists for the rest of the session. - Decisive capture: start the bridge with
--verbose --debug-file <path>, capture the relay stream-json, and diff it against the SDK child's project*.jsonl. The first divergence localizes the injection point (host transcript clean, relay duplicated).
Falsifiable predictions: M1 → onset correlates with cumulative tool-use ≈1000; M2 → onset correlates with a detach/reattach event, independent of tool count.
Is this a regression?
Not sure — SSH/direct paths are unaffected; only the ccr (Desktop "code") path corrupts.
Suggested fix
- Keep
resolvedToolUseIdsmonotonic for the whole session, or dedup at re-delivery time on a per-emitted-message cursor/ack rather than ontool_use_idalone. - Pin the
catchupReplaycursor to the acked offset and forbid resetting it on reattach.
Environment
- Claude Code v2.1.177 (Bun single-executable), Linux x86_64.
- Path that reproduces: Claude Desktop "code" → remote-control (ccr).
- Path that does NOT reproduce: SSH → local
claude(ssh/direct).
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗