File edits from an aborted/retried streaming attempt are executed on disk but never recorded in the transcript ("ghost write")

Open 💬 0 comments Opened Jul 15, 2026 by alejandrozenker

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Summary

During a normal turn in a Claude Desktop (CCD) session, a file on disk was modified with model-generated content that no transcript records. The write collided with the visible assistant's own subsequent Edit call, which failed with "File has been modified since read". All evidence points to tool calls from a first streaming attempt of the same API request being executed, then the stream dropping and the request being retried — the retried attempt became the recorded turn, while the first attempt's side effects persisted on disk with no trace, no revert, and no record.

Environment

  • Claude Desktop (macOS): 1.21459.0
  • Claude Code CLI (bundled): 2.1.209
  • macOS 26.5.2 (Darwin 25.5.0), Apple Silicon (Mac Studio)
  • Model at the time: claude-opus-4-8 (thinking enabled)
  • Session: very large, long-running (transcript ~100 MB; app logged "tail-loading last 50 MB" at boot)
  • Session UUID: f0175513-c424-413e-98d4-3f48b764af62 (org ID available privately on request)

Timeline (UTC, 2026-07-15) — from transcript JSONL, app main.log, and filesystem mtimes

| Time | Event |
|---|---|
| 07:51:26.9 | User message delivered (app sendMessage logged exactly once, only to this session) |
| 07:53:28.2 | Recorded assistant issues Read of the target file → returns the pre-existing version (request req_011Cd3Uj) |
| 07:53:29.0 | Read result committed; next API request dispatched: req_011Cd3Ut |
| 07:53:29 → 07:55:36 | ~2 minutes of silence for req_011Cd3Ut (no recorded output) |
| 07:54:49 | ⚡ Target file mtime — the ghost write (falls inside that silent window) |
| 07:55:36.1 | First recorded output (thinking) of req_011Cd3Ut |
| 07:55:44.7 | Recorded assistant's Edit → rejected: "File has been modified since read" |
| 07:55:48.8 | Recorded assistant re-Reads → sees the ghost content |

Why the ghost content provably came from this same session's context

  1. It correctly applied the user's freeform notes from the 07:51 message (HTML-ified, restructured — clearly LLM-authored).
  2. It contained a distinctive phrase that appears only in the assistant's own earlier prose in this conversation, not in the user's notes → the writer had the full conversation context.
  3. It made the same editorial decision the recorded assistant made independently moments later, with the same idiosyncratic habits (updating <title> + meta description + JSON-LD schema together with the <h1>).

Ruled out (with positive evidence)

  • Any other local session/subagent: scanned every *.jsonl under ~/.claude/projects and ~/Library/Application Support/Claude modified in the prior 8 h (1,061 files), including all tool_use inputs — no write of this content anywhere. App's CliGovernor logged "1 effective" session during the window. Transcript DAG of the session is strictly linear (no forks, no sidechains; requestIds sequential).
  • App-level duplication: main.log shows the user message was sent exactly once to exactly one session.
  • MCP file tools (Desktop Commander): in-memory call history contains only months-old calls.
  • Remote tools bridge: only periodic keepalives + get_device_info, zero served tool calls.
  • SSH / other machines: no logins since Jul 12 (console only). Hooks: none configured. HTML preview window: no activity in the window. The user: denies editing; content is unmistakably model-generated.

