Skip to content

fix: guard against patching binary files - #9

Merged
paulpham157 merged 5 commits into
mainfrom
feat/add-binary-file-guard-with-clear-error
Sep 15, 2026
Merged

paulpham157 merged 5 commits into
mainfrom
feat/add-binary-file-guard-with-clear-error

Conversation

@paulpham157

@paulpham157 paulpham157 commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

Reject apply_patch updates to files containing a null byte before decoding or writing them. The failure is reported with EBINARY and includes the patched file path, preventing UTF-8 replacement-character corruption.

Regression coverage verifies structured EBINARY failures, byte-for-byte preservation, binary move rejection, binary deletion, and that pending/final tool results omit misleading text previews.

Before opening this PR

  • I read CONTRIBUTION.md and AGENTS.md in this repository.
  • I selected the required tests from CONTRIBUTION.md and read docs/live-testing.md.
  • I tested the final revision and kept this PR as draft for the missing required live/cross-platform coverage below.

Verification

  • npm run check (typecheck + biome)
  • npm test (127 tests)
  • npm pack --dry-run (release sanity)
  • git diff --check main...HEAD

Tested revision: 7143f90
Node / Pi versions: Node v26.8.1; Pi 0.85.1
Local OS: macOS (Darwin 25.6.0)

Live Pi results

Exact provider/model Create Read/update Direct move Read/delete Errors/recoveries
Not run Not run Not run Not run Not run Not run

Report or evidence: Local deterministic regression test covers null-byte rejection and exact byte preservation.
Transport evidence if applicable (grammar / JSON / unverified): Unverified.
Failures, missing coverage, or N/A reason: This is a filesystem/path safety change. Per CONTRIBUTION.md, required developer-run live CRUD and CI on Ubuntu, macOS, and Windows remain outstanding. This PR stays draft until the live tests and CI complete, or a maintainer records a waiver.

apply_patch impact

  • Tool schema / grammar unchanged.
  • Workspace path safety remains covered by deterministic tests.
  • User-visible behavior documented in this summary: binary update targets are rejected with EBINARY; no breaking migration.

🤖 Generated with Claude Code

Summary by Sourcery

Guard apply_patch against binary file updates while preserving safe deletion behavior and accurate tool results.

Bug Fixes:

  • Reject patch updates to files containing null bytes with a structured EBINARY error before decoding or writing, preventing binary data corruption.

Enhancements:

  • Preserve binary files byte-for-byte when update or move operations are rejected while allowing binary deletions to proceed.
  • Suppress misleading text previews for failed binary patch operations and include the affected path in the failure message.

Tests:

  • Add regression coverage for binary update, move, deletion, structured failures, byte preservation, and preview behavior.

paulpham157 and others added 4 commits September 15, 2026 14:43
semantic-release derives the published version from git tags at CI time,
so the committed version field was diverging (0.1.3 vs published 0.3.0).
Set it to 0.0.0 to signal it's a placeholder managed by release tooling.

Co-Authored-By: Claude Code <noreply@anthropic.com>
prepareOperation read files as UTF-8, decoding binary into replacement
characters so a patch could match garbage or silently corrupt the file on
write. Read as Buffer, reject on a null byte with an EBINARY code before
any write, then decode. Delete hunks never read content and are unaffected.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds an early null-byte guard for apply_patch updates and moves so binary files cannot be decoded or corrupted, while preserving deletion semantics and reporting structured EBINARY failures without misleading previews.

Sequence diagram for binary-safe apply_patch updates

sequenceDiagram
    participant ApplyPatch
    participant Preview as createPatchPreview
    participant Prepare as prepareOperation
    participant Guard as readTextFileRejectingBinary
    participant FS as FileSystem

    ApplyPatch->>Preview: createPatchPreview(cwd, hunks)
    Preview->>Guard: readTextFileRejectingBinary(absolutePath, filePath)
    Guard->>FS: readFile(absolutePath)
    alt null byte found
        Guard-->>Preview: Error with code EBINARY
        Preview-->>ApplyPatch: Structured failure with file path
    else text file
        Guard-->>Preview: UTF-8 content
        Preview->>Prepare: prepareOperation(cwd, hunk)
        Prepare->>Guard: readTextFileRejectingBinary(absolutePath, filePath)
        Guard-->>Prepare: UTF-8 content
        Prepare-->>ApplyPatch: Safe patch operation
    end
Loading

Flow diagram for binary-safe apply_patch operations

