# Why a patch fails to apply: broken unified diffs

> A patch is rejected when its hunk counts, context lines, line endings or last-line marker disagree with the file. How to spot each and test before applying.

Published 2026-10-28 · https://aiskills402.com/blog/unified-diff-broken-patches

A unified diff is a small contract between a patch and one specific file. Every line that the patch calls unchanged or removed has to be present in that file, byte for byte, and the numbers in each section header have to describe the section that follows. When a person writes the diff by running a tool, the contract holds. When a model, a chat window or a hand edit produces it, one detail often slips, and the patch program answers with a rejection that names a line number but not the real cause. This guide lists the parts of a diff that break most often, what each break looks like, and how to test a diff before it touches a working tree.

## What a unified diff promises

The layout is set out in the GNU diffutils manual. A header of two lines names the source file and the target file. After it come one or more hunks, each covering one area where the files differ. A hunk opens with a line between double at-signs that carries the position in the original and the position in the result, given as a first-line number followed by a length, and a hunk of just one line may show only the first-line number ([GNU diffutils manual, unified format](https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html)). The lines that follow begin with a space for text common to both files, a minus for a removed line and a plus for an added one.

From that layout, a reader can derive the check that matters most. Old length: unchanged lines plus deleted ones. New length: unchanged lines plus inserted ones. This arithmetic is a consequence of the format rather than a sentence quoted from the manual, so it is worth verifying on a real diff produced by a tool. Everything below is a way for the header, the lines or the file to disagree with that bookkeeping.

## Break one: the hunk header counts do not match the lines

This is the commonest failure from generated patches. A writer deletes one line from the middle of a hunk, or adds two, and forgets to touch the header. The patch tool reads the header, expects a certain number of lines, and either runs out of hunk early or meets the next header while still expecting content. The result is usually a message about a corrupt patch or a malformed hunk at some line.

