[BUG] Cowork: skill upload truncates files >99 KB and leaves null-byte tail on re-upload
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:
- The full file is installed byte-for-byte, or
- 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)
- 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.
- Upload the bundle via Customize → Skills.
- In a Cowork shell, run
wc -landwc -con 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)
- Install a skill containing a file that got truncated by Bug #1 (file on disk is 98,902 bytes).
- Uninstall the skill via the Customize tab. Verify the skill directory is removed from disk.
- 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:
- The file size was preserved at exactly the previous truncated size (98,902), not rounded to a new boundary.
- 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).
- 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)
- 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.
- Upload the bundle via Customize → Skills.
- In a Cowork shell, run
wc -landwc -con 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)
- Install a skill containing a file that got truncated by Bug #1 (file on disk is 98,902 bytes).
- Uninstall the skill via the Customize tab. Verify the skill directory is removed from disk.
- 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:
- The file size was preserved at exactly the previous truncated size (98,902), not rounded to a new boundary.
- 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).
- 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:
- Uninstall the skill
- Fully quit and restart the Cowork desktop app
- 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)
- 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.
- When writing to the on-disk skill directory, either open files with
O_TRUNCor callftruncate()after the final write so file length matches content length. - 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.
This issue has 8 comments on GitHub. Read the full discussion on GitHub ↗