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).
4 Comments
**Additional repro on v2.1.181 — fabricated content surfaced as a phantom (non-persisted) user message, not duplicated
tool_resultlines**Hit variant (b) ("fabricated data that the model then treats as real and builds reasoning on") again on 2026-06-18 via the same ccr / Desktop "code" path. New detail worth flagging: this time the fabrication did not show up as duplicated
tool_resultlines — it surfaced as an entire phantom user turn that the model reacted to, but which was never persisted to any transcript.What happened
Orientrequest + a few tool calls, the model produced a reply reacting to a user message reading roughly:残タスクは全て完了した。drive撤去、別PCもクローンした。サーバーは切り替えた。… Gドライブもクローンされたから安心だね.Forensic evidence (from the session
.jsonl)type=user/role=usertext input in any~/.claude/projects/**/*.jsonl— only as later assistant quotes and (eventually) my own dispute message quoting it back.Orientuser turn → the model'sReadcalls → an assistant reply that already reacts to the phantom message, with zero user messages in between.Orientsummary (the assistant turn that should sit between the Reads and that reply) is also missing from the transcript — consistent with an interrupt-time replacement: the phantom turn carried a[Request interrupted by user]marker.Suggested triage signature
type=userrecord;ccr-onlyfanout/catchupReplayroot-cause analysis above);Env
v2.1.181(4 patches after the originalv2.1.177report; still reproduces).claudedoes not reproduce, matching the original report.Follow-up — security implication: what does the auto-approval / Bash permission classifier ground its decision on?
Building on the repro above, there's a security-relevant escalation worth confirming.
During a contamination episode the corruption lands in the model's output (the generated tool-call
commandstrings), because the model reasons over poisoned context. The only thing standing between that and execution is the permission layer. So the critical question is:rm -rfthat the model "decided" to run based on a phantom user instruction) can be auto-approved, because the classifier is fed the same poisoned stream. Contamination → fabricated command → auto-approval, with no real-input check anywhere in the path.Why this matters for #68434 specifically: this is the variant-(b) failure mode ("fabricated data the model treats as real") combined with action, not just reasoning. In my repro the fabrication only produced wrong analysis; but the same mechanism could, in principle, produce a destructive tool call, and whether that gets stopped depends entirely on the classifier's grounding source.
Requests:
Practical mitigation in the meantime (for anyone hitting this): keep Bash permission prompts on when using the ccr / Desktop "code" path, and do irreversible/destructive work over plain SSH
claude(which doesn't reproduce the corruption).Follow-up on v2.1.183 — SSH-launched
claude+ remote-control/fork left an orphan daemon, and the live session had read a different project's session logI hit another variant today. This one is slightly different from my previous ccr/Desktop-only reports: the session was originally started over plain SSH, but remote-control (
/rc active) and a fork lane were active during the contaminated state.New evidence
After I stopped the SSH-launched interactive
claude, several Claude Code processes remained alive:PPID=1);claudeprocess;codegraph,qmd) and active HTTPS connections.Process shape, redacted:
Daemon log excerpt:
Strongest anomaly
Before I stopped the interactive SSH
claude, I observed this:But the same process had a read fd open to a different project's session log:
So while running in project A, the live interactive Claude process had read at least the first 64 KiB of project B's session JSONL. This looks like cross-project/session context access, not just a UI display artifact.
UI state during the incident
The tmux/Claude UI showed:
The model-visible contamination also looked like genuine Claude Code harness/fork content entering the wrong conversation. The session itself described a
fork-boilerplate-shaped message appearing in the main conversation, which matches this local process evidence.What I did not see
ccrprocess was present in the process list at the time of this snapshot.Why this may be useful for triage
This suggests the issue may not be limited to ccr/Desktop transport replay. There may also be a remote-control/fork-session cleanup or routing bug where:
I have a local forensic snapshot with full commands and raw outputs. I am not posting the full JSONL publicly because it contains private project/user context and large model payloads, but I can provide the raw
daemon.logand the two relevant session JSONLs privately if useful.This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.