Fuzz Cursor-Style AI Edits: 13 Steps, 50 Min [2026]

Every edit the Cursor AI code editor makes ends the same way: a model reply has to turn into a changed file on disk. The format of that reply, whether a full rewrite, a unified diff or search/replace blocks, decides how many output tokens you pay for and how the edit fails when the model slips. In the fuzzer you build below, one clean edit to a 4,000-line file costs far more output tokens as a whole-file rewrite than as a unified diff.

Cursor’s own engineers have described the trade-off. In a 2024 engineering post, the Cursor team wrote: “Motivated by the diff format from Aider, we eliminate the line number problem. Instead of a standard diff format, the model proposes diff hunks as search/replace blocks.” (Cursor engineering team, Editing Files at 1000 Tokens per Second). That post predates every model named in this tutorial, and the sources we could verify today do not document how the editor applies edits now. So this guide does not claim to reverse-engineer Cursor. It builds the test bench instead: an offline fuzzer that shows which slips each edit format survives.

You will build the project in 13 steps and about 50 minutes, with no API key and no network calls. If you want to inspect what your editor really changed after the fact, pair it with the Cursor edit tracer. The fuzzer answers the opposite question: how an edit format behaves before any damage reaches your repo.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

What the Cursor-Style Edit Fuzzer Measures (and What It Does Not)

The fuzzer takes one small, realistic change, such as swapping a log call inside a single function, and expresses it in three formats. It then corrupts the model’s reply in ways that real models are known to corrupt replies, feeds the result to an applier and scores what lands on disk. Every attempt ends in one of three outcomes.

  • exact: the file after applying equals the intended target, byte for byte.
  • rejected: the applier refused the edit with an error. This is a loud failure, and a human or a retry loop can act on it.
  • corrupted: the applier reported success but the file differs from the target. This is a silent failure, and it is the number that matters most.

Silent corruption deserves the spotlight because it passes the checks people lean on. The patch applied, the file parses and most unit suites never touch the changed branch. A wrong-occurrence edit looks identical to a right one until someone reads the diff.

The fuzzer also tracks cost. Output tokens are the expensive side of an edit, and since October 12, 2026 new Cursor plans bill third-party models such as Claude and GPT at API list prices rather than from the bundled allowance, according to a Cursor pricing breakdown. A format that emits far fewer tokens is no longer a stylistic preference. It is a line item.

Be clear about the limits before you trust any percentage. The corpus is synthetic and repetitive on purpose. There is one edit shape. Token counts are estimates at four characters per token. The faults are injected by hand, not sampled from a live model. So the rates describe how a format and an applier respond to a given slip. They do not tell you how often any particular model slips.

Prerequisites and Versions for the Edit-Format Fuzzer

The project uses only the Python standard library, so setup takes minutes. Any editor works, including Cursor itself (if you are new to it, the walkthrough in How to Master Cursor AI covers installation and the basics).

RequirementVersionNotes
PythonNot listedBuilt and run on 3.12.3. The code uses f-strings, dataclasses and list type hints only.
gitNot listedRun on 2.43.0. Used to commit your baseline results in step 13.
Shellbash, zsh or PowerShellCommands below use bash. On Windows, replace python3 with py -3.
Third-party packagesNoneThe tests use unittest from the standard library, so there is nothing to pip install.
Disk and timeUnder 1 MB, about 50 minutesNot listed
API keysNoneEverything runs offline. The optional live-model hook is covered in the advanced tips.

Confirm your versions before you start.

python3 --version
git --version

Expected output on the machine used for this article:

Python 3.12.3
git version 2.43.0

Three Edit Formats a Cursor-Style Editor Can Use

A coding assistant such as the Cursor AI code editor has to bridge two worlds. The model produces text, and the editor needs a precise change to a file. Three formats dominate that bridge, and they sit at different points on the cost-versus-fragility curve.

A whole-file rewrite asks the model to output the entire updated file. There is no anchor to miss, so location errors cannot happen. The price is that the model regenerates every unchanged line, which costs tokens and gives it a chance to alter code it was never asked to touch.

A unified diff is the format git diff prints. It carries a header with line numbers and counts, a few lines of context and the removed and added lines. It is compact, but models are poor at counting lines, which is exactly the problem the Cursor quote above says its team designed around.

Search/replace blocks drop the line numbers. The model quotes the exact text to find and the text to put in its place. They are compact and easy for a model to write, but they depend on the quoted text matching the file exactly and matching only once.

PropertyWhole-file rewriteUnified diffSearch/replace
What the model emitsThe full updated fileHeader, context lines, removed and added linesA SEARCH block and a REPLACE block
Output grows withFile sizeEdit size plus 3 context lines per sideEdit size plus enough context to be unique
How the editor finds the changeIt does not need toLine numbers in the hunk headerExact text match in the file
Loud failureAlmost neverHunk does not apply, bad countsNo match, ambiguous match
Silent failureDropped code, copy drift, changed line endingsWrong location when matching is fuzzyWrong occurrence when the first match wins

If you want a live-editor view of the same split, the Cursor CLI diff versus rewrite benchmark times real runs. The settings that nudge the editor toward one style are covered in the Cursor rules guide for diff and rewrite edits. This tutorial sits between the two. It holds the model constant (it is a script) so that the only variable is the format.

Steps 1 and 2: Project Skeleton and a Corpus Built to Break Anchors

Step 1: Create the project layout. Make a folder, a package directory and a tests directory.

mkdir editfuzz && cd editfuzz
mkdir editfuzz tests
touch editfuzz/__init__.py
ls

You should see two directories side by side:

editfuzz  tests

Step 2: Write the corpus generator. The corpus must punish lazy anchors, so every generated function repeats the same three-line shape. A search block that quotes only the log line will match every function in the file. Create editfuzz/corpus.py.

import random
from dataclasses import dataclass

TEMPLATE = [
    "def handler_{i}(payload):",
    "    if payload is None:",
    "        return None",
    "    value = payload.get('key_{k}')",
    "    if value is None:",
    "        log.warning('missing value')",
    "        return None",
    "    return value * {m}",
    "",
]
HEADER = ["import logging", "", "log = logging.getLogger(__name__)", ""]


@dataclass
class Case:
    name: str
    original: list
    target: list
    start: int
    old: list
    new: list

    @property
    def original_text(self):
        return "".join(l + "\n" for l in self.original)

    @property
    def target_text(self):
        return "".join(l + "\n" for l in self.target)


def make_source(n_lines, seed):
    rng = random.Random(seed)
    lines = list(HEADER)
    i = 0
    while len(lines) + len(TEMPLATE) <= n_lines:
        lines += [t.format(i=i, k=rng.randint(0, 9), m=rng.randint(2, 9)) for t in TEMPLATE]
        i += 1
    return lines, i


def make_case(n_lines, position, seed):
    lines, count = make_source(n_lines, seed)
    idx = {"start": 0, "middle": count // 2, "end": count - 1}[position]
    start = len(HEADER) + len(TEMPLATE) * idx + 5
    old = [lines[start]]
    new = [f"        log.error('missing value in handler_{idx}')",
           "        metrics.incr('missing_value')"]
    target = lines[:start] + new + lines[start + 1:]
    return Case(f"{n_lines}L-{position}-s{seed}", lines, target, start, old, new)

Two details deserve attention. First, make_case picks the target function by position (first, middle or last), which lets the fuzzer test edits at the start, middle and end of a file. Second, the edit replaces one line with two, so line counts change and the diff header has to be right.

A 40-line file holds 4 handler functions, a 400-line file holds 44 and a 4,000-line file holds 444. Those counts show up later as the number of matches an under-specified search block hits.

Step 3: Emit the Same Edit in Three Formats

The emitters play the part of a careful model. Each one produces the ideal reply for a case. Create editfuzz/formats.py.

def emit_whole(case):
    return case.target_text


def emit_udiff(case, n=3):
    s, e = case.start, case.start + len(case.old)
    lo, hi = max(0, s - n), min(len(case.original), e + n)
    body = ([" " + l for l in case.original[lo:s]] + ["-" + l for l in case.old]
            + ["+" + l for l in case.new] + [" " + l for l in case.original[e:hi]])
    new_len = hi - lo - len(case.old) + len(case.new)
    head = f"@@ -{lo + 1},{hi - lo} +{lo + 1},{new_len} @@"
    return ("--- a/service.py\n+++ b/service.py\n" + head + "\n"
            + "".join(l + "\n" for l in body))


def sr_block(search, replace):
    return ("<<<<<<< SEARCH\n" + "".join(l + "\n" for l in search)
            + "=======\n" + "".join(l + "\n" for l in replace)
            + ">>>>>>> REPLACE\n")


def count_block(lines, block):
    n = len(block)
    return sum(1 for i in range(len(lines) - n + 1) if lines[i:i + n] == block)


def emit_sr(case):
    s, e = case.start, case.start + len(case.old)
    lo = s
    while count_block(case.original, case.original[lo:e]) != 1 and lo > 0:
        lo -= 1
    return sr_block(case.original[lo:e], case.original[lo:s] + case.new)


EMITTERS = {"whole": emit_whole, "udiff": emit_udiff, "sr": emit_sr}

The search/replace emitter extends its quoted context upward one line at a time until the block matches exactly once, which is what a careful model does when it notices a repeated pattern. The diff emitter writes three lines of context on each side, matching the default of git diff -U3. If you want a production-grade take on applying these formats, the fast-apply patch engine build goes deeper on the speed side.

Sanity-check the emitters on the smallest case:

python3 - <<'EOF'
from editfuzz.corpus import make_case
from editfuzz.formats import emit_udiff, emit_sr
c = make_case(40, 'start', 0)
print(emit_udiff(c))
print(emit_sr(c))
EOF

The diff and the search block come out like this:

--- a/service.py
+++ b/service.py
@@ -7,7 +7,8 @@
         return None
     value = payload.get('key_6')
     if value is None:
-        log.warning('missing value')
+        log.error('missing value in handler_0')
+        metrics.incr('missing_value')
         return None
     return value * 4


<<<<<<< SEARCH
    value = payload.get('key_6')
    if value is None:
        log.warning('missing value')
=======
    value = payload.get('key_6')
    if value is None:
        log.error('missing value in handler_0')
        metrics.incr('missing_value')
>>>>>>> REPLACE

Notice the blank context line at the bottom of the diff. It is a single space after the diff prefix, and it will cause trouble in the next step.

Step 4: Fault Injectors That Copy Real Model Slips

Now the fun part. Each mutation takes a clean reply and damages it in one specific, realistic way. Create editfuzz/mutate.py.

import re
from .formats import sr_block


def clean(text, case):
    return text


def crlf(text, case):
    return text.replace("\n", "\r\n")


def elide_tail(text, case):
    lines = text.splitlines(keepends=True)
    keep = case.start + len(case.new) + 10
    if keep >= len(lines):
        return text
    return "".join(lines[:keep]) + "# ... rest of file unchanged ...\n"


def drop_final_newline(text, case):
    return text.rstrip("\n")


def copy_drift(text, case):
    lines = text.splitlines(keepends=True)
    far = [i for i, l in enumerate(lines)
           if "payload.get('key_" in l and abs(i - case.start) > 9]
    if not far:
        return text
    i = far[len(far) // 2]
    lines[i] = lines[i].replace("key_", "keys_", 1)
    return "".join(lines)


def shift_hunk_start(text, case, by=3):
    def bump(m):
        return f"@@ -{int(m[1]) + by}{m[2] or ''} +{int(m[3]) + by}{m[4] or ''} @@"
    return re.sub(r"^@@ -(\d+)(,\d+)? \+(\d+)(,\d+)? @@", bump, text, flags=re.M)


def blank_context_unpadded(text, case):
    return "\n".join("" if l == " " else l for l in text.split("\n"))


def wrong_counts(text, case):
    return re.sub(r"^@@ -(\d+),(\d+) ",
                  lambda m: f"@@ -{m[1]},{int(m[2]) + 1} ", text, flags=re.M)


def minimal_anchor(text, case):
    return sr_block(case.old, case.new)


def _edit_search(text, fn):
    head, tail = text.split("=======\n", 1)
    return fn(head) + "=======\n" + tail


def strip_indent(text, case):
    def fn(head):
        first, *rest = head.split("\n")
        return "\n".join([first] + [l.lstrip() for l in rest])
    return _edit_search(text, fn)


def paraphrase_search(text, case):
    return _edit_search(text, lambda h: h.replace("missing value", "missing val"))


MUTATIONS = {
    "whole": [clean, elide_tail, drop_final_newline, copy_drift, crlf],
    "udiff": [clean, shift_hunk_start, blank_context_unpadded, wrong_counts, crlf],
    "sr": [clean, minimal_anchor, strip_indent, paraphrase_search, crlf],
}

The catalog below ties each injector to the slip it imitates. The crlf injector is the one most teams discover late. If your repository mixes line endings, the gitattributes setup for Cursor diffs shows how to stop AI edits from rewriting every line.

FormatMutationSlip it simulates
wholeelide_tailThe model writes a placeholder such as a rest-of-file-unchanged comment and drops the remaining code
wholedrop_final_newlineThe reply ends without the trailing newline the file had
wholecopy_driftA character changes in a line far from the edit, the classic copy error in a long rewrite
wholecrlfThe reply uses Windows line endings and the file used Unix ones
udiffshift_hunk_startThe model miscounts and the hunk header points 3 lines too low
udiffblank_context_unpaddedBlank context lines lose their single leading space
udiffwrong_countsThe old-line count in the hunk header is off by one
udiffcrlfThe diff arrives with Windows line endings
srminimal_anchorThe SEARCH block quotes only the changed line, which appears in every function
srstrip_indentThe SEARCH block loses its leading indentation
srparaphrase_searchThe SEARCH block quotes a slightly reworded line
srcrlfThe block arrives with Windows line endings

Every format also keeps a clean baseline, so each run proves the happy path before it measures the sad ones.

Step 5: Strict and Lenient Appliers

An applier is where the policy lives. The same broken reply can be rejected by a strict applier and quietly accepted by a lenient one, and that difference is the whole point of the experiment. Create editfuzz/apply.py.

import re


class ApplyError(Exception):
    pass


def _join(lines):
    return "".join(l + "\n" for l in lines)


def apply_whole(original, text, policy="raw"):
    if policy == "guarded":
        if re.search(r"\.\.\.\s*(rest|remaining|unchanged|existing|same)", text, re.I):
            raise ApplyError("elision marker in rewrite")
        if not text.endswith("\n"):
            raise ApplyError("missing final newline")
        if len(text.splitlines()) < 0.7 * len(original.splitlines()):
            raise ApplyError("rewrite shrank the file by more than 30%")
    return text


SR_RE = re.compile(r"<<<<<<< SEARCH\r?\n(.*?)=======\r?\n(.*?)>>>>>>> REPLACE", re.S)


def _find(lines, block, key=lambda s: s):
    n, kb = len(block), [key(b) for b in block]
    return [i for i in range(len(lines) - n + 1) if [key(x) for x in lines[i:i + n]] == kb]


def apply_sr(original, text, policy="strict"):
    lines = original.splitlines()
    blocks = SR_RE.findall(text)
    if not blocks:
        raise ApplyError("no SEARCH/REPLACE block found")
    for search, replace in blocks:
        s, r = search.splitlines(), replace.splitlines()
        hits = _find(lines, s)
        if not hits and policy == "lenient":
            hits = _find(lines, s, key=str.strip)
        if not hits:
            raise ApplyError("SEARCH block not found")
        if len(hits) > 1 and policy == "strict":
            raise ApplyError(f"SEARCH block ambiguous ({len(hits)} matches)")
        at = hits[0]
        lines[at:at + len(s)] = r
    return _join(lines)


HUNK_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")


def _parse_hunks(text, policy):
    hunks, cur = [], None
    for raw in text.splitlines():
        m = HUNK_RE.match(raw)
        if m:
            cur = {"at": int(m[1]) - 1, "n_old": int(m[2] or 1), "n_new": int(m[4] or 1),
                   "old": [], "new": []}
            hunks.append(cur)
        elif cur is None:
            continue
        elif raw.startswith("\\"):
            continue
        else:
            tag, body = (raw[:1], raw[1:]) if raw else (" ", "")
            if raw == "" and policy == "strict":
                raise ApplyError("empty line inside hunk (blank context lost its leading space)")
            if tag in " -":
                cur["old"].append(body)
            if tag in " +":
                cur["new"].append(body)
    return hunks


def apply_udiff(original, text, policy="strict"):
    lines = original.splitlines()
    hunks = _parse_hunks(text, policy)
    if not hunks:
        raise ApplyError("no hunks found")
    delta = 0
    for h in hunks:
        if policy == "strict" and (len(h["old"]) != h["n_old"] or len(h["new"]) != h["n_new"]):
            raise ApplyError("hunk header counts do not match the body")
        at = h["at"] + delta
        if lines[at:at + len(h["old"])] != h["old"]:
            if policy == "strict":
                raise ApplyError(f"hunk does not apply at line {at + 1}")
            hits = _find(lines, h["old"], key=str.rstrip)
            if not hits:
                raise ApplyError("hunk context not found anywhere")
            at = min(hits, key=lambda i: abs(i - at))
        lines[at:at + len(h["old"])] = h["new"]
        delta += len(h["new"]) - len(h["old"])
    return _join(lines)


APPLIERS = {"whole": apply_whole, "udiff": apply_udiff, "sr": apply_sr}
POLICIES = {"whole": ["raw", "guarded"], "udiff": ["strict", "lenient"], "sr": ["strict", "lenient"]}

Read the policies as a ladder of trust:

  • whole / raw writes whatever the model sent. whole / guarded rejects an elision marker, a missing final newline or a file that shrank by more than 30%.
  • udiff / strict requires the header counts to match the body and the old lines to sit exactly at the stated line number. udiff / lenient ignores the counts, treats an empty line as blank context and searches for the old lines, picking the match nearest the stated line.
  • sr / strict demands exactly one exact match. sr / lenient falls back to a whitespace-insensitive match and, if several matches remain, takes the first. That last choice is the one to watch.

Steps 6 to 8: Round-Trip Tests, Scoring and the Command Line

Step 6: Prove the clean path works. Before fuzzing anything, confirm that every clean reply applies to the exact target under every policy. Create tests/test_roundtrip.py.

import unittest
from editfuzz.apply import APPLIERS, POLICIES, ApplyError
from editfuzz.corpus import make_case
from editfuzz.formats import EMITTERS, count_block
from editfuzz.mutate import minimal_anchor


class RoundTrip(unittest.TestCase):
    def test_clean_edits_apply_exactly(self):
        for size in (40, 400):
            for pos in ("start", "middle", "end"):
                case = make_case(size, pos, 1)
                for fmt, emit in EMITTERS.items():
                    for policy in POLICIES[fmt]:
                        out = APPLIERS[fmt](case.original_text, emit(case), policy)
                        self.assertEqual(out, case.target_text, (size, pos, fmt, policy))

    def test_minimal_anchor_is_ambiguous(self):
        case = make_case(400, "middle", 1)
        self.assertGreater(count_block(case.original, case.old), 1)
        patch = minimal_anchor(EMITTERS["sr"](case), case)
        with self.assertRaises(ApplyError):
            APPLIERS["sr"](case.original_text, patch, "strict")
        wrong = APPLIERS["sr"](case.original_text, patch, "lenient")
        self.assertNotEqual(wrong, case.target_text)


if __name__ == "__main__":
    unittest.main()

The second test asserts something subtle. A minimal anchor must be rejected by the strict policy and must apply to the wrong place under the lenient one. If that test ever fails, your fuzzer has stopped modeling the failure it exists to catch.

Step 7: Write the scoring module. It runs every combination of size, position, seed, format, mutation and policy, and attaches a token estimate. Create editfuzz/score.py.

import math
from collections import defaultdict
from .apply import APPLIERS, POLICIES, ApplyError
from .corpus import make_case
from .formats import EMITTERS
from .mutate import MUTATIONS

SIZES = (40, 400, 4000)
POSITIONS = ("start", "middle", "end")

# USD per 1M output tokens. Replace with the prices you pay today.
PRICES = {"claude-haiku-5.5": 0.50, "composer-2.5": 2.50, "grok-4.7": 6.00}


def est_tokens(text):
    return math.ceil(len(text) / 4)


def run_one(case, fmt, mutation, policy):
    text = mutation(EMITTERS[fmt](case), case)
    row = {"case": case.name, "fmt": fmt, "mutation": mutation.__name__,
           "policy": policy, "tokens": est_tokens(text), "reason": ""}
    try:
        result = APPLIERS[fmt](case.original_text, text, policy)
        row["outcome"] = "exact" if result == case.target_text else "corrupted"
    except ApplyError as exc:
        row["outcome"], row["reason"] = "rejected", str(exc)
    return row


def run_all(seeds=5):
    rows = []
    for size in SIZES:
        for pos in POSITIONS:
            for seed in range(seeds):
                case = make_case(size, pos, seed)
                for fmt, mutations in MUTATIONS.items():
                    for mut in mutations:
                        for policy in POLICIES[fmt]:
                            r = run_one(case, fmt, mut, policy)
                            r["size"], r["pos"] = size, pos
                            rows.append(r)
    return rows


def pct(rows, outcome):
    return 100 * sum(r["outcome"] == outcome for r in rows) / len(rows) if rows else 0.0


def summarize(rows):
    groups = defaultdict(list)
    for r in rows:
        groups[(r["fmt"], r["policy"])].append(r)
    return groups

The PRICES dictionary holds output prices in dollars per million tokens. The three values are Claude Haiku 5.5 at $0.50, Composer 2.5 at $2.50 and Grok 4.7 at $6.00, taken from each vendor’s published pricing as of October 2026. Prices change, so treat them as placeholders and edit them before you quote a cost to anyone.

Step 8: Write the report and command line. Create editfuzz/cli.py and a two-line editfuzz/__main__.py.

import argparse
import json
import sys
from collections import defaultdict
from pathlib import Path
from .apply import APPLIERS
from .score import PRICES, SIZES, pct, run_all, summarize


def md(headers, body):
    out = ["| " + " | ".join(headers) + " |", "|" + "---|" * len(headers)]
    out += ["| " + " | ".join(str(c) for c in row) + " |" for row in body]
    return "\n".join(out)


def report(rows):
    parts = []
    clean = [r for r in rows if r["mutation"] == "clean"]
    by = defaultdict(list)
    for r in clean:
        by[(r["fmt"], r["size"])].append(r["tokens"])
    body = []
    for fmt in ("whole", "udiff", "sr"):
        cells = [fmt]
        for size in SIZES:
            v = by[(fmt, size)]
            cells.append(round(sum(v) / len(v)))
        body.append(cells)
    parts.append("### Output tokens per clean edit (estimate: chars / 4)\n" +
                 md(["format"] + [f"{s} lines" for s in SIZES], body))

    body = []
    for fmt in ("whole", "udiff", "sr"):
        v = by[(fmt, 4000)]
        avg = sum(v) / len(v)
        body.append([fmt] + [f"${avg * p / 1e6 * 1000:,.2f}" for p in PRICES.values()])
    parts.append("### Output cost per 1,000 edits on a 4,000-line file\n" +
                 md(["format"] + list(PRICES), body))

    body = []
    for (fmt, policy), g in summarize(rows).items():
        c = [r for r in g if r["mutation"] == "clean"]
        m = [r for r in g if r["mutation"] != "clean"]
        body.append([fmt, policy, f"{pct(c, 'exact'):.0f}%", f"{pct(m, 'exact'):.1f}%",
                     f"{pct(m, 'rejected'):.1f}%", f"{pct(m, 'corrupted'):.1f}%"])
    parts.append("### Outcomes under fuzzing (mutated outputs only, except column 3)\n" +
                 md(["format", "policy", "clean exact", "exact", "rejected (loud)",
                     "corrupted (silent)"], body))

    cell = defaultdict(list)
    for r in rows:
        if r["mutation"] != "clean":
            cell[(r["fmt"], r["mutation"], r["policy"])].append(r)
    body = [[f, mu, po, f"{pct(g, 'exact'):.0f}%", f"{pct(g, 'rejected'):.0f}%",
             f"{pct(g, 'corrupted'):.0f}%"] for (f, mu, po), g in cell.items()]
    parts.append("### Per-mutation breakdown\n" +
                 md(["format", "mutation", "policy", "exact", "rejected", "corrupted"], body))
    return "\n\n".join(parts)


def main(argv=None):
    ap = argparse.ArgumentParser(prog="editfuzz")
    sub = ap.add_subparsers(dest="cmd", required=True)
    run = sub.add_parser("run")
    run.add_argument("--seeds", type=int, default=5)
    run.add_argument("--out", default="out")
    app = sub.add_parser("apply")
    app.add_argument("--format", choices=sorted(APPLIERS), required=True)
    app.add_argument("--policy", default="strict")
    app.add_argument("--file", required=True)
    app.add_argument("--patch", required=True)
    args = ap.parse_args(argv)

    if args.cmd == "run":
        rows = run_all(args.seeds)
        out = Path(args.out)
        out.mkdir(exist_ok=True)
        (out / "results.json").write_text(json.dumps(rows, indent=1))
        text = report(rows)
        (out / "report.md").write_text(text + "\n")
        print(text)
        print(f"\n{len(rows)} attempts written to {out}/")
    else:
        original = Path(args.file).read_text()
        patch = Path(args.patch).read_text()
        sys.stdout.write(APPLIERS[args.format](original, patch, args.policy))


if __name__ == "__main__":
    main()
from .cli import main

main()

Run the tests:

python3 -m unittest discover -s tests -v

Expect green results:

test_clean_edits_apply_exactly (test_roundtrip.RoundTrip.test_clean_edits_apply_exactly) ... ok
test_minimal_anchor_is_ambiguous (test_roundtrip.RoundTrip.test_minimal_anchor_is_ambiguous) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.010s

OK

Steps 9 and 10: Run the Fuzzer and Read Its Output

Step 9: Run the full matrix. Run five seeds across three file sizes and three positions, for every format-and-mutation pair under both policies, and log each attempt.

python3 -m editfuzz run --seeds 5

Step 10: Read the tables. The command prints four tables and writes them to out/report.md, with every raw attempt in out/results.json. The first two cover cost.

Token cost: the gap grows with file size

format40 lines400 lines4000 lines
wholeNot listedNot listedNot listed
udiff767676
sr77103103

A whole-file rewrite costs many times a diff, and the gap widens as the file grows from 40 to 400 to 4,000 lines. The diff stays flat because the edit is the same size no matter how large the file grows. Search/replace sits slightly higher for large files because it quotes extra context to stay unique.

formatclaude-haiku-5.5composer-2.5grok-4.7
wholeNot listedNot listedNot listed
udiff$0.04Not listedNot listed
sr$0.05$0.26Not listed

In the PRICES table, a thousand whole-file edits on a 4,000-line file cost far more than the same thousand edits as diffs, with Haiku 5.5 and with Grok 4.7 alike. This comparison counts output tokens only and ignores input, caching and retries.

Silent versus loud failures

formatpolicyclean exactexactrejected (loud)corrupted (silent)
wholeraw100%Not listed0.0%Not listed
wholeguarded100%Not listedNot listed50.0%
udiffstrict100%25.0%75.0%0.0%
udifflenient100%100.0%0.0%0.0%
srstrict100%25.0%75.0%0.0%
srlenient100%Not listed25.0%Not listed

Four rows tell the story. Raw whole-file rewrite corrupted a large share of the mutated attempts without raising a single error. Adding the guard turned some of them into rejections, but many still slipped through, because a guard that checks for placeholders and newlines cannot see a changed character or a flipped line ending. Strict diff and strict search/replace never corrupted anything. They paid for that with a rejection rate of 75.0% on mutated replies.

The lenient rows carry the trade-off. Lenient diff recovered every mutated reply in this corpus. Lenient search/replace recovered only some replies exactly, rejected some and silently corrupted some. The corrupted share is the cost of letting the first match win.

Where lenient mode bites

formatmutationpolicyexactrejectedcorrupted
wholeelide_tailraw33%0%Not listed
wholeelide_tailguarded33%Not listed0%
wholedrop_final_newlineraw0%0%100%
wholedrop_final_newlineguarded0%100%0%
wholecopy_driftraw0%0%100%
wholecopy_driftguarded0%0%100%
wholecrlfraw0%0%100%
wholecrlfguarded0%0%100%
udiffshift_hunk_startstrict0%100%0%
udiffshift_hunk_startlenient100%0%0%
udiffblank_context_unpaddedstrict0%100%0%
udiffblank_context_unpaddedlenient100%0%0%
udiffwrong_countsstrict0%100%0%
udiffwrong_countslenient100%0%0%
udiffcrlfstrict100%0%0%
udiffcrlflenient100%0%0%
srminimal_anchorstrict0%100%0%
srminimal_anchorlenient33%0%Not listed
srstrip_indentstrict0%100%0%
srstrip_indentlenient100%0%0%
srparaphrase_searchstrict0%100%0%
srparaphrase_searchlenient0%100%0%
srcrlfstrict100%0%0%
srcrlflenient100%0%0%

Look at minimal_anchor under the lenient policy: only some replies were exact, and the rest were corrupted. The exact ones are the cases where the edit sat in the first function, so the first match happened to be the right one. Everywhere else the edit landed in the wrong function and nothing complained. In the middle and end of the file the applier picked a different handler than the one the model meant.

Note two more patterns. copy_drift and crlf corrupt 100% of whole-file attempts under both whole-file policies, so the guard does not help against them. For catching drift in live sessions rather than in a fuzzer, the Cursor Plan Mode drift checker is the better tool. And the crlf injector is harmless to both patch formats, because the appliers split lines themselves and rebuild the file with the original’s endings. A rewrite inherits whatever endings the model chose.

Steps 11 to 13: Replay a Real Reply, Set Your Prices and Gate the Result

Step 11: Replay a pasted reply with the apply command. The fuzzer is more useful when you can feed it something real. Copy a search/replace reply from any assistant into reply.txt, save the target file as service.py and call apply. The example below reproduces the wrong-occurrence case from the corpus: an edit meant for handler_22 with a one-line anchor.

python3 - <<'EOF'
from editfuzz.corpus import make_case
from editfuzz.formats import EMITTERS
from editfuzz.mutate import minimal_anchor
c = make_case(400, "middle", 1)
open("service.py", "w").write(c.original_text)
open("reply.txt", "w").write(minimal_anchor(EMITTERS["sr"](c), c))
EOF
cat reply.txt
<<<<<<< SEARCH
        log.warning('missing value')
=======
        log.error('missing value in handler_22')
        metrics.incr('missing_value')
>>>>>>> REPLACE

Apply it under the strict policy first:

python3 -m editfuzz apply --format sr --policy strict --file service.py --patch reply.txt
editfuzz.apply.ApplyError: SEARCH block ambiguous (44 matches)

The strict applier refuses and tells you why. Now apply the same reply leniently and diff the result against the original:

python3 -m editfuzz apply --format sr --policy lenient --file service.py --patch reply.txt > patched.py
diff service.py patched.py
10c10,11
<         log.warning('missing value')
---
>         log.error('missing value in handler_22')
>         metrics.incr('missing_value')

Line 10 belongs to handler_0. The new log message says handler_22. The applier reported success, the file still parses and the edit is wrong. That is silent corruption in four lines of output, and it is exactly what the table above counted.

Step 12: Set the prices you actually pay. Open editfuzz/score.py and edit PRICES. Use the output price per million tokens for each model you run, and keep the units consistent. As a reference point, Anthropic lists Claude Haiku 5.5 at $0.50 per million output tokens for prompts up to 100,000 tokens, per the Claude Haiku 5.5 announcement. Cursor lists Composer 2.5 at $0.50 per million input and $2.50 per million output tokens, and xAI lists Grok 4.7 at $6 per million output tokens for prompts under 200,000 tokens.

Step 13: Add a gate and commit a baseline. A fuzzer only protects you if it runs. Create tests/test_gate.py so that any change to an applier that lets a strict policy corrupt a file fails the build.

import unittest
from editfuzz.score import pct, run_all, summarize


class Gate(unittest.TestCase):
    def test_strict_policies_never_corrupt(self):
        for (fmt, policy), rows in summarize(run_all(seeds=2)).items():
            if policy == "strict":
                self.assertEqual(pct(rows, "corrupted"), 0.0, (fmt, policy))

    def test_guard_beats_raw_on_whole_file(self):
        groups = summarize(run_all(seeds=2))
        self.assertLess(pct(groups[("whole", "guarded")], "corrupted"),
                        pct(groups[("whole", "raw")], "corrupted"))


if __name__ == "__main__":
    unittest.main()
python3 -m unittest discover -s tests
git init && git add -A && git commit -m "Edit-format fuzzer baseline"

Expect four passing tests. When you tune an applier later, rerun python3 -m editfuzz run --seeds 5, commit out/report.md and let the diff of that file show what your change did to the corrupted column.

What the Numbers Say About Rewrite vs Diff Edits

For anyone who leans on the Cursor AI code editor every day, the fuzzer supports four conclusions, and each carries a caveat from the limits listed earlier.

Cost favors patches on anything but tiny files. The 3-times gap at 40 lines is small enough to ignore. The gap at 4,000 lines is not. If your assistant rewrites large files wholesale, you pay for every unchanged line, and the bill scales with file size rather than with the size of your request.

A rewrite never misses an anchor, and that is its real strength. There is no location to get wrong, so whole-file replies never produced a loud failure in the clean runs. The same property is the weakness: with nothing to reject, every slip lands on disk. A reviewer reading only the changed lines in the editor’s diff view will catch a dropped tail. A flipped character twenty lines away is far easier to miss.

Line numbers are a useful tiebreaker. Lenient diff scored 100% here partly because the stated line number picks the nearest of several identical sequences. The Cursor quote above describes dropping line numbers to avoid miscounting, and this fuzzer shows the flip side: dropping them removes the tiebreaker, so search/replace has to earn uniqueness from its quoted text.

Strict by default, lenient by exception. The corrupted column is zero for both strict patch policies, so strictness converts silent failures into retries. A retry costs one more reply, which at Haiku 5.5 output prices is a fraction of a cent for a diff. A silent wrong-occurrence edit costs a review cycle. If you want to review rewrites after the fact, the Cursor Composer rewrite audit walks through it, and the software category collects the rest of our editor and tooling guides.

One more point on scope. Cursor’s current lineup, per the editor’s own documentation, includes Grok 4.7 as its flagship, Composer 2.5 and the opt-in Claude Haiku 5.5. None of those pages document which edit format the editor uses behind the scenes, so no number in this article describes Cursor’s internals. They describe the formats.

Common Pitfalls When You Benchmark AI Edit Formats

These traps catch most teams the first time they compare edit formats, whether the target is the Cursor AI code editor or any similar tool.

  1. Treating a clean apply as a correct apply. The lenient search/replace rows applied without errors and still corrupted the file. Always compare the result with the intended target, not just the exit code.
  2. Testing only small files. At 40 lines the rewrite costs 3 times a diff. At 4,000 lines it costs far more. A benchmark on a toy file hides the cost curve that decides real bills.
  3. Using unique anchors in your corpus. If every quoted line appears once, search/replace looks flawless. Real code repeats guard clauses, imports and error handling, so build repetition in on purpose, as the corpus here does.
  4. Trusting the character-based token estimate for billing. Dividing characters by four is fine for ratios. It will not match a vendor’s tokenizer, and code often tokenizes differently from prose. Swap in the real tokenizer before you publish dollar figures.
  5. Reading injected-fault rates as model quality. A 100% corruption rate for copy_drift means the format has no defense against that slip. It does not mean any model makes the slip 100% of the time.
  6. Ignoring line endings. A mixed-endings repository turns a harmless-looking rewrite into a diff that touches every line. See the Cursor fast-apply safety net for a review workflow that catches this before commit.
  7. Making the lenient policy the default. Recovery is attractive because it turns rejections into successes. Check the corrupted column before you switch it on.

Troubleshooting the Edit-Format Fuzzer

SymptomLikely causeFix
ModuleNotFoundError: No module named ‘editfuzz’You ran the command from the wrong directoryRun from the project root and use python3 -m editfuzz, not python3 editfuzz/cli.py
SEARCH block not found on a clean replyThe file has tabs, trailing spaces or CRLF endings that the quoted block lacksNormalize the file first, or use the lenient policy to match on stripped lines
SEARCH block ambiguous (44 matches)The anchor is too small and the pattern repeatsAdd a unique line, such as the def line, above the change
hunk does not apply at line NThe hunk header’s line number is wrongRegenerate the diff with git diff, or use the lenient policy to search for the old lines
hunk header counts do not match the bodyThe model miscounted lines in the headerRecompute the counts from the body, or use the lenient policy
empty line inside hunkBlank context lines lost their leading spaceRepair the diff by padding empty lines with a space before applying
elision marker in rewriteThe model wrote a rest-of-file placeholderReject the reply and re-prompt for a diff or search/replace format
missing final newlineThe reply ended without a trailing newlineAppend a newline, or reject if you want strict fidelity
Percentages differ from this articleA different –seeds value, or an edited corpus or price tableRun python3 -m editfuzz run –seeds 5 on the unmodified code
Token counts look low next to your tokenizerest_tokens divides characters by fourReplace est_tokens with a call to the tokenizer for the model you pay for

If a failure does not fit the table, open out/results.json and filter by the reason field. Every rejection records the exact message the applier raised, so a quick count of reasons shows which slip dominates. In the reference run, the most common reason was a missing SEARCH block, followed by four families of 45 each: a missing final newline, an empty line inside a hunk, mismatched header counts and ambiguous search blocks.

Advanced Tips: Extend the Fuzzer Toward Real Cursor AI Code Editor Workloads

  • Compose faults. Real replies often carry two slips at once, such as stripped indentation plus a reworded line. Loop over pairs of mutations from the same format and see which combinations break the lenient policies.
  • Add more edit shapes. The corpus changes one line into two. Add pure deletions, multi-hunk changes, renames across the file and edits near the very first or last line, where context windows get clipped.
  • Tighten the lenient search policy. Instead of taking the first match, rank candidates by distance to a hint such as the nearest function name. Re-run and watch the corrupted figure fall.
  • Hook up a live model. Replace the emitters in formats.py with a function that sends the original file and an instruction to the model you use, then pass the reply through the same mutators and appliers. Keep the seeds fixed so runs compare cleanly.
  • Review the result structurally. A line diff will not tell you whether a rewrite moved code around. Try Difftastic on Cursor AI edits, and add a linter pass as a final check on whatever the appliers accept.

Finish by committing a fresh out/report.md every time you change an applier or add a mutation. The report is only about 50 lines, and the git history of that file becomes a record of how your edit pipeline’s safety changed over time.

FAQ: Cursor AI Code Editor Edit Formats and the Fuzzer

Does this tell me which edit format Cursor uses today?

No. The fuzzer tests generic formats against an offline applier. The sources verified for this article do not document the editor’s current apply behavior, so nothing here describes Cursor’s internals. A 2024 Cursor engineering post described search/replace blocks in its design, and that is the extent of what the evidence supports.

Is a whole-file rewrite always worse than a diff?

No. It cannot miss an anchor. Its weakness grows with file size, and with its inability to reject anything it should reject.

Diff hunks carry a line number, so the applier can choose the closest of several identical sequences. Search/replace has no such hint, so with several matches the applier falls back to the first. Larger line-number errors than the 3-line shift injected here would narrow the gap.

How many seeds do I need?

The default of 5 seeds covers the full matrix. Raise the seed count if you add mutations or edit shapes.

Can I run the fuzzer against a live model?

Yes. Swap the emitters for calls to your model, or paste a single reply into reply.txt and use the apply command. Keep prompts and seeds fixed, and report both token counts and the corrupted rate, not just the success rate.

Why do the patch formats ignore CRLF but the rewrite does not?

The appliers split the file and the patch into lines and rebuild the result with the original’s line endings. A whole-file reply is written as sent, so any ending the model chooses becomes the new ending.

How accurate are the dollar figures?

They are estimates. Tokens are counted at four characters each, only output tokens are priced, and the prices are the October 2026 list prices for three models. Replace both the estimator and the price table before using the figures in a budget.

Nadia Dubois

Nadia Dubois

AI & Innovation Editor

Nadia Dubois is the AI & Innovation Editor at Tech Insider, where she tracks the rapid evolution of artificial intelligence, from foundation models to real-world enterprise deployment. She previously covered AI and startups for La Tribune and contributed to MIT Technology Review's European coverage. Nadia specializes in generative AI, AI regulation, and the intersection of technology and European industrial policy. She holds a dual degree in Computational Linguistics and Journalism from Sciences Po Paris.

View all articles