[BUG] Cowork: skill upload truncates files >99 KB and leaves null-byte tail on re-upload

Status Fixed / completed
Maintainer reply None cached
Activity 8 comments · opened Apr 21, 2026 · closed Aug 25, 2026

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?

Two distinct bugs in the Cowork desktop app's skill upload pipeline (Customize → Skills tab), both causing silent data corruption:

Bug #1: Files >99 KB in a skill bundle are silently truncated to exactly 98,902 bytes during install. For Python files this produces a SyntaxError at import time.

Bug #2: When a file is re-uploaded (after uninstall + reinstall), the pipeline writes new content into the existing on-disk byte stream without truncating. If the new file is smaller than the old one, the tail of the old content remains as null bytes, producing "ValueError: source code string cannot contain null bytes" when Python tries to import.

Both bugs no longer reproduce as of the 2026-04-21 Cowork desktop app update. Filing for the historical record and to verify the fix was intentional. Reproduction details below.

What Should Happen?

Either:

  1. The full file is installed byte-for-byte, or
  2. The upload is rejected with a clear error message identifying which file(s) exceeded the cap.

On re-upload, the target file should be truncated to the new content length before writing, so no stale bytes remain.

Silent truncation or dirty overwrite is the worst of both worlds — the skill appears to install successfully but is broken.

Error Messages/Logs

No errors surfaced in the Cowork UI. Failures only visible via shell:

$ wc -c invoice_extractor.py
98902 invoice_extractor.py          # file truncated to this exact size

$ python3 -c "import ast; ast.parse(open('invoice_extractor.py','rb').read())"
ValueError: source code string cannot contain null bytes   # bug #2

Steps to Reproduce

Reproduction — Bug #1 (size-cap truncation)

  1. Prepare a skill bundle containing a single Python file larger than ~100 KB.

The file used here was invoice_extractor.py, 2,310 lines, original size greater than 98,902 bytes.

  1. Upload the bundle via Customize → Skills.
  2. In a Cowork shell, run wc -l and wc -c on the installed file and compare to the source.

Observed

The installed file was truncated to exactly 98,902 bytes (2,174 lines). The cut landed mid-statement — the last bytes of the installed file were:

                        row[f"_{fn}_source"] = "tax_group_split"
                        row[f"_{fn}_bbox"] = [0, 0, 0, 0]
                        row[f"_{fn

No closing brace, quote, or newline. No warning or error was shown in the Cowork UI.

Evidence this was a deterministic size cap, not a flaky transfer

The skill was uninstalled and re-uploaded. Both installs produced byte-for-byte identical truncation:

| Check | Upload #1 | Upload #2 |
|---|---|---|
| Installed file size | 98,902 bytes | 98,902 bytes |
| Line count (wc -l) | 2,174 | 2,174 |
| Last bytes of file | row[f"_{fn | row[f"_{fn |

Two independent uploads landing on the same byte boundary rules out network flakiness. The cap lived somewhere in the upload, extraction, or write path.

Reproduction — Bug #2 (dirty overwrite, null-byte tail)

  1. Install a skill containing a file that got truncated by Bug #1 (file on disk is 98,902 bytes).
  2. Uninstall the skill via the Customize tab. Verify the skill directory is removed from disk.
  3. Re-upload a new bundle where the same-named file is now smaller than 98,902 bytes.

Observed

The freshly-uploaded file on disk was still 98,902 bytes, not the expected smaller size. The file's mtime remained pinned to the original install time (14:09), not the new install time (17:38), even though other files in the bundle got correct new mtimes.

Byte-level inspection showed:

total size:                 98,902 bytes
null bytes (0x00):          80,627
real content ends at byte:  18,273

The first 18,274 bytes were my new file content (complete and correct). Bytes 18,274 through 98,901 were \0 — the tail of the previous truncated install, never cleared.

Python rejected the file at parse time:

ValueError: source code string cannot contain null bytes

Evidence the write path skipped O_TRUNC or equivalent

Three signals pointed to a non-truncating write rather than any data corruption:

  1. The file size was preserved at exactly the previous truncated size (98,902), not rounded to a new boundary.
  2. The file's mtime was preserved from the previous install, not updated to the current time (while other files in the same upload had fresh mtimes).
  3. The real content stopped cleanly at a valid newline at byte 18,274, matching the exact length of the new source file. Only the tail was stale.

This is consistent with 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.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

N/A — Cowork desktop app issue, not Claude Code CLI

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Other

Additional Information

Related prior reports: #38993 (open) and #40264 (closed as invalid).
Neither is a duplicate — #38993 is host→VM FUSE staleness on user-mounted folders, #40264 is VM-written file truncation on subsequent reads. Both share FUSE metadata caching as the probable root cause with this report.

[BUG] Cowork skill upload truncates files >99 KB and leaves null-byte tail on re-upload

Summary

Two distinct bugs in the Cowork desktop app's skill upload pipeline (Customize → Skills tab). Both caused silent data corruption of installed skill files.

Bug #1 — silent truncation at ~99 KB cap: Any file in a skill bundle whose size exceeds roughly 99 KB is silently truncated to exactly 98,902 bytes during install. For Python files this produces a SyntaxError at import time, rendering the skill unusable.

Bug #2 — dirty overwrite on re-upload: When a file is re-uploaded through an uninstall + re-install cycle, the pipeline writes the new content into the existing on-disk byte stream without truncating the file to the new length. If the new file is smaller than the old one, the tail of the old content remains as null bytes (\0), producing ValueError: source code string cannot contain null bytes when Python tries to import the file.

Status: Both bugs appear to be fixed as of the Cowork update released on 2026-04-21 morning (UTC+9). A clean re-upload after that update installed cleanly, with correct file sizes, fresh mtimes, and zero null bytes. This issue is filed for the historical record, in case the regression recurs or in case the fix needs to be verified on other platforms.

Environment

  • Product: Cowork (desktop app), skill upload via the Customize → Skills tab
  • OS: Windows 11
  • Dates observed: 2026-04-20 (both bugs reproduced); resolved 2026-04-21
  • Skill installation path (in Cowork sandbox): /sessions/<session-id>/mnt/.claude/skills/<skill-name>/

Reproduction — Bug #1 (size-cap truncation)

  1. Prepare a skill bundle containing a single Python file larger than ~100 KB.

The file used here was invoice_extractor.py, 2,310 lines, original size greater than 98,902 bytes.

  1. Upload the bundle via Customize → Skills.
  2. In a Cowork shell, run wc -l and wc -c on the installed file and compare to the source.

Observed

The installed file was truncated to exactly 98,902 bytes (2,174 lines). The cut landed mid-statement — the last bytes of the installed file were:

                        row[f"_{fn}_source"] = "tax_group_split"
                        row[f"_{fn}_bbox"] = [0, 0, 0, 0]
                        row[f"_{fn

No closing brace, quote, or newline. No warning or error was shown in the Cowork UI.

Evidence this was a deterministic size cap, not a flaky transfer

The skill was uninstalled and re-uploaded. Both installs produced byte-for-byte identical truncation:

| Check | Upload #1 | Upload #2 |
|---|---|---|
| Installed file size | 98,902 bytes | 98,902 bytes |
| Line count (wc -l) | 2,174 | 2,174 |
| Last bytes of file | row[f"_{fn | row[f"_{fn |

Two independent uploads landing on the same byte boundary rules out network flakiness. The cap lived somewhere in the upload, extraction, or write path.

Reproduction — Bug #2 (dirty overwrite, null-byte tail)

  1. Install a skill containing a file that got truncated by Bug #1 (file on disk is 98,902 bytes).
  2. Uninstall the skill via the Customize tab. Verify the skill directory is removed from disk.
  3. Re-upload a new bundle where the same-named file is now smaller than 98,902 bytes.

Observed

The freshly-uploaded file on disk was still 98,902 bytes, not the expected smaller size. The file's mtime remained pinned to the original install time (14:09), not the new install time (17:38), even though other files in the bundle got correct new mtimes.

Byte-level inspection showed:

total size:                 98,902 bytes
null bytes (0x00):          80,627
real content ends at byte:  18,273

The first 18,274 bytes were my new file content (complete and correct). Bytes 18,274 through 98,901 were \0 — the tail of the previous truncated install, never cleared.

Python rejected the file at parse time:

ValueError: source code string cannot contain null bytes

Evidence the write path skipped O_TRUNC or equivalent

Three signals pointed to a non-truncating write rather than any data corruption:

  1. The file size was preserved at exactly the previous truncated size (98,902), not rounded to a new boundary.
  2. The file's mtime was preserved from the previous install, not updated to the current time (while other files in the same upload had fresh mtimes).
  3. The real content stopped cleanly at a valid newline at byte 18,274, matching the exact length of the new source file. Only the tail was stale.

This is consistent with 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.

Workaround used at the time

Split the affected Python file into ten smaller modules, none exceeding ~28 KB, so that no individual file approached the 99 KB cap. This worked around Bug #1 but still hit Bug #2 when re-uploading into a directory that previously held a file of the same name at the old truncated size.

The reliable fix during 2026-04-20 was:

  1. Uninstall the skill
  2. Fully quit and restart the Cowork desktop app
  3. Re-upload the bundle with a renamed entry-point file (so the filename did not collide with any cached blob)

After the 2026-04-21 update, a direct re-upload of the original (non-renamed) bundle installed cleanly on the first try.

Impact

Both bugs produced silent data loss visible only via shell inspection. Users without shell access would see a skill that installed "successfully" but failed at first invocation with no obvious cause. Diagnosing the underlying issue required wc -c, wc -l, and od -c / byte-level Python to distinguish the two modes of corruption.

For Python skills the failure mode was always a SyntaxError or ValueError at import. For non-Python skill files (Markdown, JSON, text assets) the corruption could have gone unnoticed indefinitely, depending on how the skill consumed those files.

Suggested improvements (if the fix regresses)

  1. If a size cap is required, validate file sizes at upload time and reject the bundle with a clear UI error listing the oversize files. Silent truncation is never the right answer.
  2. When writing to the on-disk skill directory, either open files with O_TRUNC or call ftruncate() after the final write so file length matches content length.
  3. Surface the install receipt (list of files written, with sizes) in the Customize tab so users can sanity-check a fresh install without shell access.

Reporter

Cowork user — reproduction performed inside a remote Cowork session on 2026-04-20. Assisted in diagnosis by Claude.

View original on GitHub ↗

8 Comments

github-actions[bot] · 4 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/47016
  2. https://github.com/anthropics/claude-code/issues/40231
  3. https://github.com/anthropics/claude-code/issues/46844

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

0xbrainkid · 4 months ago

This is a serious integrity bug because the upload pipeline does not merely fail, it pretends to succeed while corrupting bytes. Silent truncation and dirty overwrite are among the worst failure modes for skill/package workflows because users only discover the breakage later at import/runtime.

The fact that there are two distinct corruption paths is especially important:

  • large files are clipped to a fixed byte threshold
  • smaller replacement files can inherit stale tail bytes from prior content

That means the system can break both first installs and reinstalls in different ways while preserving the illusion that installation worked.

Even if the bug is now fixed, documenting it is valuable because this class of issue undermines trust in the package/install layer itself. Byte-for-byte integrity is the contract for upload/install pipelines.

wnunezc · 4 months ago

The problem isn't whether it's a bug or not; the problem is wasting an entire week's worth of tokens just resolving problems associated with or caused by the agent or the model... Can I request a reset of my weekly quota?

funkymonkeyjam · 3 months ago

Hit this today on Windows. Posting in case it's useful, because I think Bug 2 has regressed since the April 21 fix.
I reinstalled two skills via the Cowork desktop app this afternoon (Version 1.9255.2, build 1dc8f7). For each one I uninstalled the prior version through the Customize UI, restarted the app, then installed the new .skill archive. Got byte-identical broken installs on both attempts for both skills.

Six files came out wrong. Three were smaller than what was already installed, and all three ended up at the prior file's size with NULL bytes padding the tail: 189 nulls on a Python file, 79 on a Markdown reference, 8 on a JavaScript file. The Python file blew up on import with ValueError: source code string cannot contain null bytes. That's exactly the Bug 2 signature.

The other three files were larger than the prior install, and those got truncated to the prior install's size with the new content past that byte boundary silently dropped. Two Markdown files and a JSON file. The JSON ended up unparseable because the cut landed mid-string. The SKILL.md only worked because the truncation point happened to fall past the instructional content. That half matches #40231, which is still open.

None of the affected files were anywhere near 99 KB, so the size-cap half of the original report (Bug 1) didn't fire. Just the dirty-overwrite half.

The hypothesis from the original report still holds up here. Post-install file size equals the prior install's byte length exactly, new content sits at the head, tail is \0 padding. Looks like a write that didn't truncate the existing file or call ftruncate() after writing shorter content.

Workaround for now: I just load the affected skills from a separate source tree instead of the user-level install. That sidesteps the broken copy entirely.``

wnunezc · 3 months ago

@funkymonkeyjam

In my case, I got fed up with this situation and I can't keep paying for something defective that, to top it all off, ruins my projects. I switched to another interface and another model. Thank God, for the moment I don't have any problems, but these people aren't going anywhere...

HollowMisty · 2 months ago

This isnt stale, its crazy it hasnt been fixed.

wnunezc · 2 months ago

This is theft and they don't realize it. If the content exceeds the limit, you have to request it again in parts. And if the content doesn't even fill the entire package, they add junk. And that's where your tokens went. That's why the number of messages is so-so every X amount of time.

Satal · 1 month ago

I don't suppose anyone has found any good workarounds for this? I've got Claude to add instructions to the CLAUDE.md to check for this, but having to spend more tokens/time on checking this bug hasn't happened isn't optimal or appropriate.