~/.claude/file-history was not in use for this session (not even the assistant's recorded, successful edits appear there). No CLI-side API/retry logs appear to be persisted, so the retry cannot be observed client-side — but it should be verifiable server-side via req_011Cd3Ut (whether that request had a dropped first stream / multiple attempts around 07:53:29–07:55:36 UTC on 2026-07-15).

Hypothesis

Tool_use blocks from a streaming attempt that later aborted were executed (file written 07:54:49), the attempt's output was never committed to the transcript, the request was retried, and the retry became the recorded turn — colliding with its predecessor's side effects.

Impact

  • Integrity: files change on disk with no attributable record — undetectable unless a subsequent edit happens to collide.
  • Downstream harm actually observed: the recorded assistant, confronted with an inexplicable "modified since read", confabulated attributions (first blamed the user, then a colleague of the user) before a forensic pass identified the likely mechanism. User trust was materially damaged before the correction.

Suggested directions

  1. Don't execute tool calls until the message/turn they belong to is durably committed to the transcript; or
  2. If tools may execute mid-stream, then on stream abort either (a) record the executed calls anyway (flagged as aborted-attempt), or (b) revert/checkpoint their side effects; and
  3. Surface retries in a client-side log so post-hoc forensics don't require filesystem archaeology.

Related (but distinct)

#3513 / #76361 track false-positive "modified since read" errors (linters, branch switches). This report is the inverse: the error fired correctly, exposing an unrecorded write by an orphaned attempt of the same turn.

Available artifacts (on request)

Full timeline with transcript excerpts, app main.log extracts, the ghost file version, and an MD5 manifest of the affected directory.

What Should Happen?

Tool side effects should only ever exist as part of a recorded turn: either tool_use blocks don't execute until the message is durably committed to the transcript, or side effects of aborted streaming attempts are recorded (flagged) or reverted. No file should change on disk without a corresponding transcript record of who/what changed it.

Error Messages/Logs

== Transcript (session f0175513-c424-413e-98d4-3f48b764af62), UTC 2026-07-15 ==
07:51:26.986  user      message (the user's notes; length 4584)
07:53:23.299  assistant thinking                     req_011Cd3Uj
07:53:28.772  assistant tool_use Read -> sobre.html  req_011Cd3Uj
07:53:29.020  user      tool_result (pre-existing file version)
07:55:36.140  assistant thinking                     req_011Cd3Ut   <- first recorded output, ~2 min after dispatch
07:55:44.765  assistant tool_use Edit -> sobre.html  req_011Cd3Ut
07:55:44.771  user      tool_result: "File has been modified since read, either by the user or by a linter."
07:55:48.818  assistant tool_use Read -> sobre.html  req_011Cd3V4   (returns the ghost content)

== Filesystem ==
$ stat -f 'mtime: %Sm | bytes: %z' sobre.html
mtime: Jul 15 01:54:49 2026 | bytes: 16496        # = 07:54:49 UTC, inside the silent window

== Claude Desktop main.log (timestamps local, UTC-6) ==
2026-07-15 01:51:26 [info] LocalSessions.sendMessage: sessionId=local_8311e597-..., messageLength=4584, imageCount=0, toolStates=0, attachmentCount=0, priority=(default)
2026-07-15 01:51:27 [info] Mapping internal session local_8311e597-... to CLI session f0175513-c424-413e-98d4-3f48b764af62
2026-07-15 01:53:30 [warn] [CliGovernor] memory pressure (warning): would evict 0 idle session(s), 1 effective   # only ONE CLI session running
2026-07-14 23:58:33 [warn] [CCD] Session f0175513-... transcript is 100237150 bytes; tail-loading last 50 MB     # (from app boot, shows session scale)

== Transcript DAG check (07:51-07:58 window) ==
strictly linear parent->child chain, zero forks, zero sidechains, requestIds sequential (req_011Cd3Uj -> Ut -> V4 -> V5 -> VB)

Steps to Reproduce

This is a race condition, not deterministically reproducible. Conditions under which it occurred, and how to verify:

  1. Use a very large, long-lived Claude Desktop (CCD) session — transcript ~100 MB (app logs "tail-loading last 50 MB") — with thinking enabled (observed on claude-opus-4-8). Time-to-first-token in this state ran ~2 minutes per request.
  2. Send a message that will lead the assistant to edit a local file it has already Read.
  3. If the streaming attempt drops after tool_use blocks were emitted and executed, and the request is auto-retried, the retry becomes the recorded turn — while the first attempt's file edits persist on disk with no transcript record.
  4. Detection signature (what to look for): the recorded assistant's next Edit fails with "File has been modified since read"; the target file's mtime falls inside the silent gap between request dispatch and the request's first recorded output; and grepping ALL local transcripts (including tool_use inputs) finds no writer of the file's new content, while the content itself demonstrably derives from the session's own context.
  5. Server-side verification (recommended): inspect request req_011Cd3Ut of session f0175513-c424-413e-98d4-3f48b764af62 (2026-07-15, dispatched ~07:53:29 UTC, first recorded output 07:55:36 UTC) for a dropped first stream / multiple attempts. The unrecorded file write landed at 07:54:49 UTC, between the two.

Claude Model

Not sure / Multiple models

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Claude 1.21459.0 (f7518f) 2026-07-14T05:32:17.000Z

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

_No response_

View original on GitHub ↗