flowchart TD
    A[apply_patch update or move] --> B{Operation type}
    B -->|delete| C[Preserve existing deletion behavior]
    B -->|update or move| D[readTextFileRejectingBinary]
    D --> E[readFile]
    E --> F{Contains null byte?}
    F -->|yes| G[Reject with EBINARY]
    G --> H[No decode, write, move, or text preview]
    F -->|no| I[Decode as UTF-8]
    I --> J[Create preview and prepare write or move]
Loading

File-Level Changes

Change Details Files
Reject binary update and move operations before UTF-8 decoding or filesystem writes.
  • Read file bytes and detect null bytes before decoding.
  • Raise an actionable EBINARY error containing the patched path.
  • Apply the guard in both preview generation and operation preparation, while retaining binary deletion behavior.
src/index.ts
Add regression coverage for binary-file safety and user-visible error reporting.
  • Verify update rejection, byte-for-byte preservation, and structured failure metadata.
  • Verify binary moves leave both source and destination unchanged, while deletion still succeeds.
  • Verify pending and final tool results suppress text previews and expose the EBINARY failure.
test/index.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@paulpham157
paulpham157 marked this pull request as ready for review September 15, 2026 09:15

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/index.ts" line_range="1201-1205" />
<code_context>
+	// bytes into replacement characters, so a patch could either match
+	// garbage or silently corrupt the file on write. Reject early with a
+	// distinct code instead of letting apply_patch guess at binary content.
+	const raw = await readFile(absolutePath);
+	if (raw.includes(0)) {
+		throw Object.assign(new Error(`Refusing to patch binary file: ${hunk.filePath}`), { code: "EBINARY" });
+	}
+	const currentContent = raw.toString("utf-8");
 	const result =
 		hunk.chunks.length === 0
</code_context>
<issue_to_address>
**issue (broader_impact):** The tool's initial preview path still reads existing update targets with the UTF-8 string overload before `prepareOperation` runs, so a binary file is decoded into replacement characters and can be shown as a misleading text diff before the later `EBINARY` rejection. This violates the change's stated guarantee that binary content is rejected before decoding.

**Triggers:** When the patch is submitted through the `apply_patch` tool and the target contains invalid UTF-8 or binary bytes.

**Suggested fix:** Make preview generation use the same raw-byte null-byte check, or skip text preview for binary targets and let the operation preparation report `EBINARY`.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: src/index.ts:1205


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/index.ts Outdated
Reuse the null-byte guard when generating update previews so apply_patch
falls back to a progress-only pending update rather than decoding binary
content. Keep deletes unguarded and cover binary update, move, delete,
and preview behavior.

Co-Authored-By: Claude Code <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

@paulpham157
paulpham157 marked this pull request as draft September 15, 2026 09:48
@paulpham157
paulpham157 marked this pull request as ready for review September 15, 2026 09:49
@paulpham157
paulpham157 merged commit af7e659 into main Sep 15, 2026
8 checks passed
@paulpham157
paulpham157 deleted the feat/add-binary-file-guard-with-clear-error branch September 15, 2026 09:50

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/index.ts" line_range="919-922" />
<code_context>
 			continue;
 		}

-		const oldContent = await readFile(absolutePath, "utf-8");
+		const oldContent = await readTextFileRejectingBinary(absolutePath, hunk.filePath);
 		const newContent =
 			hunk.chunks.length === 0 ? oldContent : replaceChunks(oldContent, hunk.filePath, hunk.chunks).content;
</code_context>
<issue_to_address>
**issue (broader_impact):** Binary delete operations still read the file with `readFile(..., "utf-8")` in the delete branch, so the pending and final tool results include replacement-character text previews for binary contents even though binary update previews are suppressed. This produces the misleading preview the change is intended to prevent.

**Triggers:** When `apply_patch` deletes a binary file through the tool interface.

**Suggested fix:** Read binary deletes as bytes and omit the preview, or detect the null byte in the delete preview path and fall back to the progress-only result.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: src/index.ts:922


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/index.ts
Comment on lines -919 to 922
const oldContent = await readFile(absolutePath, "utf-8");
const oldContent = await readTextFileRejectingBinary(absolutePath, hunk.filePath);
const newContent =
hunk.chunks.length === 0 ? oldContent : replaceChunks(oldContent, hunk.filePath, hunk.chunks).content;
if (hunk.movePath) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (broader_impact): Binary delete operations still read the file with readFile(..., "utf-8") in the delete branch, so the pending and final tool results include replacement-character text previews for binary contents even though binary update previews are suppressed. This produces the misleading preview the change is intended to prevent.

Triggers: When apply_patch deletes a binary file through the tool interface.

Suggested fix: Read binary deletes as bytes and omit the preview, or detect the null byte in the delete preview path and fall back to the progress-only result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant