[BUG] Cowork Edit/Write tools silently truncate files via byte-conservation buffer cap (deterministic, fires at all file sizes)
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.
- On-disk write corruption. Intended content never reaches disk. Observed variants: clipped at an offset, zero-filled (a contiguous run of
\x00where 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 bygit diff --statreporting a markdown file as binary. - 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, soMoveFileExW(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', Pythonopen(path, 'w'), or writefile.NEWthenos.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 commitfire 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:
- 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:
- Verify pre-edit state via bash:
wc -c file.md → 139 bytes
wc -l file.md → 5 lines
- In Cowork, Read the file via the Read tool.
- 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."
- Edit tool reports success.
- 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
- First — add an error path. If the tool can't write the full intended content, surface an error instead of silently truncating.
- Second — investigate the buffer-cap mechanism. The conservation symmetry suggests a fixed-size buffer that should be dynamic.
- 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/.gitdocumenting 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.
43 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
[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 -cbash: would have shown post-edit bytes = pre-edit bytes (~486), confirming the cap.Additional confirmation of #53940 findings:
open()/write()/close()via bash, 100% success rateSuggested addition to #53940: The Read-tool lie can be unmasked by any external tool that reads from disk — not only
wc -cbut also interpreters and linters. This provides an additional detection path when byte counts are not tracked."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.
+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>.NEWthen callingos.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_sizeat open and clips overflow rather than extending. Same shape #51435 inferred from the skill-upload pipeline missingO_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 pushstraight 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.
+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 -cafter 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 ||
.NEWsibling +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. Pythonopen('w')works because the'w'flag triggersO_TRUNC, the file goes to zero length first, and there's no inherited cap to clip against. Jeff'sos.replace()pattern works for the same reason the.NEWfile is a fresh inode with no priorst_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 -cafter 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):
os.replace()for anything where atomicity matters (data files getting written while something else might read them)open('w')via bash subprocess for full-file rewritesbashheredoc (cat > file << 'EOF') when we're already in shell anywayWhat 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.
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: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
\x00padding 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 byMove-Item -Force tmp dest) on a fresh file in the same workspace folder:| Scenario |
CoworkVMServicerunning | 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
statthat captures thePreserving file permissions: 100666value 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. Whenfs.renamethen callsMoveFileExW(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:
Suggested fix
Preserving file permissions: 100666log line strongly suggests the SDK opened or stat'd the destination just before writing the temp; that's the prime suspect.fs.renamewith the Win32ReplaceFileWAPI directly for the replace step.ReplaceFileWis designed to atomically replace a file even when the destination is open in other handles, whichMoveFileExW(REPLACE_EXISTING)is not.statthe 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.Happy to share the full investigation log (with VM debug logs, SDK debug log, mount tables, isolation tests) on request.
+1 reproducing on Windows 11, Cowork desktop (Max plan).
Environment:
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 -creveals 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.
Support ticket regarding this bug filed 2026-05-13, no response in
3 days. Refund request pending without acknowledgment.
Escalation forthcoming.
Adding additional context:
Environment for the record: Windows 11 host, virtiofs mount of Documents/ into Ubuntu 22.04 container.
@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.
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)
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.
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.
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 -cagainst 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.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!!!
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.
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 -candtail.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 80with no94) ||
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
Writecreating new files, not justEditon 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.j2ended at byte 1225 with bytese2 80(the first two bytes of an em-dash, U+2014 =e2 80 94) with the third byte missing. Subsequent renders failed withUnicodeDecodeError: '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 -cwc -cafter 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:
Workaround confirmed
mcp__workspace__bashheredoc (cat > file << 'EOF' ... EOF) writes straight to disk via the sandbox mount and persisted fullcontent 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 -cshowed 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
C:\Users\<user>\Documents\(not OneDrive-backed)
@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.
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.
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:
bashheredoc andpython open('w')via the sandbox shell. We no longer touch the Edit/Write tools on any file we want to keep intact.@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.
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).
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 PowerShellGet-Itemas 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, butlsmodin the sandbox shows9p/9pnetloaded and novirtiofsmodule at all, same as @sk-j-hiros found..virtiofs-rootis 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 whydrop_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.
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: ased -ithat 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 initleft me with a.git/configthat 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.@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.
@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.txtfile is back, but file-tool writes don't produce theWriting to temp file:/Temp file written successfullylines 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.
@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?
@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.
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
cc_version=2.1.165.da6(from the billing header inoutputs/sdk-debug.txt- same SDK family @msull00 tested). x64. Windows. Vault on local NTFS, not OneDrive.lsmodshows9p/9pnet/9pnet_fd, novirtiofsmodule - confirms @msull00; the.virtiofs-rootname is a red herring. The user mounts are stacked FUSE over that 9p root: the workspace folder isfuse.bindfs,outputs/uploadsare a plainfusepassthrough. Two attr-caches sit on the read path.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:bash→ 2566, appended bytes invisible - STALE, pinned at the cachedst_sizeNothing in-session refreshed the sandbox view:
mv f f.x && cat f.x,cp,dd,sync, a freshopen()- 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_sizepinned on inode16325548…), and rename preserves the inode. Proof: a brand-new host file (Copy-Itemto a new name → new inode16607023…) 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 filelines disappeared on 2.1.165. On my build they're present - but only host-side. From the sandbox,sdk-debug.txthad frozen:bashreportedwc -l = 566, last entry06: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 through07: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: NreportsNas 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
git commit,sed -i,sort -o, an auto-commit, evencp- 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.bashread, 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.txtslices) on request.I face this issue a dozen times a day.
Is there a chance someone at Anthropic would take a look at this bug?
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 -cview | 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:wc -c/statin bash reads through the same stale cache and reports the old size, which is the false truncation signal.Full diagnosis and fix recommendations (close-to-open consistency / active invalidation) posted on #38993. Caveat on scope: three small files on the virtio-fs
outputsmount, 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.
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):
.txtconfig consumed by a downstream parser. So it is not language-specific.py_compile/node -cor 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:
Since switching to write-temp-then-
os.replacefor 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.
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.
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.
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.
+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:
Claude_1.12603.1.0_x64__pzs8sxrjxfjjc(MS Store)C:\rep(not OneDrive)claude.exeprocesses + 1cowork-svcPID without public PathGet-Process)C:\via exclusion (so no AV in the write path)Symptoms observed over the past two weeks:
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 beif uid <= 0:). Read-tool returned the full 188-line buffer;bash wc -lon the mount returned 180. Lost the trailing block including areturn jsonify(...)and the# eofmarker.bot/services/access.py(~630 lines / ~25 KB) lost the entireRoleMiddleware.__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 stayedactive+pollingbut every middleware-mediated handler stopped working — silent prod outage untilpython3 -m py_compile+grepexposed the missing function.Detection method that worked:
wc -l+tail -3viamcp__workspace__bashafter every Edit-affected file.git diffandgit statusalso 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)viamcp__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.Backstop in the repo: a
.githooks/pre-commitrunsbash -n/python3 -m py_compile/ Vue tag balance / NUL-scan on staged files viagit diff --cached --name-only. Catches the corruption before it can leave the workstation in agit push. NUL-scan uses Pythonb'\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."
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 -cfrom bash exposed the truth — the byte count didn't match what the Write tool reported.Detection method that worked: run
wc -c /path/to/fileimmediately after Write and compare against expected byte count.wc -lalone 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 customGIT_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.
@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(orgit show :file | wc -c) right after a Write, compared against the expected size.wc -lis not enough — a cut mid-line can still leave a plausible line count.2. A
core.hooksPathpre-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 againstgit show :file. It doesbash -nfor shell,py_compilefor Python, a Vuescript/template/styletag-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 analyzeor a brace-balance count, but the staged-blob mechanism is the reusable part.)Hook script (
.githooks/pre-commit, enable withgit config --local core.hooksPath .githooks):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.
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.
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.
@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.
I wonder if this bug is not intentional.
Thanks to it, our quotas are used twice faster...
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
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
\x00bytes:| 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 --statcorrectly 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>andgit 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
Readtool 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:
Readreflects the tool's intended write, not disk truth. Onlywc -c/git hash-object/ a freshgit statusfrom 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:
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'andcat -A), correct content, notruncation, 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):Two parts made this actually stick over a long session rather than drift after a few turns:
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.
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
bash), mounted to the same folderReproduced 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 offrom .bootstrap import EnsureCredentialsdropped) || 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()raisingSyntaxErroron the on-disk content (plainpy_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 abash wc -l/tail -con 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-sidewc -c/wc -lplustail -c 100 <file>immediately after every Edit/Write call, cross-checked withpython3 -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 viapy_compile, missed the bug in multiple instances here.Workaround used, 100% success rate
cat > file <<'EOF' ... EOFvia 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.
Another data point, from a Cowork session on Windows — same signature, one detail that doesn't match your byte-conservation model.
Environment
C:\Users\...\Documents\Claude\Projects\.../proc/self/fd/3 on /sessions/<name>/mnt/<Folder> type fuse (rw,nosuid,nodev,relatime,...)What happened
Two
Editcalls on the same file, made ~4 minutes apart. Both reported success. Both truncated the file at the same point: line 509, mid-expression, insideThe file on disk ended at
...tostring(FOOTNOTES_EL, xmlthe 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 andif __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
Editcalls used a short, uniqueold_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 -509of 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
.bakcopy of the same file (identical content, 22,215 bytes), when written viapython3 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
Editattempts:Confirming the Read-tool caveat — this is the part that cost us the most
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 closedis 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: