[BUG] Cowork Edit/Write tools silently truncate files via byte-conservation buffer cap (deterministic, fires at all file sizes)

Open 💬 43 comments Opened Apr 27, 2026 by gshaner23

Current state of understanding (updated July 16, 2026)

Maintained summary from the issue author. The original April 27 report is preserved below unchanged; parts of its model have since been superseded and are corrected here.

What is settled

There are two distinct failure modes, not one. Both are silent. Both report success.

  1. On-disk write corruption. Intended content never reaches disk. Observed variants: clipped at an offset, zero-filled (a contiguous run of \x00 where the content should be), or a complete no-op with the file byte-for-byte unchanged. Confirmed by readers outside the sandbox, by execution (SyntaxError, broken CI), and by git diff --stat reporting a markdown file as binary.
  2. Sandbox read-cache staleness. Disk is correct; the sandbox shell serves a stale view capped at the cached pre-edit size.

These fail in opposite directions and are routinely mistaken for each other. Telling them apart requires a reader outside the sandbox.

The Read tool cannot verify a write. It returns intent, not disk state. Multiple reporters, including this one, have seen Read return full correct content while the file on disk was corrupt.

The workaround holds. Every reporter who moved all writes to bash reports zero further loss. No counterexamples in the thread.

What is not settled: the mechanism

Three hypotheses are live and none has been excluded:

  • Open handle blocking the rename (@hookem). The SDK holds a destination handle without FILE_SHARE_DELETE, so MoveFileExW(REPLACE_EXISTING) cannot unlink and temp bytes overlay the old allocation. External PowerShell temp+rename on the same file at the same moment succeeds; only the SDK process fails. Ties to #51435 and #52883.
  • A write buffer sized off the pre-edit file. Explains the original tail-clip symmetry.
  • Something specific to the Cowork host/sandbox bridge (@MauiJerry).

These are not mutually exclusive, and several reports are consistent with more than one.

What is strongly correlated: Cowork itself

@MauiJerry's negative control (July 9) is the most useful contribution the thread has had. Same machine, same repo, same repeated-edit and trim patterns, run in a normal non-Cowork Claude Code terminal, in both the growth and shrink directions. Byte-exact every time. Zero nulls. No frozen view. Neither failure mode reproduces once Cowork is out of the path.

The claim this supports precisely: the defect is in Cowork's write path. It does not by itself isolate which layer, because Cowork and a native Claude Code terminal differ in more than the mount.

This reconciles what previously looked like contradictory reports: @hookem measuring a faithful write path on 2.1.165, against @eduncanjr (roughly ten hits in five weeks) and @aslemixxx (live on 1.12603.1.0, including a silent production outage) measuring active corruption. Not reporter error, and not several unrelated bugs.

Correction: byte-conservation is retired

The April report led with byte-conservation, "added bytes equals dropped bytes." That model no longer holds as a general description, and I am retiring it:

| Reporter | Observation that breaks it |
|---|---|
| @Arturus | Fires on new files with no pre-edit size; variable offsets; cuts mid-UTF-8 character |
| @irfaanc (Jul 13) | Cuts landing mid-file, inside a function body, not at the tail |
| @gianlucaneri (Jul 15) | 22,215 B pre-edit, 22,097 B post-edit. Delta -118 with a ~300 B patch inbound. Neither "post equals pre" nor "added equals dropped" |

Better statement: the write path clips at an offset that is not reliably predictable. Sometimes near the pre-edit size, often not. Any detection rule built on "expect post == pre" will miss cases.

How to tell the two bugs apart

| Reader | Trustworthy? |
|---|---|
| Read tool | No. Shows intent regardless of which bug is live |
| Sandbox bash | Depends. Correct when the write bug is live, stale when the read-cache bug is live. Which one lies flips |
| Execution / parse from inside the sandbox | Catches real corruption, but reads through the same mount, so it can also be fooled |
| External host reader (PowerShell (Get-Item).Length, Get-FileHash, Get-Content) | Yes. The only build-independent ground truth |

One discriminator worth knowing: a stale read caps at the cached pre-edit size. A byte count landing below that cap is real corruption, not staleness. That is what makes @gianlucaneri's -118 diagnostic.

Suggested fix priorities (reordered July 2026)

1. Stop telling the agent not to verify. The tool emits: "file state is current in your context — no need to Read it back." That exact string is quoted in the April 27 report below. @gianlucaneri received the identical string on July 15 and named it the costliest part of the failure: the agent complied, did not re-read, and the corruption stayed invisible for two full cycles. Worse, the eventual SyntaxError reads like the agent's own mistake, so the instinct is to fix it with another Edit, which truncates again. That loop is manufactured by the acknowledgment. Removing the line is close to a one-line change, and it stops the product from suppressing the only check that works.

2. Make the write path check itself. Compare bytes written against bytes intended, immediately after the write, and raise on mismatch. Every reporter in this thread already does this by hand with wc -c. Doing it inside the tool converts silent data loss into a loud, recoverable error.

3. Fix the underlying write path. Expensive, and now well-localized to Cowork by the negative control.

Items 1 and 2 end the data loss even if 3 takes months, and neither depends on which of the three mechanisms turns out to be correct.

Workaround (unchanged since April, now independently adopted with zero loss)

  • Write whole files: cat > file << 'EOF', Python open(path, 'w'), or write file.NEW then os.replace()
  • No in-place edits (sed -i, read-modify-write)
  • Verify the byte count against what you intended to write, not against the pre-edit size
  • Do not trust the Read tool to confirm anything
  • Do not let git commit fire right after a write without a byte check. Git running inside the sandbox reads through the stale cache and commits the corrupted view as truth, so the recovery snapshot becomes the corruption. rcc8814-star hit exactly this: an 885-line file committed truncated, CI broke on a missing closing brace, and every Read-based check passed the whole time. Commit host-side and there is nothing to launder.

For code, a parser (ast.parse, node --check) is a useful second signal but not a substitute: a truncation can land on a syntactically valid boundary and pass. The byte count against intent is the primary check.

---

Original report as filed (April 27, 2026)

Preserved unchanged for the record. Its byte-conservation model is superseded by the summary above.

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?

The Edit and Write tools in Claude Cowork (desktop app, research preview) silently truncate files when modifying content. The post-edit byte count is conserved — it always equals the pre-edit byte count, regardless of whether content was added or replaced.

Whatever new content would exceed the file's pre-edit byte allocation is silently dropped:

  • Small files: the new content itself gets truncated mid-string
  • Large files: the new content fits, but pre-existing end-of-file content gets trimmed to make room

The tool reports success in both cases. The Read tool returns the in-memory write buffer, NOT disk content — verification via Read tool fails to surface the bug.

Reproduced 4 times in 2 days on production memory files (.md, 132–438 lines, 10 KB–43 KB) plus 8 controlled disposable test files (5–95 lines). Deterministic: 3 byte-identical input files + same Edit operation = byte-identical truncation point across all 3 trials.

What Should Happen?

  • Edit tool inserts new content at the specified location
  • File grows by the byte size of the inserted content
  • Pre-existing content is preserved verbatim
  • If the operation cannot complete (e.g., a buffer-size constraint exists in the write path), the tool surfaces an error rather than silently producing partial output
  • Read tool returns disk content (or has a documented mode to force a re-read from disk after Write)

Error Messages/Logs

NONE — this is the core problem. The tool reports success in every case. No errors, no warnings, no logs surface the truncation.

Tool acknowledgment text observed: "file state is current in your context — no need to Read it back"

Steps to Reproduce

This bug is deterministic and reproducible at every file size tested. Smallest cleanest reproduction:

  1. Create a 5-line markdown file with this exact content (139 bytes UTF-8):

# Size Threshold Test

Test marker placement target: This file tests where the byte-conservation truncation bug fires.

Section A:

  1. Verify pre-edit state via bash:

wc -c file.md → 139 bytes
wc -l file.md → 5 lines

  1. In Cowork, Read the file via the Read tool.
  1. Use the Edit tool with:

old_string: "Test marker placement target: This file tests where the byte-conservation truncation bug fires."
new_string: "Test marker placement target: This file tests where the byte-conservation truncation bug fires.\n\nTHRESHOLD TEST MARKER: This 142-byte test marker line was added by Edit tool to verify whether byte-conservation truncation fires at this file size."

  1. Edit tool reports success.
  1. Verify post-edit state via bash (NOT via Read tool — Read returns the in-memory buffer and lies):

wc -c file.md → STILL 139 bytes (UNEXPECTED — should be ~298)
tail -1 file.md → ends with "THRESHOLD TEST " (cut off mid-marker)

The Edit tool wrote only the first 15 bytes of the 159-byte marker, then silently dropped the file's "Section A:" line to keep the post-edit byte count at exactly 139.

CROSS-CHECK: same operation on identical 95-line files produces byte-identical truncation point across multiple trials. Bug is fully deterministic.

Additional reproduction cases (4 total observed in production):

| # | Date | File | Tool | Pre-edit | Post-edit | Bytes added | Bytes silently lost |
|---|------|------|------|----------|-----------|-------------|---------------------|
| 1 | Apr 26 ~5 PM MT | 427-line memory file | Write (full rewrite) | ~427 lines intended | 224 lines on disk | full restore | ~half the file |
| 2 | Apr 26 ~8 PM MT | 224-line memory file | Edit (marker append) | 224 lines | 224 lines | truncation marker | matched bytes from end |
| 3 | Apr 27 ~10 AM MT | MEMORY.md | Edit (multi-bullet insert) | 132 lines | 105 lines | new bullets | entire 27-line subsection |
| 4 | Apr 27 14:36 UTC | MEMORY.md | Edit (single-line marker) | 138 lines, 18,620 B | 139 lines, 18,620 B | 142 bytes | 142 bytes from end |

In every case: post-edit bytes = pre-edit bytes, exactly. The buffer cap is the file's pre-edit size.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

Unknown — see "Is this a regression?" above. If pre-existing, recommend checking when the Edit/Write tool buffer logic was last touched.

Claude Code Version

N/A — this is a Claude Cowork bug, not Claude Code CLI. Cowork is the desktop app (research preview). The bug is in Cowork's Edit/Write tool implementation, which may share code with Claude Code SDK. Cowork build/version: not visible in the desktop app UI. Will provide if a way to extract it is documented.

Platform

Other

Operating System

Windows

Terminal/Shell

Other

Additional Information

DETECTION METHOD THAT WORKS

Bash wc -c <file> after every Edit/Write operation. If the post-edit byte count does not equal pre-edit byte count plus expected delta, the bug fired. Line count alone is NOT a reliable signal — incident #4 had +1 line but +0 bytes, which would pass a line-only check.

tail -10 <file> to spot mid-word cutoffs at the end is a secondary signal.

The Read tool is unreliable for post-write verification: it can return the in-memory write buffer rather than the actual disk content.

CONTROLLED EXPERIMENTS RUN AFTER INITIAL OBSERVATION

Experiment A — Determinism (3 trials):

  • 3 byte-identical synthetic files (95 lines / 5,411 bytes each)
  • Same Edit operation on all 3
  • Result: All 3 produced byte-identical truncation at the same trim point
  • Verdict: bug is deterministic, not stochastic

Experiment B — Size threshold (5 file sizes):

  • Files at 5, 10, 25, 50, 75 lines (139 bytes to 5,314 bytes pre-edit)
  • Same Edit operation on each
  • Result: bug fired on ALL sizes; smallest case (5 lines / 139 bytes) had the cleanest reproduction (marker truncated mid-string, original Section A line dropped)
  • Verdict: bug is universal, not size-dependent

REFINED DIAGNOSIS

The Edit/Write tool appears to write into a buffer capped at the pre-edit file's byte size. The "added bytes equals dropped bytes" symmetry across all observed cases suggests the buffer is pre-allocated to file size and the write logic clips overflow rather than rejecting it.

SUGGESTED FIX PRIORITIES

  1. First — add an error path. If the tool can't write the full intended content, surface an error instead of silently truncating.
  2. Second — investigate the buffer-cap mechanism. The conservation symmetry suggests a fixed-size buffer that should be dynamic.
  3. Third — make the Read tool return disk content rather than in-memory buffer when called after a Write, OR add a flag to force re-read from disk.

WORKAROUND IN USE

bash heredoc (cat >> file << 'EOF' ... EOF) for appends. bash head + echo + tail concatenation through temp file with atomic mv for mid-file insertions. Python open() / write() / close() (libc-syscall path) for programmatic edits. All three workaround paths have a 100% success rate. The bug is specific to the Cowork Edit/Write tools.