The git manual treats this as a known situation. Its `--recount` option tells git not to trust the counts in the headers and to work them out by inspecting the patch, with the manual giving the example of a patch edited without adjusting the headers ([git-apply manual](https://git-scm.com/docs/git-apply)). That option is a rescue for a diff whose lines are right and whose numbers are stale. It does not help when the lines themselves are wrong, which the next sections cover.

A related slip is the start number. The tool uses it as a first guess for where to look. The patch manual says it begins at the stated line, adjusted by the offset found for the previous hunk, and if the context is not there it scans forward and backward for a place where the context matches ([patch manual](https://man7.org/linux/man-pages/man1/patch.1.html)). So a drifted start is often forgiven by `patch`, while a wrong count is not. Treat that as a difference in tools, not a promise: `git apply` is stricter about placement than the classic program, and this guide did not check every version.

## Break two: context lines retyped from memory

Context lines and removed lines have to equal what the file holds. A model that writes a diff from its picture of the file, rather than from the file itself, tends to normalise what it sees. Common changes:

- a tab turned into spaces, which is especially damaging in files where the tab is meaningful, such as a Makefile;
- trailing spaces dropped from a line that had them;
- a blank line that held spaces in the file but is written as a completely empty line (a truly empty line in the diff is accepted as blank context by both `git apply` and `patch`, as a small test for this guide showed);
- a quote style, a comment or a variable name subtly altered.

The `patch` program has a loose-matching mode for the whitespace differences. With `-l` or `--ignore-whitespace`, runs of blanks in the patch match any run of blanks in the file and blanks at line ends are ignored, but ordinary characters must still match exactly ([patch manual](https://man7.org/linux/man-pages/man1/patch.1.html)). Git has a comparable `--ignore-whitespace` option for context lines ([git-apply manual](https://git-scm.com/docs/git-apply)). These switches are a workaround. They apply the hunk without fixing the diff, so the next person who uses the same patch hits the same wall. A wrong word in a context line is never forgiven, and that is correct: it means the diff was written for a different version of the file.

## Break three: the missing-newline marker

The diffutils manual calls a final line that lacks its terminating newline incomplete, and a diff flags such a line by putting another line right after it, opening with a backslash ([GNU diffutils manual, incomplete lines](https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html)). In practice the marker reads `\ No newline at end of file`, and it belongs directly after the line it describes. The marker is metadata rather than content, so neither header number includes it.

Writers drop it constantly, because it looks like noise. The consequence is real, though: without it, the diff claims that the last line is complete, the file says otherwise, and the hunk fails or silently alters the tail of the text. The mirror error also occurs, where the marker is left after a line that does have a break, or where new lines are appended behind an unterminated final line without turning that line into a removed and re-added one. Git even ships an `--inaccurate-eof` option for patches made by diff programs that detected the condition wrongly ([git-apply manual](https://git-scm.com/docs/git-apply)).

## Break four: line endings

Suppose the file ends its lines with the Windows pair of control characters and the diff arrives with only the second one: every context line then differs from the file by one invisible character. Chat windows, clipboards and web forms are where the first character usually vanishes. The patch manual notes that it takes CRLF into account when it strips indentation or encapsulation from a diff, and it documents a `--binary` switch that disables the heuristic for turning CRLF into LF ([patch manual](https://man7.org/linux/man-pages/man1/patch.1.html)). On a machine that does not translate line endings, the safest assumption is that each context line must carry exactly the ending that the file has.

## Break five: text around the diff

The patch manual says the program tries to skip leading and trailing garbage, so an email that contains a diff can be fed to it directly ([patch manual](https://man7.org/linux/man-pages/man1/patch.1.html)). That tolerance belongs to `patch`; a strict consumer, a script that splits the answer of a model on hunk markers, or a tool that expects only a diff, will take a greeting, a closing sentence or a Markdown code fence as a patch line. Fence lines and chat prose are the easiest breaks to prevent: ask for the diff alone, and strip anything outside the first header and the last hunk before checking.

## Checking a diff before it is applied

Do not discover the break by applying. Both programs have a mode that tests without writing:

1. Run `git apply --check change.diff`. It looks at the working tree and the patch and reports errors without applying anything ([git-apply manual](https://git-scm.com/docs/git-apply)). By default git is atomic: when some hunks fail, it refuses the whole patch and leaves the tree alone, and the `--reject` option instead applies what fits and writes the rest to `.rej` files.
2. For the classic tool, `patch --dry-run` prints what would happen and changes no file ([patch manual](https://man7.org/linux/man-pages/man1/patch.1.html)). Its exit status is zero when every hunk applied, one when some hunks could not be applied, and two for more serious trouble, so a script can branch on the result.
3. Read the messages for offsets and fuzz. The patch manual warns that a larger fuzz factor raises the odds of a faulty patch and that a large offset can mean a hunk landed in the wrong place ([patch manual](https://man7.org/linux/man-pages/man1/patch.1.html)). A hunk that applies with a big offset or a lot of fuzz is not a clean pass.

A check is only worth running if it can fail. Start from a diff that applies cleanly, change one count by one, replace one tab with spaces, delete the marker, and confirm that the check turns red each time. If it stays green after those edits, it was not testing what you thought, and a green result on the real patch means nothing. Run the same check on the final file too: after applying, look at the diff between the result and the intended file, because a patch that applied cleanly can still have placed lines in the wrong spot.

A diff can also pass every one of these checks and still not be the change that was meant. Applying cleanly only shows that the lines fit; it says nothing about whether the added lines are the right ones, so any repair should touch only the bookkeeping around the change, never the change. For a related lesson about trusting descriptions of a change instead of the change itself, see our write-up on [pull request descriptions that claim more than the diff](/blog/llm-pr-description-claims-diff).

## When this does not apply

Plain-text unified diffs applied with `git apply` or `patch` are the subject. Binary patches, context-format or ed-script diffs, patches with renames and mode changes, and merge tools that resolve conflicts by themselves are outside it. Behaviour differs between versions of `patch`, `git` and the platform's line-ending handling, so confirm details in the manual pages cited above; this guide did not check every version. The count arithmetic was derived from the format, not quoted from a manual. No experiments went into it beyond one small test of the empty-line case: the examples are illustrations from the documentation and from common patterns, not measurements, and a diff that fits none of these breaks may be broken in a way not listed here.

## One tool for the repair step

For a patch that is rejected only because of this bookkeeping, our [unified-diff-repair](https://aiskills402.com/skills/unified-diff-repair) skill takes the original file plus the broken diff and returns a diff in which every hunk header is recounted, unchanged and deleted lines are rewritten from the file, a missing last-line marker is added where required, hunks are sorted, and surrounding prose or a code fence is dropped. Inserted and deleted lines keep their exact text. If a hunk expects text that the file lacks, or that matches several places, the answer is a fixed one-line refusal rather than a guess.
