Repository navigation
fix(cli): externalize/internalize corrupt binary files under .squad/ - #1492
Conversation
readSync/writeSync round-trips file contents through a UTF-8 string, so any byte >= 0x80 becomes U+FFFD. Because externalize deletes the source right after the copy, non-UTF-8 files are silently destroyed and internalize can't restore them. Copy bytes via storage.copySync at all three copy sites (externalize, internalize, copyDirRecursive) instead. Fixes github#1489
🟡 Impact Analysis — PR #1492Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (1 file)
squad-cli (1 file)
tests (1 file)
This report is generated automatically for every PR. See #733 for details. |
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| Status | Check | Details |
|---|---|---|
| ✅ | Single commit | 1 commit — clean history |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | No Copilot review threads |
| ❌ | CI passing | 4 check(s) still running |
Files Changed (3 files, +36 −13)
| File | +/− |
|---|---|
.changeset/fix-externalize-binary-corruption.md |
+5 −0 |
packages/squad-cli/src/cli/commands/externalize.ts |
+7 −13 |
test/external-state.test.ts |
+24 −0 |
Total: +36 −13
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
There was a problem hiding this comment.
Pull request overview
Fixes binary-file corruption during squad externalize / squad internalize by switching the copy mechanism from a UTF-8 string round-trip (readSync/writeSync) to a byte-accurate file copy (storage.copySync). This prevents irreversible data loss for non-UTF-8 files under .squad/ when externalize deletes the originals.
Changes:
- Replace
readSync/writeSync-based file copying in externalize/internalize andcopyDirRecursivewithstorage.copySyncto preserve bytes. - Add a regression test that round-trips a 0x00–0xFF binary buffer through externalize → internalize and asserts byte equality.
- Add a changeset for the CLI patch release noting the fix.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/external-state.test.ts | Adds a regression test ensuring non-UTF-8 binary files survive externalize/internalize byte-for-byte. |
| packages/squad-cli/src/cli/commands/externalize.ts | Switches file copy paths to storage.copySync to avoid UTF-8 decode/encode corruption. |
| .changeset/fix-externalize-binary-corruption.md | Declares a patch release and documents the binary corruption fix. |
|
Thanks for the thorough repro and the byte-for-byte fix. Confirmed the corruption on Non-blocking follow-up for whoever wires up |
tamirdresher
left a comment
There was a problem hiding this comment.
Externalize binary corruption fix looks correct: text-mode s.readFileSync + trim replaced with copyFileSync (byte-for-byte). Two-pass Buffer-based diff catches the same regression class if it ever comes back. CI (test suite, security review, architectural review, bootstrap protection, diff-size guard, samples build, sdk-exports validation) all green on head 1b667dc. Approving.
Fixes #1489.
squad externalize(andinternalize) copy every file under.squad/by reading it as a UTF-8 string and writing the string back out. Any byte >= 0x80 gets decoded to U+FFFD on the way in, so non-UTF-8 files come out corrupted — and since externalize deletes the source right after the copy, the only intact copy is gone. internalize copies back through the same lossy path, so the round trip never restores the original.Core state is markdown/JSON today so text-only projects don't hit this, but it bites any binary a user or agent keeps under
.squad/(diagrams/screenshots referenced from decisions, PDFs, archives), and it would destroy the whole state db the momentSqliteStorageProvider(.squad/squad.db) becomes reachable.The fix swaps the three
readSync/writeSynccopy sites — the externalize file branch, the internalize file branch, andcopyDirRecursive— forstorage.copySync, which iscopyFileSyncunderneath and already does the recursive mkdir for the destination. Byte-for-byte copy, no string decode.Test
Added a regression in
test/external-state.test.tsthat writes a file containing every byte value 0x00–0xFF (invalid UTF-8), runs externalize then internalize, and asserts the externalized copy and the restored copy are both byte-identical to the original. It fails onmain(Buffer.equalsis false — the 256-byte file comes back 512 bytes with U+FFFD) and passes with this change. Fulltest/external-state.test.tssuite is green (19 tests), andtsc --noEmiton both packages is clean.