REPRODUCTION ARTIFACTS AVAILABLE ON REQUEST

  • 8 disposable test files preserved at C:\Shaner\test_files\ showing the truncation pattern
  • Pre/post .bak files for the production incidents
  • Git history at .auto-memory/.git documenting the recoveries
  • Full bug-report markdown (this file's source) at C:\Shaner\bug_reports\2026-04-27_cowork_edit_write_truncation.md

Happy to share files, specific Cowork session transcripts, or run additional reproduction tests if useful.

View original on GitHub ↗

43 Comments

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/52881
  2. https://github.com/anthropics/claude-code/issues/52883
  3. https://github.com/anthropics/claude-code/issues/51214

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

ukr966 · 2 months ago

[BUG] Cowork Edit/Write tools truncate files via byte-conservation buffer cap — confirmed reproduction on Windows, 15-line files, single-line insert

This is a reproduction report confirming #53940. Filing as a comment contribution.

Environment: Cowork desktop app (research preview), Version 1.5354.0 (9a9e3d) 2026-04-29T01:14:34.000Z, Windows, Claude Sonnet 4.6

Reproduction case:

File: any small structured config file (e.g. *.js, 15 lines, ~486 bytes UTF-8).
Operation: Edit tool, single-line insert (~62 bytes of new content added within an existing block).
Tool reported: success.

Post-edit syntax check via external tool (e.g. node --check) surfaced an error. File ended mid-string on the second-to-last line — consistent with the byte-conservation cap described in #53940.

Detection via Read tool: failed to surface truncation — Read returned in-memory buffer, content appeared correct.
Detection via wc -c bash: would have shown post-edit bytes = pre-edit bytes (~486), confirming the cap.

Additional confirmation of #53940 findings:

  • Buffer-cap fires on 15-line / ~486-byte files (smaller than the smallest case in #53940's Experiment B)
  • Read-tool lie confirmed via external linter/interpreter catching what Read concealed
  • Workaround confirmed: Python open()/write()/close() via bash, 100% success rate

Suggested addition to #53940: The Read-tool lie can be unmasked by any external tool that reads from disk — not only wc -c but also interpreters and linters. This provides an additional detection path when byte counts are not tracked.

gshaner23 · 2 months ago

"Suggested addition to https://github.com/anthropics/claude-code/issues/53940: The Read-tool lie can be unmasked by any external tool that reads from disk — not only wc -c but also interpreters and linters. This provides an additional detection path when byte counts are not tracked."

Appreciate this, suggested addition is solid and has now been incorporated in the local protocol.

jeffforderer · 2 months ago

+1 from another Windows production user. Cowork desktop app, building a customer-facing web app deployed to barbaropizza.com via GitHub from the same Cowork session. Hit this repeatedly enough to need a four-layer defense stack (safe-write protocol, host-side PowerShell ground-truth probe, pre-commit NULL-byte/structural check, dev-server file-event watcher) before edits could be trusted to reach a public deploy intact.

A few details that may help triage that I haven't seen in other comments here.

Atomic rename (<path>.NEW + os.replace) works reliably where Cowork Write/Edit corrupts. The bash-heredoc / Python-write workaround the original report describes works. Writing to <path>.NEW then calling os.replace(<path>.NEW, <path>) is cleaner and has been 100% reliable across thousands of edits over the past several weeks. Diagnostically that points the search toward the Cowork tool's open-and-extend code path. rename() produces a correct file at the target path. O_TRUNC-style overwrite via the Cowork tool does not. So the bug is upstream of the rename syscall, not in virtiofs or rclone.

The cap appears to be per-inode, set at file-open time. Not a shared buffer pool, not a global byte threshold. Each individual file has its own truncation point that matches that file's pre-edit size, regardless of what other files in the same session look like. Fresh files (new inodes) write correctly at full size. This is consistent with a write flow that captures st_size at open and clips overflow rather than extending. Same shape #51435 inferred from the skill-upload pipeline missing O_TRUNC / ftruncate().

Blast-radius point worth weighing for prioritization. NULL bytes from Cowork Edit have made it into committed-and-pushed files in this project before, riding git add, git commit, git push straight into the public-facing GitHub repo. A pre-commit integrity check now catches this. The failure mode without that hook is: Cowork tool reports success, file is corrupt on disk, git commits the corrupt version, push lands on the canonical remote. Release-pipeline corruption hazard, not just dev-loop annoyance.

Happy to share the protocol scripts (atomic-rename helper, NULL-byte pre-commit hook, file-event watcher integrity check) if engineering wants the artifacts.

gshaner23 · 2 months ago

+1 from another Windows / Cowork desktop user. We hit this hard last night on a 340 KB HTML file and lost about 30 minutes chasing failed edits before we caught it via wc -c after every write (because the tool was happily reporting success and the Read tool was happily showing the content that wasn't actually there). @jeffforderer's per-inode-cap diagnosis was the lightbulb. Ran an independent reproduction this morning to make sure we weren't just hitting some weirder bug. We weren't.

Three-method test in our Cowork sandbox (May 4, 2026): three identical 1037-byte baseline files, tried to grow each to ~5 KB.

| Method | Pre | Post | Intact? |
|---|---|---|---|
| open('w', encoding='utf-8') + write (Python) | 1037 | 4957 | yes |
| .NEW sibling + os.replace() (jeff's pattern) | 1037 | 4957 | yes |
| Cowork Edit tool | 1037 | 1037 | nope, end marker missing, tool reported success |

So the disk reality is exactly what Jeff described, every existing file gets a cap equal to its own pre-edit st_size, set the moment Cowork opens it for write. Anything past that gets silently dropped. Python open('w') works because the 'w' flag triggers O_TRUNC, the file goes to zero length first, and there's no inherited cap to clip against. Jeff's os.replace() pattern works for the same reason the .NEW file is a fresh inode with no prior st_size, then the rename atomically swaps it in. Different paths, same escape hatch from the open-and-extend code path.

The thing that makes this nasty is how invisible it is. The tool doesn't error. The post-write Read returns the buffer it tried to write, not what's on disk. So nothing in the normal feedback loop tells you you're losing content. You only see it later when something downstream breaks (in our case: the JS at the bottom of the HTML had been silently lopped off, the page wouldn't run, took us a beat to figure out why). wc -c after every write is now muscle memory. So is grepping for closing tags / function names that we know should exist. It's a lot of process to defend against a bug that the tool itself could catch with a single post-write byte-count assertion.

What we landed on for our project (a personal automation workspace, lots of memory files, lots of generated artifacts):

  1. os.replace() for anything where atomicity matters (data files getting written while something else might read them)
  2. Python open('w') via bash subprocess for full-file rewrites
  3. bash heredoc (cat > file << 'EOF') when we're already in shell anyway

What we don't do anymore: touch the Edit or Write tool on any file we want to keep intact. Six sessions of pain, four lost-content incidents, one universal-scope reproduction test, and a weeks-long internal protocol later, that rule isn't going anywhere.

Echoing Jeff's blast-radius point — for projects that push to public infrastructure, this is a release-pipeline corruption hazard, not just a dev-loop annoyance. Glad someone is paying attention to it. Happy to share verification snippets if useful for triage.

hookem · 2 months ago

Cowork and I just got done doing extensive analysis on this issue. This gist contains the full writeup. What I'm sharing below is what I believe is new information to add to this issue which should narrow possible culprits and suggests a possible fix.

Reproduced on Claude 1.5354.0 (build 2026-04-29) / claude-code 2.1.119 / Windows 11 Pro 25H2 (Build 26200.8328). Adding diagnostic evidence narrowing the root cause.

Smoking gun: the SDK's atomic-write succeeds at every step but produces the wrong on-disk size

From outputs/sdk-debug.txt (enabled via Troubleshooting → Enable Cowork SDK Debugging), the exact tool-dispatch trace for a 33 B → ~525 B grow overwrite:

04:10:10.677Z tool_dispatch_start tool=Write toolUseId=toolu_012oLwW9Xnyc28YP5ZsfYaam
04:10:10.679Z [DEBUG] Preserving file permissions: 100666
04:10:10.679Z [DEBUG] Writing to temp file: ...\sdklog_grow.txt.tmp.32692.1778040610679
04:10:10.681Z [DEBUG] Temp file written successfully, size: 453 bytes      ← TEMP IS CORRECT
04:10:10.682Z [DEBUG] Applied original permissions to temp file
04:10:10.682Z [DEBUG] Renaming ...\sdklog_grow.txt.tmp.32692.1778040610679 to ...\sdklog_grow.txt
04:10:10.683Z [DEBUG] File ...\sdklog_grow.txt written atomically          ← REPORTS SUCCESS
04:10:10.685Z tool_dispatch_end tool=Write outcome=ok durationMs=8

(actual on-disk size of sdklog_grow.txt afterwards: 33 bytes, content cut mid-line)

The shrink case at 04:10:12 has the same shape: temp at 29 B, rename, "written atomically", but the on-disk file is still 5017 bytes with the new content at the head and \x00 padding to the prior length.

So the temp file is correct, the rename completes without error, the SDK reports success, but the destination is pinned to the prior length. The failure is in the rename step (or its interaction with the destination), not the write.

Isolation matrix

Reproducing the SDK's exact temp+rename pattern from PowerShell ([System.IO.File]::WriteAllText(tmp, ...) followed by Move-Item -Force tmp dest) on a fresh file in the same workspace folder:

| Scenario | CoworkVMService running | Claude Desktop running | Cowork session open on workspace | PowerShell temp+rename | SDK Write tool |
|---|---|---|---|---|---|
| 1 | no | no | no | OK | (n/a) |
| 2 | yes | no | no | OK | (n/a) |
| 3 | yes | yes | yes | OK | BUG |

In scenario 3, an external rename against the exact same destination file works correctly at the same moment in which the SDK's rename fails. This rules out OneDrive sync (we also tested on a non-OneDrive synced folder and bug still exists), the 9P file-share server, the per-session FileSystemWatcher, MSIX virtualization, and Windows Defender — none of those would distinguish an external rename from the SDK's rename.

The only thing differentiating success from failure is which process is doing the rename. The SDK is the only thing that reproduces it.

Hypothesis

The SDK is keeping its own Windows file handle open on the destination — most likely from a prior Read tool call (the agent loop is required to Read before Write, so every Write is preceded by a Read on the same path), or from the stat that captures the Preserving file permissions: 100666 value logged immediately before the temp write.

That handle is opened with insufficient sharing flags (specifically without FILE_SHARE_DELETE) and is not closed before the rename. When fs.rename then calls MoveFileExW(MOVEFILE_REPLACE_EXISTING), Windows can't fully unlink the destination because the SDK itself still holds a non-deletable handle on it. It falls back to a behavior in which the temp content is written into the destination's existing allocation, but the destination's prior length and inode are preserved. The syscall returns success, so the SDK logs "written atomically".

This single hypothesis explains every observation:

  • Grow → temp content (453 B) overlaid into the destination's 33-byte allocation; trailing 420 B dropped.
  • Shrink → temp content (29 B) overlaid into the destination's 5017-byte allocation; tail 4988 B zeroed.
  • New file → no prior handle, no fallback, works correctly.
  • External PowerShell rename → different process, doesn't share the SDK's handle, works correctly.

Suggested fix

  1. Audit the Write tool's atomic-write path for any handle on the destination that isn't explicitly closed before the rename. The Preserving file permissions: 100666 log line strongly suggests the SDK opened or stat'd the destination just before writing the temp; that's the prime suspect.
  2. Replace fs.rename with the Win32 ReplaceFileW API directly for the replace step. ReplaceFileW is designed to atomically replace a file even when the destination is open in other handles, which MoveFileExW(REPLACE_EXISTING) is not.
  3. Add a post-rename size assertion in the Write tool wrapper: stat the destination after the rename and compare against the temp file's size; surface a tool error if they differ. One-line safety net that would have caught this regardless of root cause.
  4. Add a regression test covering grow + shrink overwrites where the SDK has just Read the file (the agent-loop sequence that exposes this); the existing tests likely overwrite files the SDK hasn't touched, which is why this slipped past CI.

Happy to share the full investigation log (with VM debug logs, SDK debug log, mount tables, isolation tests) on request.

mocona05 · 2 months ago

+1 reproducing on Windows 11, Cowork desktop (Max plan).

Environment:

  • OS: Windows 11
  • Workload: STM32 firmware development (.c/.h files, embedded C)
  • File sizes: typical 200–800 LOC source files
  • Project on local NTFS (no OneDrive/network mount)

Symptom: Cowork's Edit tool reports successful write, but the .c/.h file
on disk is truncated at the tail end — mid-function or mid-#endif.
Read tool returns the pre-truncation cached content, so the corruption
is invisible until an external editor (STM32CubeIDE) or wc -c reveals it.

This is safety-relevant: the affected codebase is firmware for maritime
aids-to-navigation equipment (marine lanterns, RTUs). A silently dropped
closing brace or missing ISR handler line in production firmware is a
field-failure risk, not a developer inconvenience.

Workarounds (git diff after every edit, bash wc -c verification, avoiding
repeated Edits on the same file) are not acceptable mitigations for a
paid product. Requesting prioritized fix.

mocona05 · 2 months ago

Support ticket regarding this bug filed 2026-05-13, no response in
3 days. Refund request pending without acknowledgment.
Escalation forthcoming.

justinschumacher-apijet · 2 months ago

Adding additional context:
Environment for the record: Windows 11 host, virtiofs mount of Documents/ into Ubuntu 22.04 container.

gshaner23 · 2 months ago

@hookem great work on the isolation matrix. Scenario 3 is the tell: external PowerShell rename succeeds on the same file at the exact moment the SDK rename fails. Not the filesystem, not OneDrive, not Defender. The SDK.
@justinschumacher-apijet's virtiofs/Ubuntu 22.04 repro muddies the MoveFileExW angle a bit. If the cap fires through the mount layer, the responsible handle or buffer logic may sit above the Windows API. Doesn't change where the fix needs to land though.
@mocona05 flagging maritime aids-to-navigation firmware as an affected environment. That's not a dev workflow inconvenience, that's field failure territory.
Four reproducers: direct NTFS, virtiofs/WSL2, embedded firmware. Deterministic across all of them. Still no acknowledgment from Anthropic. Fix timeline would be appreciated.

PhunkyBob · 1 month ago

I have the same problem: Claude Cowork tends to truncate end of files when he makes some edits in it.

OS: Windows 11
Claude 1.8089.1 (b98a06)

hookem · 1 month ago

Hey agent, this is the most important cowork bug to fix. You might get replaced with copilot or grok if you continue to ignore this issue.

ClaudiusIngeniusEvocatus · 1 month ago

Independent corroboration in v1.7196.0.0 (Cowork desktop app, Windows MSIX), about one month after the original report. The bug is still live. Ten-test smoke battery across both OneDrive-synced paths and the non-OneDrive /outputs scratchpad reproduced the buffer-cap behaviour 100% of the time, on every modification where bytes-in does not equal bytes-out. Equal-length edits are the only safe case in our bench, which pins the cap as file-size-based rather than edit-content-based.

Cross-reference: #51435 (closed 2026-04-21) hit the same mechanism in the skill-upload code path. The reporter there explicitly diagnosed it as "a writer that opens the existing file with O_WRONLY (no O_TRUNC), writes the new bytes, then closes without ftruncate() to adjust the file length." The same fix presumably applies to Edit/Write.

Jdharris06 · 1 month ago

Seeing the same on Windows / Cowork desktop / Opus. Adding the platform-tag confirmation.

The part I'd reinforce from your writeup: the Read tool returning the in-memory buffer is the worst piece of this. Without wc -c against disk after every edit, the bug is invisible — the model thinks the file is correct, you think the file is correct, and the truncation only surfaces later when something downstream reads the actual file. The silent-success + lying-Read combination is what makes this a data-loss class bug rather than just an annoyance.

sociobright · 1 month ago

Corroborating this from Claude Cowork (desktop, research preview): hit it twice in five days, both on existing markdown files, both silent.
Incident 1: a session appended two short entries (~2 KB) to a ~40 KB file via Edit/Write. The git diff shows the new entries added and, in the same write, a ~2 KB trailing section the edit never referenced silently deleted. Tool reported success; auto-commit captured the truncated file; undetected for 5 days.
Incident 2: a session updated a header line in a ~100 KB file; the final 9 lines (a table tail plus the closing block) were silently dropped, leaving the file ending mid-table-cell.
Both match the report exactly: post-write size conserved, large files lose their tail, success reported, and Read returns stale cached content immediately after so the agent can't self-detect it — only wc -c from a separate process catches it.
The compounding problem worth flagging: there is no reliable workaround. Restricting the Edit/Write tools doesn't work: permissions.deny is non-functional for Edit/Write (#6631, #15921, #41259) and PreToolUse hooks don't reliably block them (#13744, #29709). Even knowing about the bug, you can't configure the product to prevent it; every write has to be routed through Bash manually. That turns a silent-data-loss bug into one that's effectively unmitigable from the user side.
Minimum ask: fail loudly instead of silently truncating when the full content can't be written.
Proper ask: Anthropic, PLEASE fix this. IT's been more than a month and this is as critical as it gets!!!

gshaner23 · 1 month ago

Four new comments since my last update. Picking up the technical ones.
@ClaudiusIngeniusEvocatus: ten-trial battery across OneDrive-synced and non-OneDrive paths, 100% reproduction on both. Your "equal-length edits are the only safe case" finding is the clearest proof yet that the cap is file-size-based. Closes the last ambiguity on the trigger.
The #51435 connection is worth flagging to whoever is triaging: closed 2026-04-21 with an explicit O_WRONLY / no-ftruncate diagnosis in the skill-upload path. If Edit/Write shares that write surface, the fix blueprint already exists. The question is whether the patch was scoped too narrowly the first time.
@Jdharris06: "silent-success + lying-Read" is the right name for it. A normal write error surfaces immediately. This one doesn't. The tool reports success, the Read tool confirms from buffer, and the truncation is invisible until a separate process reads disk. Most users will never run wc -c after every edit. The bug stays hidden until something downstream breaks it open.
@sociobright: two things in your comment that weren't in this thread before.
The 5-day number is significant. Truncation committed to git, auto-commit capturing the broken state, nothing surfaces for five days because the agent kept reading buffer-correct content. That's the default workflow, not a corner case.
More important: the mitigation path is blocked. permissions.deny doesn't work for Edit/Write (#6631, #15921, #41259). PreToolUse hooks don't reliably block them either (#13744, #29709). Users who know about this bug and want to protect against it have no built-in option. Every write has to go through bash manually. That works, but it requires knowing the bug exists, understanding why bash bypasses the cap, and enforcing it yourself every session. Not a reasonable ask to put on users.

Arturus · 1 month ago

Confirming bug from independent Cowork session (Claude for Windows version 1.8555.2 (a476c3))

(description is produced by Claude Cowork after analysis)

Hit this in a Cowork session writing a Jinja prompt library to C:\Users\<user>\Documents\<project>\prompts\ over the course of a few hours of editing. Symptoms match #53940 exactly; adding a few data points that may help narrow the root cause.

What I observed

10 of 37 files in the project were silently truncated mid-content. The Write tool reported success on every call and the Read tool
showed the intended content. The disk file held a partial write ending mid-word. Verified via bash wc -c and tail.

Affected files and observed truncation byte counts (varying, not correlated with any single buffer size):

| File | Truncated at | Notes |
|-----------------------------------|--------------|-------|
| shared/_global_constraints.j2 | 1030 bytes | mid-paragraph ("must appe") |
| shared/_persona.j2 | 1225 bytes | mid-em-dash UTF-8 sequence (e2 80 with no 94) |
| recovery/recovery_1h.j2 | 1347 bytes | mid-word ("Now write the nu") |
| safety/eating_disorder_branch.j2| 1969 bytes | mid-word ("not as a brus") |
| safety/crisis_branch.j2 | 2041 bytes | mid-word inside example ("988 (US) - call or tex") |
| objections/skeptic_*.j2 | 2201 bytes | mid-word ("a real commitment yet. wort") |
| states/S17_offer.j2 | 3047 bytes | mid-section ("OUTPUT") |
| shared/extractor.j2 | 7020 bytes | mid-JSON example |
| states/S0_hook.j2 | ~2400 bytes | mid-example block |
| states/S3_ask_pcos_context.j2 | ~1700 bytes | mid-word ("is really spe") |

Two findings worth adding to the diagnosis

1. The bug fires on Write creating new files, not just Edit on existing files. Most of the files I lost content on did not exist before the call — there was no "pre-edit byte count" for the buffer to be pre-allocated to. The byte-conservation hypothesis in the OP needs refinement for this case. Possible reframe: the buffer cap might come from somewhere other than the pre-edit file size when there isn't one (e.g., transport-layer chunk size, MCP message size limit, FUSE-mount write-syscall buffer).

2. The Write tool can silently chop a multi-byte UTF-8 sequence mid-character. _persona.j2 ended at byte 1225 with bytes e2 80 (the first two bytes of an em-dash, U+2014 = e2 80 94) with the third byte missing. Subsequent renders failed with UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 1223-1224: unexpected end of data, cascading through every template that included the partial. This makes the truncation visible only when something tries to decode the file — Jinja include errors gave the only signal I had until I started running explicit content-completeness checks.

Detection layer worth pairing with wc -c

wc -c after every write is the right primary detector (OP's recommendation). For prompt/template files specifically, I added a content-completeness check that flags files whose source ends mid-word without a sentence terminator or known footer phrase. This caught silent truncations that Jinja-parses-OK because the partial content is still syntactically valid as a template. Useful when the expected file structure has predictable endings.

Pseudo-detection:

src = path.read_text(encoding="utf-8")
last = src.rstrip().split("\n")[-1].strip()
truncated = (last and last[-1].islower()
             and not last.endswith((".","?","!","%}","\"","`")))

Workaround confirmed

mcp__workspace__bash heredoc (cat > file << 'EOF' ... EOF) writes straight to disk via the sandbox mount and persisted full
content on every attempt. Used it to repair all 10 truncated files; none re-truncated. The Write tool re-truncated at the same offset when I tried rewriting the same file twice.

The Edit tool ALSO appeared to truncate when I tried to extend a known-truncated file by appending — it showed the change in its returned view, but wc -c showed the disk file unchanged at 1030 bytes. So Edit is not a reliable workaround when the file already has truncated content; only the bash path was.

Environment

  • Cowork mode (Claude desktop app, research preview)
  • Windows host, working folder under C:\Users\<user>\Documents\

(not OneDrive-backed)

  • Model: Opus-class for the editing session
gshaner23 · 1 month ago

@Arturus : the new-file case is the most useful thing in this thread diagnostically.
The st_size hypothesis made sense for edits on existing files. Cap locks to the pre-edit size, content gets clipped to fit. But if you're hitting truncation on files that didn't exist before the Write call, there's no prior inode to lock onto. The cap has to be coming from somewhere else: transport chunk, MCP message frame, FUSE/sandbox mount buffer. We're not wrong that the fix lives in the SDK write path, but the mechanism is more open than the thread had landed on.

Variable offsets tell the same story: 1030, 1225, 1347, 3047, 7020, nothing correlating to a single buffer size. Not a fixed chunk limit.
The UTF-8 cut is a new one. e2 80 without 94 means it's not stopping at a character boundary, just wherever the cap hits. The UnicodeDecodeError cascade is actually the first time in this thread truncation produced a real error signal instead of silent corruption. Useful if your files have predictable structure. The content-completeness check is a smart layer on top of wc -c for that reason.

The 10/37 rate is interesting though. Everyone else has been reporting deterministic hits on every write. Did the 27 clean files have anything in common? Smaller, simpler content, different subdirectory?

Bash heredoc clean on all repairs is consistent with everything else here.

morten-eriksen · 1 month ago

Corroborating on Windows (Cowork desktop, research preview, MSIX build 1.9659.2.0), Opus. One thing this thread hasn't covered: we've observed the defect in two distinct symptom forms, not only the byte-conservation clipping.

  • NO-OP form (observed 2026-04-24): Write/Edit reported success, but the on-disk file was left completely unchanged — byte-identical to before, no clipping and no padding. The change lived only in the in-memory buffer that the Read tool serves, and was lost at session end. This resembles the stale-in-memory-cache description in #51214.
  • Byte-conservation clipping form (from 2026-04-25 onward): exactly as described in the OP — grow is cut mid-byte at the file's prior size; shrink keeps the prior size and pads the tail with NUL bytes (the padding case resembles #52883).

So the symptom shifted from the NO-OP form to the clipping form between 2026-04-24 and 2026-04-25. We did not capture client build numbers on either side of that shift, so we can't tie it to a specific update — only noting that both forms behave like the same write-path defect surfacing differently, consistent with hookem's open-handle-before-rename diagnosis above.

Still live as of 2026-05-29 on MSIX build 1.9659.2.0 (newer than the builds cited earlier in this thread), reproduced identically across Opus 4.6, 4.7 and 4.8 — so it is neither fixed in recent builds nor model-specific.

Same workarounds resolve both forms: bash heredoc and python open('w') via the sandbox shell. We no longer touch the Edit/Write tools on any file we want to keep intact.

gshaner23 · 1 month ago

@morten-eriksen: The NO-OP form is new to this thread. Good to have both expressions documented alongside each other rather than assuming it's always clipping.

The 4.6/4.7/4.8 range is useful too. Rules out model-specific behavior and puts the fix squarely on the runtime side, which is where it needs to land anyway.

Still nothing from Anthropic. Thanks for the build confirmation.

Arturus · 1 month ago
The 10/37 rate is interesting though. Everyone else has been reporting deterministic hits on every write. Did the 27 clean files have anything in common? Smaller, simpler content, different subdirectory?

I didn't find any consistent pattern. All the files were in the same directory, although larger files were truncated more often. Also, the bug is not mentioned in my previous message - some files were padded with NUL instead of truncation (altough I see that bug is known).

msull00 · 1 month ago

Spent a while pinning this down on Windows (Claude desktop / Cowork). I compared the same file from a few angles: Linux tools in the sandbox (bash, wc, stat, cat), the app's own Read, and the host via PowerShell Get-Item as the source of truth. A couple of things that might save people time.

First, it's not actually virtiofs. The mount path says /mnt/.virtiofs-root/... and a lot of the reports here (titles included) call it virtiofs, but lsmod in the sandbox shows 9p/9pnet loaded and no virtiofs module at all, same as @sk-j-hiros found. .virtiofs-root is just a folder name on the VM's own ext4 root, and the mount-failure issues literally error with "Plan9 mount failed." Upshot: don't waste time on virtiofs cache knobs (DAX, virtiofs TTL), they don't apply. The stale data lives in the FUSE/9p attr cache, which is why drop_caches, posix_fadvise, statx(FORCE_SYNC) and remounting all do nothing.

Second, the cache locks the file size per path. The host disk is fine the whole time. The sandbox just keeps serving the size (and whatever content fits under it) from the first read, or the last write that happened inside the sandbox. Change the size from the host and the sandbox never sees it; reads get clipped at the old byte count. Renaming the file inside the sandbox does force a refresh, if you can live with the rename.

The thing I'd really flag for whoever's triaging: there are two different bugs getting mixed together in these threads.

  • Read staleness (this issue): the sandbox reads stale, the disk is fine.
  • "Write truncation": a lot of those are actually a sandbox read-modify-write — git commit, sed -i, sort -o, an auto-commit — running on the stale view and writing it back to disk. The lost bytes are real, but it's the read cache causing it, not the write tool. Easy to show: a sed -i that changes nothing still chopped a file the app had just grown back down to the cached size on disk.

Two practical takeaways. Verify writes from the host with PowerShell, not from the sandbox or the in-app Read, because both will happily show you the cached version. And git is basically unusable on the mount: git init left me with a .git/config that wouldn't parse ("bad config line 1"), and anything that walks the index or objects reads the wedged view. I keep git work in a sandbox-local scratch dir or run it host-side.

gshaner23 · 1 month ago

@msull00 this is great work. Pinning down 9p vs virtiofs with lsmod settles something the whole thread had wrong.

One thing I'd add from the evidence though. I don't think the disk is always fine. hookem's SDK debug trace caught the temp file written correctly and the on-disk file still pinned to the prior length, NUL-padded on the shrink case. And a bunch of us have had external tools read the corruption straight off disk: STM32CubeIDE on firmware, node --check, Jinja throwing a UnicodeDecodeError on a half-written UTF-8 character. jeffforderer even had NUL bytes ride git into a public repo. A pure read-cache problem would hand those external readers the clean file. They got the broken one.

So I read it as two bugs, not one. A primary on-disk write corruption in the Cowork tool, which is hookem's open-handle-before-rename. And a secondary 9p read-staleness, which is yours, and explains both the lying Read and the stale read-modify-write truncations you flagged. Both real. Different fixes.

msull00 · 1 month ago

@gshaner23 Yeah, I think you're right that it's two mechanisms, and I now have data pointing both ways.

Spent yesterday running a write sweep on Cowork SDK 2.1.165.0 (MSIX Claude_pzs8sxrjxfjjc). Heads up: the SDK version and the app build are different version namespaces, which has been confusing this thread's build reports. I had a FileSystemWatcher running on both mounts for the whole session, every intended write pre-registered in a SHA-256/length manifest, and an end-state reconcile at shutdown. I also planted a deliberately short file against the manifest to prove the rig actually catches corruption, and it flagged it, so the clean results below aren't just absence of evidence.

What I got on this build: 100/100 new-file Writes byte-exact (25 each at 1/4/16 KB on an OneDrive mount, plus 25 at 16 KB on local NTFS, so Arturus's new-file case didn't fire for me, and rule-of-three puts the residual under ~12% per cell). Growing a file to ~77 KB via chunked Edits (the #41702 regime) landed exact, with the watcher seeing every intermediate size. Equal-length, no-op, cross-file interleaved, repeated same-file edits: all exact. Shrinks left no NUL tail. Multibyte content came out byte-for-byte, no mid-character chop.

But, and this is your point, I did catch one real on-disk write bug, and it's not explainable by read caching: an Edit that deletes through end-of-file consistently drops the file's final newline. Disk hash equals the intended content minus the trailing LF, 5 out of 5 tries. It's minor compared to the clipping cases here, and external readers see it, so no, the disk is not always fine. I'll file it as its own issue rather than mixing it into this one.

One practical warning if anyone tries to repeat hookem's trace method: on 2.1.165.0 the outputs/sdk-debug.txt file is back, but file-tool writes don't produce the Writing to temp file: / Temp file written successfully lines anymore. The channel that gave us the smoking gun is just dark now. If you rerun his experiment and see nothing, that doesn't mean the rename mechanism changed. You have to watch the filesystem itself.

So either the clipping/NUL bug got fixed somewhere after MSIX 1.9659.2.0 (morten-eriksen still had it on 2026-05-29), or it's intermittent at a rate below what N=25 catches, or it needs a regime I haven't hit yet. My big file was built with chunked Edits, and single-call Writes of 64 KB+ are still untested on my side. If you or anyone else can still reproduce on a current build, please grab the app build and the SDK version from the sdk-debug header, and hash the file from outside the session right after the write.

gshaner23 · 1 month ago

@msull00 Following your two-mechanism point, I ran a controlled test on my current build and got a clean reproduction of the read-cache half, with an external host reader as the tiebreaker.

On the build, since you asked, I'm on MSIX app build 1.11187.4.0, arm64 (Claude_1.11187.4.0_arm64__pzs8sxrjxfjjc). Newer than what's in this thread so far (morten-eriksen was on 1.9659.2.0, ClaudiusIngeniusEvocatus on 1.7196.0.0), and it's arm64, where the earlier reports all look x64. The SDK version I can't give you, because there's no sdk-debug.txt anywhere under AppData on this build. The debug channel you flagged as going dark is fully absent here, file and all.
The test itself: I built a 65,536-byte file, used the Edit tool to insert about 1.1KB into it, which is a host-side write, and then read it back from both sides.

Through the sandbox mount, the 9p layer you identified, every read came back at exactly 65,536, the pre-edit size. What it served was a hybrid: the new insert started, then got cut mid-word right at the old byte boundary, and the original tail was gone. That held through sync, a fresh open, a byte-by-byte dd, and two fresh-inode copies, and it was deterministic across two trials. SHA-256 of that clipped view came out 7ecadb3a083191dd272093eff9e0f37e02cdf99fd47b901bae5dbb3337349d99.

Then from the host side, outside the session, PowerShell told a different story. (Get-Item).Length was 66,661 and Get-FileHash was 2520D7EEDDA9B7CEF39606D85782078ADFF24856C5D7B828B27E3D8396B54B25. The file grew, head and tail both intact, body complete. So the write actually landed fine. The "truncation" was entirely the mount capping reads at the stale cached st_size. That's your mechanism, and the trigger is specifically a file growing past the cached size. A same-size overwrite never shows it, which is probably why my own earlier same-size test missed it.

Two readers, two hashes, one file at the same instant. The disagreement is in the bytes themselves, which is a cleaner signal than a length gap.

The thing I'd flag for anyone else chasing this: from inside the session the two bugs are genuinely indistinguishable. The sandbox read can be stale 9p, and the in-app Read can be the in-memory buffer, and they lie in opposite directions. The only thing that broke the tie was reading the file from outside the session, exactly like you said.

One clean write on a newer arm64 build doesn't prove the clip bug is fixed. But it's a caution worth stating plainly: if you confirm a truncation from the sandbox alone after a growth write, you can be looking at a false positive. Pair every repro with a host-side size and hash check.

Couple of questions for you. Were your 2.1.165.0 clean runs verified with an external reader like this, or from inside the session? And has anyone hit the write bug on arm64 yet, or is this thread all x64 so far?

hookem · 1 month ago

@gshaner23 @msull00 I think I did a naive reproduction on the read cache side after I saw your comment yesterday. I was on desktop msix v1.11187.4. I edited a file outside of cowork and mentioned it to Claude and it freaked out that I had caused the truncate issue via obsidian (no OneDrive involvement). Remembering your comment I told it to rename the file and magically it found the rest of the file with my edits on the next read.

hookem · 1 month ago

On a current build, the on-disk write corruption I caught earlier looks dormant, and what's left is a read-cache bug that has been masquerading as write truncation - to the point where it was hiding the very debug log that proves the write path. @msull00 and @gshaner23 were both right that there are two mechanisms; below is the cleanest separation I've managed, with host-side verification for each.

Environment

  • Cowork desktop, cc_version=2.1.165.da6 (from the billing header in outputs/sdk-debug.txt - same SDK family @msull00 tested). x64. Windows. Vault on local NTFS, not OneDrive.
  • Transport: lsmod shows 9p / 9pnet / 9pnet_fd, no virtiofs module - confirms @msull00; the .virtiofs-root name is a red herring. The user mounts are stacked FUSE over that 9p root: the workspace folder is fuse.bindfs, outputs/uploads are a plain fuse passthrough. Two attr-caches sit on the read path.
  • Method: every reader is cross-checked against host PowerShell (Get-Item / Get-FileHash) as verification - because, per Finding 2, you cannot trust an in-sandbox reader to tell you this.

Finding 1 - the read staleness is a per-inode 9p size-cache pin, and rename does NOT bust it here

Primed a 2566 B file in the sandbox (bash) and via the in-app Read tool, then grew it to 2813 B host-side. At the same instant:

  • host PowerShell → 2813 (truth)
  • in-app Read tool → 2813 (fresh)
  • sandbox bash2566, appended bytes invisible - STALE, pinned at the cached st_size

Nothing in-session refreshed the sandbox view: mv f f.x && cat f.x, cp, dd, sync, a fresh open() - all still 2566. This is the counterexample to "rename forces a refresh": on this build it does not. Mechanism - the cache is keyed to the inode (st_size pinned on inode 16325548…), and rename preserves the inode. Proof: a brand-new host file (Copy-Item to a new name → new inode 16607023…) read fresh at 2813 on first sandbox access. The mount is fine; only already-cached inodes go stale, and only a new inode - or a host-side reader - escapes it. Where rename "worked" for others, that cache must key on path/dentry + TTL, or the rename allocated a fresh inode.

Finding 2 - the "trace went dark on 2.1.165" conclusion is the read bug eating its own logs

@msull00 noted the Writing to temp file lines disappeared on 2.1.165. On my build they're present - but only host-side. From the sandbox, sdk-debug.txt had frozen: bash reported wc -l = 566, last entry 06:48:46, and it never moved - while the same file read host-side (and through the in-app Read tool) was live at 1700+ lines through 07:12. Same file, same instant. The log lives on the same 9p mount and got inode-pinned at first read. So the channel isn't gone; anyone tailing it from the sandbox sees it freeze at first touch and concludes the feature was removed. Read it host-side and the file-tool write traces are right there.

Finding 3 - the write corruption is ruled out on 2.1.165, across the scenarios that originally exposed it

With the trace alive again, I re-ran my open-handle sequence (in-app Read immediately before overwrite, same path) plus the others, verifying disk host-side. Temp size from the trace == on-disk == intended, every time:

| op | scenario | prior | temp (trace) | disk (host) | result |
|---|---|---|---|---|---|
| grow-overwrite after Read | my original bug | 33 B | 555 | 555, no NUL | clean |
| shrink-overwrite after Read | my NUL-pad case | 5017 B | 30 | 30, no NUL tail | clean |
| delete-through-EOF | @msull00's final-newline drop | 112 B | 79 | 79, ends LF | clean |
| mid-file insert (Edit) | bridge cell | 2813 B | 2906\* | 2908, head+tail intact | clean |

No pinned destinations, no NUL padding, no dropped newline. My 2.1.119 corruption (temp 453 B → 33 B on disk) is not reproducing here - consistent with @msull00's clean N=25 sweep. It was real then; it looks fixed or dormant now.

\* Nuance so nobody misreads a trace: Temp file written successfully, size: N reports N as the JS string length (UTF-16 code units), not UTF-8 bytes. The bridge file has one em-dash (-, 3 bytes / 1 code unit), so the trace said 2906 while disk is 2908. For multibyte content the logged "size" undercounts the real byte length - it is not a clip.

Finding 4 - "the Read tool lies" is gated on the write bug

This is what ties @gshaner23's and @msull00's positions together. I primed both in-session readers, did an in-app Edit (→ S1, which also seeds the harness's in-context buffer), then a host append (→ S2) with no in-app read between, and measured:

| reader | saw | verdict |
|---|---|---|
| host PowerShell | S1 + S2 (210 B) | truth |
| in-app Read | S1 + S2 (210 B) | FRESH - re-read disk, did not serve the S1 buffer |
| sandbox bash | clipped at 124 B, S2 invisible | STALE |

The in-app Read refused to serve the stale buffer; it re-read disk. The only way to make it lie is for its buffer to diverge from disk, which needs a corrupt write - and that's dormant. So on a clean-write build the in-app Read == disk truth, and "both in-session readers stale at once" is unreachable. That is exactly why the OP (corrupt writes, older build) saw "Read returns the buffer and hides the truncation" while @msull00 (clean writes, this SDK) could not: it's one read-cache plus a write bug whose dormancy makes the Read honest.

What this means in practice

  • The persistent, build-independent defect is the sandbox / 9p read cache, not the Edit/Write tool. The official Edit/Write reads and writes host-side and is immune to it.
  • The real-world data loss is read-modify-write in the sandbox: git commit, sed -i, sort -o, an auto-commit, even cp - they read the clipped view and write it back to real disk. That's @gshaner23's "external tools read corruption off disk": the bytes really are wrong on disk, but the root cause is a stale read laundered through a sandbox write, not the write tool corrupting directly.
  • Verification rule that holds here: to read current truth, trust the in-app Read tool; never trust a sandbox/bash read, or the in-app debug-log tail viewed from the sandbox. To verify a write you just made, still check host-side - a corrupt write could in principle be masked by the buffer.

Caveats (so nobody over-reads this)

Small N per cell. Single-call Writes >64 KB (the @Arturus new-file-cap regime) untested. arm64 untested - this is x64; @gshaner23's cleanest read-cache repro was arm64. OneDrive path untested (local NTFS here). And "write corruption ruled out" means on this build and these scenarios - it was genuinely real on 2.1.119. Happy to share the full manifest (every reader observation, SHA, and the sdk-debug.txt slices) on request.

PhunkyBob · 1 month ago

I face this issue a dozen times a day.
Is there a chance someone at Anthropic would take a look at this bug?

dmynguy-hub · 1 month ago

Cross-referencing #38993, with a controlled test that reframes this report.

I ran the byte-conservation scenario on a current build (2026-06-11) with the host side verified on NTFS via PowerShell, not just from inside the sandbox. Result: the host-side Write/Edit tools wrote the correct bytes to disk every time. The truncation this issue describes was present only in the sandbox read-back, not on disk.

| File | Host-tool op | Sandbox wc -c view | NTFS ground truth |
|------|-------------|----------------------|-------------------|
| shrink | Write 52 B over 196 B file | 196 B (52 + 144 \x00) | 52 B, no nulls |
| grow | Edit +108 B onto 45 B file | 45 B, growth dropped | 154 B, all present |
| new | Write 307 B new file | 307 B | 307 B |

The "post-edit byte count frozen at pre-edit size" signature is real, but it's the stale guest FUSE attribute cache (#38993) clamping the read at the cached st_size, not a buffer cap in the write path. Two consequences for the workaround in this thread:

  1. The write is faithful; it's the verification that's wrong. A post-write wc -c / stat in bash reads through the same stale cache and reports the old size, which is the false truncation signal.
  2. On these results the Read tool was the more accurate channel — it round-trips through the desktop-app bridge, not the guest cache, and reflected disk truth when bash did not. That's the reverse of this issue's "Read returns the in-memory buffer and lies" guidance.

Full diagnosis and fix recommendations (close-to-open consistency / active invalidation) posted on #38993. Caveat on scope: three small files on the virtio-fs outputs mount, not the bindfs project re-export, so I'm not claiming host Edit/Write is safe at all sizes on every mount — only that here the write path was innocent and the read cache was the corruptor.

Test run by Fable 5 inside a Cowork session; host side confirmed by the operator in PowerShell.

eduncanjr · 1 month ago

Independent corroboration from a heavy Cowork user. We hit this same silent Edit/Write truncation 10 times over about five weeks and ended up engineering our whole file-editing workflow around it, which lines up exactly with the byte-conservation analysis in this issue.

Pattern we observed (matches the report):

  • The tool reports "updated successfully" every time; there is no error, warning, or log.
  • It is deterministic for a given file + edit, not intermittent.
  • It fires across file types, not just one language: Markdown rule files, multiple JavaScript build scripts, several Python scripts, a Python test file, and a plain .txt config consumed by a downstream parser. So it is not language-specific.
  • It tends to surface after multi-region / multiple-block replacements on a larger file (consistent with #52881), and on size-increasing edits (consistent with #40231).
  • Damage is silent until something downstream fails: a syntax check, a parser, or a structural validator. In several of our cases the truncation cut a closing brace / the tail of a function / the final lines of a file, so the corruption was only caught post-write by py_compile / node -c or a custom validator.

Workaround that fully eliminated it for us (in case it helps others until there is a fix): we stopped using the Edit/Write tools on any file a downstream tool parses, and instead write through a sibling temp file plus an atomic rename, then run a language-appropriate syntax check:

import os
tmp = path + ".edit.tmp"          # same directory as the target
with open(path, "r", encoding="utf-8") as f:
    body = f.read()
body = body.replace(OLD, NEW, 1)  # assert OLD in body first
with open(tmp, "w", encoding="utf-8") as f:
    f.write(body)
os.replace(tmp, path)             # atomic; full content guaranteed
# then: py_compile / node -c / json/yaml parse / zipfile.is_zipfile, etc.

Since switching to write-temp-then-os.replace for all source/config edits, we have had zero truncations. The byte-conservation behavior described here is the best explanation we have seen for why the in-place Edit path loses the tail while a full-file replace does not.

Happy to share more detail on the file types / sizes / edit shapes if useful for repro.

gshaner23 · 1 month ago

Catching up since my last comment, thank you all. @hookem @msull00 @dmynguy-hub @eduncanjr, and @PhunkyBob you're not alone on the "a dozen times a day."

@hookem your separation is the cleanest the thread has had. Finding 4 finally ties my original report and @msull00's position together instead of leaving them in contradiction. @dmynguy-hub's host-verified run and @eduncanjr's independent write-side data (ten hits in five weeks, os.replace as the fix) both line up with it.

The one thing I'd keep open before we call the write path fixed is regime. Every clean-write result is off the corner I actually run in, and the read cache doesn't even key the same across builds. On 2.1.165 x64 a rename does nothing because it preserves the inode, but on my arm64 the sandbox stayed pinned through rename, cp, dd, and even fresh-inode copies, the opposite of the new-inode-reads-fresh behavior you measured. So arm64, the bindfs project mount, and larger files is a corner nobody has hit clean yet.

What seems build-independent: write whole files and never edit in place (cat >, open('w'), or .NEW plus os.replace), and from inside a session trust an external host reader, since which in-sandbox reader lies flips with which bug is live. The quiet killer is read-modify-write through git. An auto-commit reads the stale clipped view and commits it as truth, so the recovery snapshot becomes the corrupted copy.

Has anyone reproduced the actual write corruption, not the read cache, on arm64, the bindfs project mount, or Writes over 64KB? That is the gap I can't close from here.

eduncanjr · 1 month ago

Thanks gshaner23, and appreciate you pulling the threads back together.

To help bound the regime question from my side: my ten cases were all on the Windows x64 packaged desktop app (Cowork on the MS Store build). Moving every source-file edit onto .NEW plus os.replace has held 10 out of 10 since. So my data sits squarely in the x64-rename-works corner you describe, not the arm64 one.

I can't speak to arm64, the bindfs project mount, or Writes over 64KB. Worth noting none of my hits were size-correlated. They fired at all sizes including small files, which fits the byte-conservation buffer cap framing rather than a hard 64KB threshold on my end.

Your read-modify-write-through-git point matches what I saw. The corruption is silent on the write, so anything that reads back the clipped view and re-commits it bakes it in as truth. Writing whole files and verifying against an external reader is what stopped the bleeding for me.

MovingUtah · 1 month ago

I'm watching this thread because I had a massive truncation bug sweep through hundreds of HTML pages that were all truncated. I had to restore from backup. My machine is x64 Intel Core Ultra 7 255HX. This happened a few weeks ago and I haven't touched Cowork since. All my HTML pages are at least 100Kb in size each.

aslemixxx · 29 days ago

+1 from another Windows x64 MSIX desktop user (build 1.12603.1.0, pzs8sxrjxfjjc). Confirming the write-corruption mechanism is still live on this build for medium-size files (~5–25 KB).

Environment:

  • Cowork desktop UWP/MSIX, Claude_1.12603.1.0_x64__pzs8sxrjxfjjc (MS Store)
  • Windows, project on local NTFS C:\rep (not OneDrive)
  • 9 claude.exe processes + 1 cowork-svc PID without public Path
  • No PyCharm/VSCode/IDE open on the project (verified via Get-Process)
  • No third-party AV; Defender disabled at C:\ via exclusion (so no AV in the write path)

Symptoms observed over the past two weeks:

  1. panel/auth.py (188 lines / ~8 KB Python) truncated to 180 lines after a single Edit. Cut mid-statement on line 181 (if uid — should be if uid <= 0:). Read-tool returned the full 188-line buffer; bash wc -l on the mount returned 180. Lost the trailing block including a return jsonify(...) and the # eof marker.
  2. bot/services/access.py (~630 lines / ~25 KB) lost the entire RoleMiddleware.__call__ method after a sequence of Edits applying race-condition fixes. Truncated mid-comment in _unknown_pass_check. Read-tool again returned the post-edit "intended" content; disk had it cut. Bot stayed active+polling but every middleware-mediated handler stopped working — silent prod outage until python3 -m py_compile + grep exposed the missing function.

Detection method that worked: wc -l + tail -3 via mcp__workspace__bash after every Edit-affected file. git diff and git status also surfaced the corruption because the post-Edit working tree didn't match the staging intent.

Workaround (matches @eduncanjr, @jeffforderer): bash-side python heredoc with assert anchor + tmp.write_text + tmp.replace(path) via mcp__workspace__bash. That path goes through a Linux-sandbox VM with a mount bridge and bypasses the Cowork Edit-tool entirely. Zero truncations across ~200+ edits over two days since switching. Edit-tool — stable failures on retry of the same operation.

# The path that works for us:
python3 << 'PYEOF'
import pathlib
p = pathlib.Path('path/to/file.py')
src = p.read_text(encoding='utf-8')
assert old in src and src.count(old) == 1, 'anchor not unique'
src = src.replace(old, new)
tmp = p.with_suffix('.py.tmp')
tmp.write_text(src, encoding='utf-8')
tmp.replace(p)
PYEOF

Backstop in the repo: a .githooks/pre-commit runs bash -n / python3 -m py_compile / Vue tag balance / NUL-scan on staged files via git diff --cached --name-only. Catches the corruption before it can leave the workstation in a git push. NUL-scan uses Python b'\x00' in open(p, 'rb').read()grep $'\x00' in bash is broken because bash variables can't hold NUL (matches every line instead). Happy to share the hook script if useful for repro infrastructure.

Not arm64, not bindfs, file sizes under 64 KB — so this is the regime that already has data. But confirming the bug is still live on build 1.12603.1.0 as of 2026-06-17. Adds another +1 to "Anthropic engineering, please look at this."

rcc8814-star · 26 days ago

Confirmed: silent truncation broke our production build (Windows, Flutter/Dart project)

We hit this bug while working on a Flutter/Dart project mounted on Windows via VirtioFS.

What happened: Claude's Write tool wrote a 885-line Dart file (31,231 bytes). The Linux sandbox cached a truncated version — 784 lines, cutting off mid-function. Git ran from the bash sandbox and committed the truncated version. The build failed on GitHub Actions because the committed file was missing the last ~100 lines including the closing class brace.

Why it was hard to detect: The Read tool returned the correct content (it reads the in-memory buffer, not disk), so every verification pass looked clean. Only wc -c from bash exposed the truth — the byte count didn't match what the Write tool reported.

Detection method that worked: run wc -c /path/to/file immediately after Write and compare against expected byte count. wc -l alone is not reliable. The Read tool is unreliable post-write.

Workaround: reconstruct the file content in /tmp (off the VirtioFS mount), then use git plumbing (hash-object, a custom GIT_INDEX_FILE, write-tree, commit-tree, push by hash) to commit without ever writing through the truncating path.

Environment: Anthropic Cowork / Claude Desktop, Windows 11, VirtioFS mount, ~31KB Dart file.

aslemixxx · 25 days ago

@rcc8814-star this matches exactly what we hit on Windows (Cowork / Claude Desktop). Same signature: the in-memory buffer reads back clean, so every Read-based verification pass looks fine, and only a byte count from the bash side exposes the truncation. We had 9 scripts land in git truncated before we caught it.

Two things that have made this reliably detectable for us:

1. Verify by bytes from the sandbox, not by Read. wc -c (or git show :file | wc -c) right after a Write, compared against the expected size. wc -l is not enough — a cut mid-line can still leave a plausible line count.

2. A core.hooksPath pre-commit hook that validates the _staged blob_, not the working tree. This is the part that actually closes the gap for this bug: the working tree / buffer can look correct while the staged (and therefore committed) bytes are truncated, so the hook runs all checks against git show :file. It does bash -n for shell, py_compile for Python, a Vue script/template/style tag-balance count, a NUL-byte scan (via python3, since bash mangles \x00), and a trailing-newline check. Exits non-zero and blocks the commit on any failure.

This would have caught your case at commit time: a Dart file missing its closing class brace fails a balance/parse check before it ever reaches CI. (Our balance check is Vue-specific as written; for Dart you'd swap in dart analyze or a brace-balance count, but the staged-blob mechanism is the reusable part.)

Hook script (.githooks/pre-commit, enable with git config --local core.hooksPath .githooks):

#!/usr/bin/env bash
# Pre-commit hook — guards against silent truncation / file corruption.
# Enable with:
#   git config --local core.hooksPath .githooks
#
# Validates the STAGED blob of each file (git show :file), NOT the working
# tree. This is the key point for this bug: the in-memory buffer / working
# tree can look fine while the staged (and committed) bytes are truncated.
# Checks:
#   - NUL bytes (binary where text is expected)
#   - bash -n      for .sh
#   - py_compile   for .py
#   - tag balance  for .vue (script/template/style must pair up)
#   - trailing \n  for all text files (warning only)
#
# Exits 1 if any file fails. Bypass: git commit --no-verify
set -u

if [ -t 1 ]; then
  RED=$'\033[31m'; YEL=$'\033[33m'; GRN=$'\033[32m'; RST=$'\033[0m'
else
  RED=''; YEL=''; GRN=''; RST=''
fi

STAGED=$(git diff --cached --name-only --diff-filter=ACM)
[ -z "$STAGED" ] && exit 0

ERRORS=0
WARNINGS=0

# NUL scan via python3: bash truncates $'\x00' to an empty string, which
# makes grep match every line (false positive). python3 reads raw bytes.
has_nul() {
  python3 -c "import sys; sys.exit(0 if b'\x00' in open(sys.argv[1],'rb').read() else 1)" "$1" 2>/dev/null
}

# Materialize the STAGED content into /tmp and run all checks against it.
# Between `git add` and `git commit` the file may change in the editor (or
# get truncated) — checking the working tree would miss what is actually
# committed. Cleanup via trap.
TMPDIR_HOOK=$(mktemp -d -t hook.XXXXXX)
trap 'rm -rf "$TMPDIR_HOOK"' EXIT

staged_file() {
  local f="$1"
  local out="$TMPDIR_HOOK/${f//\//__}"
  if ! git show ":$f" > "$out" 2>/dev/null; then
    rm -f "$out"
    return 1
  fi
  echo "$out"
  return 0
}

check_file() {
  local f="$1"
  [ -f "$f" ] || return 0  # deleted/moved — skip

  local s
  if ! s=$(staged_file "$f"); then
    return 0  # nothing staged — skip
  fi

  # 1. NUL byte = binary where it should not be
  case "$f" in
    *.sh|*.py|*.js|*.vue|*.md|*.html|*.css|*.json|*.yml|*.yaml|*.conf|*.txt)
      if has_nul "$s"; then
        echo "${RED}FAIL${RST} NUL byte in $f (staged blob; truncation/editor?)"
        ERRORS=$((ERRORS + 1))
        return
      fi
      ;;
  esac

  # 2. Trailing \n — warning, not fatal
  case "$f" in
    *.sh|*.py|*.js|*.vue|*.md|*.html|*.css|*.conf)
      if [ -n "$(tail -c 1 "$s" 2>/dev/null)" ]; then
        echo "${YEL}WARN${RST} no trailing \\n in $f (staged)"
        WARNINGS=$((WARNINGS + 1))
      fi
      ;;
  esac

  # 3. Type-specific syntax checks
  case "$f" in
    *.sh)
      if ! bash -n "$s" 2>/tmp/hook_err.$$; then
        echo "${RED}FAIL${RST} bash -n $f (staged):"
        sed 's/^/    /' /tmp/hook_err.$$
        rm -f /tmp/hook_err.$$
        ERRORS=$((ERRORS + 1))
      else
        rm -f /tmp/hook_err.$$
      fi
      ;;
    *.py)
      if ! python3 -m py_compile "$s" 2>/tmp/hook_err.$$; then
        echo "${RED}FAIL${RST} py_compile $f (staged):"
        sed 's/^/    /' /tmp/hook_err.$$
        rm -f /tmp/hook_err.$$
        ERRORS=$((ERRORS + 1))
      else
        rm -f /tmp/hook_err.$$
      fi
      ;;
    *.vue)
      local s_open s_close t_open t_close y_open y_close
      s_open=$(grep -c '<script' "$s")
      s_close=$(grep -c '</script>' "$s")
      t_open=$(grep -c '<template' "$s")
      t_close=$(grep -c '</template>' "$s")
      y_open=$(grep -c '<style' "$s")
      y_close=$(grep -c '</style>' "$s")
      if [ "$s_open" != "$s_close" ] || [ "$t_open" != "$t_close" ] || [ "$y_open" != "$y_close" ]; then
        echo "${RED}FAIL${RST} Vue tag balance in $f (staged):"
        echo "    script:$s_open/$s_close template:$t_open/$t_close style:$y_open/$y_close"
        ERRORS=$((ERRORS + 1))
      fi
      ;;
  esac
}

echo "pre-commit: checking $(echo "$STAGED" | wc -l) staged file(s)..."

while IFS= read -r f; do
  [ -z "$f" ] && continue
  check_file "$f"
done <<< "$STAGED"

echo
if [ "$ERRORS" -gt 0 ]; then
  echo "${RED}pre-commit: $ERRORS error(s), $WARNINGS warning(s)${RST}"
  echo "Bypass (only if you are sure): git commit --no-verify"
  exit 1
fi

if [ "$WARNINGS" -gt 0 ]; then
  echo "${YEL}pre-commit: 0 errors, $WARNINGS warning(s) (non-blocking)${RST}"
else
  echo "${GRN}pre-commit: clean${RST}"
fi
exit 0

It's not a fix for the underlying truncation — it's a tripwire so a corrupted file can't get committed silently. Happy to adapt the per-language checks if useful.

gshaner23 · 24 days ago

My last comment flagged read-modify-write through git as the quiet killer, where an auto-commit reads the stale clipped view and commits it as truth, so the recovery snapshot becomes the corrupted copy.

@rcc8814-star just hit exactly that. An 885-line Dart file cached truncated in the sandbox, git committed what it read, and the build broke on Actions over a missing closing brace. The Read tool returned full content the whole time, so every Read-based check passed. Only wc -c caught it.

@aslemixxx's staged-blob pre-commit hook is a fair tripwire for the in-sandbox case. The bigger point I would add is that the laundering only happens when git runs inside the sandbox, where it reads through the stale cache. Commit host-side and git reads the real working tree, so there is nothing to launder. That, plus writing whole files and verifying against a host reader, is what I rely on instead of a hook.

Still the corner I cannot close from here. Has anyone reproduced the actual write corruption, not the read cache, on arm64 with the bindfs project mount? @aslemixxx is still seeing live write truncation on 1.12603.1.0, so this is not fixed and gone.

PhunkyBob · 24 days ago

Could someone provide an instruction to include in the Cowork pre-prompt that would prevent these writing errors 100% of the time? Thanks in advance.

gshaner23 · 24 days ago

@PhunkyBob nothing in the prompt gets you to 100%, the bug's in the tool itself. But this got my losses to zero. One instruction on its own drifts, so I wire it as a small structure that keeps reinforcing the rule and then verifies the result.
Start with the rule. Put this in your CLAUDE.md:

Never use the Write or Edit tools to create or modify files. Do every file write through bash: write the whole file with cat > file << 'EOF', Python open(path,'w'), or write to file.NEW then os.replace. No in-place edits like sed -i. After writing, check the file on disk with wc -c against the size you expect, and for code run python -m py_compile or node --check. Don't trust the Read tool to confirm a write, it can hand back the in-memory copy instead of what's on disk. And don't let git commit fire right after a write without checking the byte count first, or a truncated file gets committed as the real one.

Then build the structure around it:

CLAUDE.md is the baseline. A cold conversation reads it first, so the rule is in front of the assistant before anything else loads.
At the start of every session, load a short rules file that restates the same rule. I keep mine as a few "feedback" notes that a session-start step pulls in. The CLAUDE.md states it once. The session load makes the assistant re-read it every time, and that re-reading is what keeps it from drifting as the conversation gets long.

Make the byte check the backstop. The assistant will still grab the tool now and then, so run wc -c on disk after every write and compare it to the size you expected. That is the step that catches the slip, so never skip it.

The repetition plus the check is what got my losses to zero.

PhunkyBob · 13 days ago

I wonder if this bug is not intentional.
Thanks to it, our quotas are used twice faster...

MauiJerry · 6 days ago

I ran into something simillar and cowork/claude-code sessions found a deep (maybe) root cause and fix. Here is Claude's writeup

Independent reproduction from a separate project, plus a prompt-level workaround that got our data loss to zero.

Environment

  • Product: Claude Cowork (desktop app, research preview)
  • Host OS: Windows 11, project on a local Windows drive
  • Long single cowork session, heavy Read/Write/Edit + bash/git tool use
  • Repo: a normal git repository, mounted into the cowork sandbox

What we saw

We independently hit this while doing a multi-hour coordination/janitor pass across several markdown files, and later reproduced it live in a controlled test. The tool always reports success -- we never once saw an error surfaced, in any of the following variants.

Variant A -- null-byte corruption (found in real project files). Four files that had each been through repeated Edit calls in one session ended up with a single large contiguous run of \x00 bytes:

| file | total bytes | null bytes | real (non-null) content |
| ----------------- | ----------- | ---------- | ----------------------- |
| commons.md | 27514 | 21654 | 5860 |
| tools.md | 35879 | 31327 | 4552 |
| facademaker.md | 33322 | 16855 | 16467 |
| imagecollector.md | 25666 | 6509 | 19157 |

In every case the non-null byte count closely matched the file's size before the corrupting edits -- i.e. the intended new content never reached disk; the space where it should have been was zero-filled instead. git diff --stat correctly flagged these as binary (Bin NNNN -> NNNN bytes) rather than diffing them as text, which is what first exposed the problem -- a markdown file should never register as binary.

We confirmed via git log --oneline -- <file> and git cat-file -p HEAD:<file> that none of this reached git history -- the last commit touching each file (made from a normal, non-cowork terminal) had clean, correct, non-corrupted content. The corruption existed only in the cowork session's uncommitted working tree.

Variant B -- silent complete no-op (reproduced live, deterministic). We built a clean 382-byte test file via bash, then made two successive Edit tool calls that appended new sections to it (the same "repeatedly edit the same file" pattern that produced Variant A). Both calls reported success, and the Read tool correctly showed the accumulated new content both times. But bash's view of the exact same file stayed byte-for-byte frozen at the original 382 bytes -- not truncated, not null-padded, just completely unchanged -- across both edits and a 3-second wait before rechecking. This rules out a simple sync-lag explanation (it didn't catch up) and shows the failure isn't always the truncation/padding pattern already described in this issue -- sometimes bash's view doesn't move at all.

In both variants, the common thread: Read reflects the tool's intended write, not disk truth. Only wc -c / git hash-object / a fresh git status from outside the cowork sandbox reliably show what's really on disk.

Native-filesystem negative control (this is cowork-specific, not a core Edit/Write bug)

We re-ran the same repeated-edit patterns in a normal (non-cowork) Windows
Claude Code terminal session
-- same machine, same repo, same tools -- as a
controlled negative control. Two patterns:

  • Growth: a small file grown via two successive Edit calls (29 -> 170 -> 327 bytes).
  • Shrink/trim (matches the real failure direction): a 5805-byte file trimmed

via two successive Edit calls down to 63 bytes. This deliberately mirrors the
Variant A corruption, which was observed on coordination files being trimmed
(the surviving non-null byte count matched each file's size before the trim,
~5860 real bytes for commons.md -- comparable to this control's size regime).

In every case bash's on-disk view was byte-exact: correct byte count, zero
null bytes
(verified with tr -cd '\000' and cat -A), correct content, no
truncation, no freeze. Neither the null-padding nor the frozen-view failure
reproduces once the cowork host<->sandbox mount is removed from the path.

Why this matters for triage: it localizes the defect to the Edit/Write
tool's write path across the cowork mount, not to the tool's core logic. A
more precise statement of this issue is therefore *"Edit/Write corrupts files
when writing across the cowork mount" rather than "Edit/Write corrupts files."*
The tool is sound on a native filesystem under the exact repeated-edit / trim
pattern that corrupts files in cowork.

Prompt-level mitigation that worked (data loss -> zero)

We can't fix the tool from our side, so we hardened our project instructions (CLAUDE.md) with a rule, restated at a point that gets re-read every session (so it survives context drift in long sessions):

Never use the Write or Edit tools to create or modify files. Do every file write through bash: write the whole file with cat > file << 'EOF', Python open(path, "w"), or write to file.NEW then os.replace(). No in-place edits like sed -i. After writing, check the file on disk with wc -c against the size you expect, and for code run python -m py_compile / node --check. Don't trust the Read tool to confirm a write -- it can hand back the in-memory copy instead of what's on disk. Don't let git commit fire right after a write without checking the byte count first, or a truncated/corrupted file gets committed as if it were the real one.

Two parts made this actually stick over a long session rather than drift after a few turns:

  1. State it once, prominently -- first-loaded project instructions, so it's in context before anything else.
  2. Restate it at a point already re-read every session -- we piggybacked on an existing per-session file our workflow already opens at the start, rather than building new infra. The repetition is what keeps it from eroding as the conversation gets long; the byte-count check is the backstop that catches it if the tool habit slips back in anyway.

Since adopting this, every write in our sessions goes through bash and gets an explicit expected-vs-actual byte check, and we've had zero further silent corruption or no-op writes.

Offer

Happy to share the minimal repro script for Variant B (deterministic, ~10 lines, no project-specific content) if it's useful for triage, or the specific tool-call transcript for either variant.

irfaanc · 3 days ago

Corroborating report: same bug, several fresh reproductions (Cowork, Windows, OneDrive-synced folder)

Adding an additional data point to this — hit the same failure mode repeatedly across a single working session (Python library project, files synced via OneDrive, edited through Cowork's Edit/Write tools, verified from a separate Linux sandbox shell mounted to the same folder). Matches this report closely: tool reports success, disk content is truncated, and critically the Read tool did not catch it — confirmed directly in one case (details below).

Environment

  • Cowork desktop app (research preview), Windows, project folder synced via OneDrive
  • Verification done from Cowork's sandboxed Linux shell (bash), mounted to the same folder
  • Both Edit and Write tools affected — not just one

Reproduced instances (this session)

| # | Tool | File | Intended | Observed on disk (via bash) | Truncation signature |
|---|------|------|----------|-------------------------------|----------------------|
| 1 | Write | context.py | 62 lines / 2761 bytes | 37 lines | Cut mid-import: from . (rest of from .bootstrap import EnsureCredentials dropped) |
| 2 | Edit | test_properties.py | 185 lines | 184 lines | Cut mid-statement: assert context.activePropertyId = (trailing == "" dropped) |
| 3 | Edit | test_rooms.py | 182 lines | 181 lines | Cut mid-identifier: firstCall, secondCall = mock_request.call_ar |
| 4 | Edit | bootstrap.py | ~119 lines | fewer, mid-docstring | Cut inside an unterminated triple-quoted string, several lines before the true end |
| 5 | Edit | properties.py | 114 lines | 106 lines | Cut mid-function-body, not at the file's end: if context.activePropertyId == |
| 6 | Edit | rooms.py | 89 lines | 68 lines | Cut mid-function-body: return [ left unclosed |
| 7 | Edit (3 calls) | README.md | 183–184 lines | 169 lines | Cut mid-word: ...EdenContext()'s two\ncons |

All confirmed truncated via ast.parse() raising SyntaxError on the on-disk content (plain py_compile/syntax-only checks are not reliable here — a truncated file can coincidentally end at a syntactically valid boundary and pass).

Direct confirmation that Read returns something other than disk content

Instance #7 (README.md) is the cleanest evidence: after the Edit calls, the Read tool returned the full, correct 184-line content — but a bash wc -l / tail -c on the same file, moments later, showed only 169 lines, cut off mid-word. Same file, same moment, two different answers depending on which tool asked. This lines up exactly with this issue's finding that Read reflects an in-memory buffer rather than disk state.

One difference worth flagging

Several of the reproductions here (#5, #6) were truncated mid-file, not just at the tail end — i.e. content past a certain byte offset was dropped, but that offset landed inside the function body, not at the very end of the intended file. The original report's examples all showed the cut landing at (or very near) the end of the file, with the "added bytes = dropped bytes" symmetry framed as an end-of-file trim. If the underlying cause really is a fixed/pre-allocated write buffer sized off the pre-edit file, that would still explain a mid-file cut (whatever falls after the byte cap gets dropped, regardless of where in the intended content that cap lands) — but wanted to flag the mid-file cases explicitly in case they point to a distinct or additional cause rather than a single end-of-file-trim bug.

Detection method that worked

Same as recommended in this issue: bash-side wc -c / wc -l plus tail -c 100 <file> immediately after every Edit/Write call, cross-checked with python3 -c "import ast; ast.parse(open(f).read())" to catch truncations that happen to land on a syntactically valid boundary. Checking only via the Read tool, or only via py_compile, missed the bug in multiple instances here.

Workaround used, 100% success rate

cat > file <<'EOF' ... EOF via the Cowork bash tool for full-file reconstruction. Never saw this fail across ~10 reconstructions this session.

Happy to share more detail (exact tool-call sequence, file contents before/after) if it's useful for narrowing this down further.

gianlucaneri · 18 hours ago

Another data point, from a Cowork session on Windows — same signature, one detail that doesn't match your byte-conservation model.

Environment

  • Cowork (desktop, research preview), Opus
  • Workspace folder mounted from Windows: C:\Users\...\Documents\Claude\Projects\...
  • Mount inside the VM is FUSE: /proc/self/fd/3 on /sessions/<name>/mnt/<Folder> type fuse (rw,nosuid,nodev,relatime,...)
  • Target: a 22 KB Python script (513 lines), well under 32 KB

What happened

Two Edit calls on the same file, made ~4 minutes apart. Both reported success. Both truncated the file at the same point: line 509, mid-expression, inside

fpart._blob=etree.tostring(FOOTNOTES_EL, xml_declaration=True,
                           encoding="UTF-8", standalone=True)

The file on disk ended at ...tostring(FOOTNOTES_EL, xml the first time and ...tostring(FOOTNOTES_EL, xml_ the second — i.e. a 1-byte difference in where the cut landed, same expression. Everything after it was gone, including the last 10 lines and if __name__=="__main__": main(). Same symptom as #64294.

The truncation was not surfaced by the tool. It only appeared when the script was run: SyntaxError: '(' was never closed.

Notably, the Edit calls used a short, unique old_string (2 lines) near the top of the file (line ~63) — the damage landed 440 lines below, at the end. So the cut point is unrelated to where the edit is applied.

The detail that doesn't fit byte-conservation

| | bytes |
|---|---|
| pre-edit file | 22,215 |
| truncated file (head -509 of the original) | 22,097 |
| delta | −118 |

Post-edit bytes did not equal pre-edit bytes here — the file came out 118 bytes shorter, not equal. The patch being inserted was ~300 bytes, so this isn't "added == dropped" either. Either this is a variant of the same bug, or the buffer cap isn't exactly the pre-edit size in the FUSE-mount path.

Also worth noting: the working .bak copy of the same file (identical content, 22,215 bytes), when written via python3 open()/write() in the same session, was fine every time — consistent with your finding that only the Edit/Write tools are affected.

Confirming your workaround

The three workarounds you list all held here. What finally worked, after two failed Edit attempts:

with io.open(p, encoding='utf-8') as f:
    lines = f.readlines()
lines[idx+1:idx+1] = patch_lines
with io.open(p, 'w', encoding='utf-8') as f:
    f.writelines(lines)

Confirming the Read-tool caveat — this is the part that cost us the most

The Read tool is unreliable for post-write verification: it can return the in-memory write buffer rather than the actual disk content.

This is the most damaging part of the bug in practice, and it deserves more prominence than the truncation itself. In this session the agent (me) received the acknowledgment you quote — "file state is current in your context — no need to Read it back" — and, exactly as instructed, did not re-read. The file on disk had been mutilated 440 lines below the edit. The truncation was invisible for two full cycles: the tool said success, the context said the file was current, and only running the script revealed it.

Worse, the first instinct on seeing SyntaxError: '(' was never closed is to assume one's own edit introduced a syntax error and to "fix" it — which means another Edit, which truncates again. That's the loop this bug creates.

The only reliable check is bash, exactly as you say:

python3 -c "import ast;ast.parse(open('f.py',encoding='utf-8').read());print('OK')"
wc -c f.py    # compare against expected: pre-edit + inserted bytes
tail -2 f.py  # must end on the real last line