AI agents inside the Cursor code editor can hand back a one-line patch or a rewritten file, and Git’s default diff presents both with the same calm formatting. A 39-line Python file that gains one discount line produces a 1-line diff. The same feature delivered as a whole-file rewrite produces 7 hunks, 23 added lines and 18 deleted lines. Both look fine at a glance, but only one deserves your full attention.
The cost of that attention is rising. Cursor began selling new plans through The X Subscription on October 12, 2026, and a pricing breakdown of the change reports that Claude, GPT and Gemini usage is billed at API rates instead of being bundled. If that holds for your account, a careless rewrite costs tokens first and review time second.
This tutorial skips the generation side. If you want a log of how each change arrived, the edit tracer tutorial covers it, and the fuzzing walkthrough stress-tests the apply step. Here you review what landed. You will build a small Python scorer around Git’s own diff engine, test it on four simulated AI edits, and add a pre-commit gate that stops rewrite-sized changes until a human reads them.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What You Build With the Cursor Code Editor and Git Diff
The finished project is a folder called aidiff-review with five working parts. Every part runs on plain Git and the Python standard library, so nothing in it depends on a model call, an API key or a Cursor plan.
- Two sample files (
orders.pyandcart.js) that stand in for the code an agent edits. - A demo script that creates four branches, each holding a different style of AI edit: a targeted patch, a whole-file rewrite, a reorder with whitespace noise, and a function swap.
- A scorer (
tools/aidiff.py) that runs the same change through four Git diff algorithms plus a whitespace-blind pass, then labels it patch-like, move-heavy or rewrite-like. - Git defaults and an alias so every
git diffyou run in the Cursor code editor’s terminal uses the better algorithm and highlights moved code. - A pre-commit gate that refuses to commit a rewrite-like change until you override it on purpose.
The idea behind the scorer is simple. A diff algorithm decides which lines to show as removed and which as added. For a patch, every algorithm agrees. For a rewrite or a reorder, they disagree, and the disagreement decides whether you read 2 tidy hunks or 4 interleaved ones. Counting hunks and moved lines turns that feeling into a number you can gate on.
Timing is a separate question. The benchmark of Cursor CLI diff and rewrite edits measures how fast each strategy applies. This project measures what a human has to read afterwards. Budget about 45 minutes: 10 for setup, 10 for the algorithm comparison, 15 for the scorer and 10 for the workflow wiring.
One honesty note before you start. As of October 29, 2026, I could not find Cursor documentation that exposes per-edit telemetry, such as which edits arrived as full rewrites or how many tokens each one used. That is why this project inspects the result in the working tree instead of the request. It works the same whether the change came from the Cursor code editor, a CLI agent or a colleague’s paste.
Prerequisites and Tested Versions
Every command below ran in a Linux shell. Older Git works for most steps, but the moved-line options in Step 7 need a release from the last several years, so use a recent version.
| Tool | Tested version | Minimum | Used for |
|---|---|---|---|
| Git | Not listed | 2.30 | Diff algorithms, --color-moved, hooks |
| Python | 3.12.3 | 3.8 | The scorer (standard library only) |
| Cursor code editor | Current build (no build number verified) | Any recent build | Integrated terminal and the optional task in Step 11 |
| Shell | sh on Linux | Bash, Zsh or Git Bash | Heredocs in the demo script |
| Disk space | Under 1 MB | Any | One repo, four branches, two small files |
Any Cursor plan works, including the free one, because the demo simulates the edits and never calls a model. For reference, a Subscription Playbook teardown lists the individual tiers as Free at $0, Plus at $8, Premium at $30 and Super at $100 a month. If you are new to the editor itself, start with the 12-step Cursor tutorial and come back.
Windows users have two options. Run everything inside WSL or Git Bash, which matches the commands as written. Or run the Python scorer natively with py -3 in place of python3 and translate the shell script by hand. The first route costs five minutes and avoids most of the troubleshooting list near the end.
Confirm the tools before you begin. If either command fails, fix that first, because nothing later works without them.
git --version
python3 --version
git version 2.43.0
Python 3.12.3
Rewrite Edits vs Patch Edits: Why the Default Diff Misleads
An AI editor has two broad ways to change a file. It can emit a targeted change, which is a search-and-replace block or a unified diff, and the editor applies that to the existing text. Or it can emit the whole file and overwrite the old one. Models pick between these styles based on file size, instructions and their own habits, and you rarely see the choice. You only see the aftermath in your working tree.
How a rewrite shows up in the working tree
After a rewrite, Git sees a file whose lines mostly differ from the committed version. It has no idea that you asked for one feature. It reports every changed line with equal weight. A renamed variable, a switch from single to double quotes, a new docstring, a function moved 10 lines down and the actual feature all arrive as the same red and green lines. The feature you asked for hides inside the reformatting.
Three kinds of noise cause most review pain. Reflow noise changes quotes, wrapping or trailing whitespace on lines the model had no reason to touch. Move noise relocates a whole block, which the default algorithm may display as a tangle of small interleaved hunks. Style drift adds type hints or docstrings everywhere because the model’s habits differ from your codebase. Each needs a different tool, and the next sections give you one for each.
You can also reduce the problem upstream. The guide to tuning diff versus rewrite edits with Cursor rules shows how to steer the model toward patches. Steering helps but never guarantees anything, so a reviewer-side check still earns its place. Think of rules as prevention and this project as detection.
A rewrite is not always wrong. A new file, a mechanical migration or a deliberate refactor can legitimately touch most lines. The goal is not to ban rewrites. The goal is to make sure a rewrite never slips into your history looking like a one-line fix. The scorer therefore reports a verdict and lets you decide, and the hook asks for a conscious override instead of a rule-follow.
Here is how the four demo edits map to the three review problems. The numbers appear in later steps, so skim this now and return to it when you read the results.
| Demo branch | File | What the simulated agent did | Main review problem |
|---|---|---|---|
| ai-patch | orders.py | Added one line to apply a discount | None, a clean patch |
| ai-rewrite | orders.py | Rewrote the file with type hints, f-strings, docstrings and a reordered function | Reflow and style drift on nearly every line |
| ai-reorder | orders.py | Moved one function and left trailing spaces on two lines | Move noise plus whitespace noise |
| ai-swap | cart.js | Swapped the order of two similar functions | Interleaved hunks under the default algorithm |
| main | both | The committed baseline | The reference point for every comparison |
Steps 1 to 3: Create the Project and Four Simulated AI Edits
Step 1: Create the folder. Pick any scratch location. The tools subfolder will hold the scorer you write in Step 8.
mkdir -p aidiff-review/tools
cd aidiff-review
Step 2: Write the two sample files. Save the Python file as orders.py. It is 39 lines long, which matters later because churn is measured as a share of the original line count.
import json
import sys
TAX_RATE = 0.08
def load_orders(path):
with open(path) as f:
return json.load(f)
def line_total(item):
return item['price'] * item['qty']
def order_total(order):
subtotal = 0
for item in order['items']:
subtotal += line_total(item)
return round(subtotal * (1 + TAX_RATE), 2)
def format_receipt(order):
lines = []
for item in order['items']:
lines.append('%s x%d %.2f' % (item['name'], item['qty'], line_total(item)))
lines.append('TOTAL %.2f' % order_total(order))
return '\n'.join(lines)
def main():
orders = load_orders(sys.argv[1])
for order in orders:
print(format_receipt(order))
print()
if __name__ == '__main__':
main()
Save the JavaScript file as cart.js. Its three functions share the same shape (an early return, one mutation, a return), and that repetition is exactly what makes diff algorithms disagree in Step 4.
function addItem(cart, item) {
if (!item) {
return cart;
}
cart.items.push(item);
return cart;
}
function removeItem(cart, id) {
if (!id) {
return cart;
}
cart.items = cart.items.filter((i) => i.id !== id);
return cart;
}
function clearCart(cart) {
cart.items = [];
return cart;
}
module.exports = { addItem, removeItem, clearCart };
Step 3: Generate the four AI edits. Save the next script as make_demo.sh. It commits the two files as a baseline on main, then builds one branch per edit style. Each branch holds a single commit, so main versus the branch is always one clean comparison. The script deliberately leaves tools/ untracked, which keeps the baseline free of the scorer and the branches exactly one commit away from main.
#!/bin/sh
set -e
git init -q -b main .
git config user.email [email protected]
git config user.name demo
git add orders.py cart.js
git commit -q -m "baseline"
# 1) targeted patch: one added line
git checkout -q -b ai-patch main
python3 - <<'PY'
s = open("orders.py").read()
s = s.replace(" return round(subtotal * (1 + TAX_RATE), 2)",
" subtotal -= order.get('discount', 0)\n return round(subtotal * (1 + TAX_RATE), 2)")
open("orders.py", "w").write(s)
PY
git commit -qam "patch: add discount"
# 2) whole-file rewrite: types, f-strings, docstrings, reordered functions
git checkout -q -b ai-rewrite main
cat > orders.py <<'PY'
import json
import sys
from typing import Any
TAX_RATE = 0.08
def load_orders(path: str) -> list[dict[str, Any]]:
"""Load orders from a JSON file."""
with open(path) as handle:
return json.load(handle)
def line_total(item: dict[str, Any]) -> float:
"""Return price times quantity for one line."""
return item["price"] * item["qty"]
def format_receipt(order: dict[str, Any]) -> str:
"""Render one order as a plain-text receipt."""
rows = [
f"{item['name']} x{item['qty']} {line_total(item):.2f}"
for item in order["items"]
]
rows.append(f"TOTAL {order_total(order):.2f}")
return "\n".join(rows)
def order_total(order: dict[str, Any]) -> float:
"""Return the order total including tax, after any discount."""
gross = sum(line_total(item) for item in order["items"])
gross -= order.get("discount", 0)
return round(gross * (1 + TAX_RATE), 2)
def main() -> None:
orders = load_orders(sys.argv[1])
for order in orders:
print(format_receipt(order))
print()
if __name__ == "__main__":
main()
PY
git commit -qam "rewrite: add discount"
# 3) reorder plus whitespace: format_receipt moves above order_total
git checkout -q -b ai-reorder main
python3 - <<'PY'
s = open("orders.py").read()
a, b = s.index("def format_receipt"), s.index("def main")
block = s[a:b]
s = s[:a] + s[b:]
t = s.index("def order_total")
s = s[:t] + block + s[t:]
s = s.replace(" return round(subtotal * (1 + TAX_RATE), 2)",
" subtotal -= order.get('discount', 0)\n return round(subtotal * (1 + TAX_RATE), 2)")
s = s.replace(" print(format_receipt(order))\n print()",
" print(format_receipt(order)) \n print() ")
open("orders.py", "w").write(s)
PY
git commit -qam "reorder: add discount"
# 4) function swap in JavaScript: where Myers and patience disagree
git checkout -q -b ai-swap main
python3 - <<'PY'
s = open("cart.js").read()
a, b, c = s.index("function addItem"), s.index("function removeItem"), s.index("function clearCart")
open("cart.js", "w").write(s[:a] + s[b:c] + s[a:b] + s[c:])
PY
git commit -qam "swap: reorder cart helpers"
git checkout -q main
Run it, then list the branches.
chmod +x make_demo.sh
./make_demo.sh
git branch
ai-patch
ai-reorder
ai-rewrite
ai-swap
* main
If you see the five names above, the setup works. If the script stops with an error about user identity, your Git install has no name or email configured globally. The script sets a local identity, so that case should not occur, but a locked-down corporate config can override it. Fix it with git config user.email [email protected] inside the folder and run the script again after deleting the .git directory.
Steps 4 and 5: Compare Myers, Patience and Histogram on Real Edits
Step 4: Diff the swap with Git’s default algorithm. Git uses the Myers algorithm unless you configure otherwise. Run it against the function swap and read the result as a reviewer would.
git diff --diff-algorithm=myers main ai-swap -- cart.js
@@ -1,16 +1,16 @@
-function addItem(cart, item) {
- if (!item) {
+function removeItem(cart, id) {
+ if (!id) {
return cart;
}
- cart.items.push(item);
+ cart.items = cart.items.filter((i) => i.id !== id);
return cart;
}
-function removeItem(cart, id) {
- if (!id) {
+function addItem(cart, item) {
+ if (!item) {
return cart;
}
- cart.items = cart.items.filter((i) => i.id !== id);
+ cart.items.push(item);
return cart;
}
This is the review nightmare. Myers found the shortest edit script, which pairs up the shared lines (the early return, the closing braces) and treats only the differing lines as changes. The result looks like two functions that had their conditions and bodies edited. Nothing says the two functions simply traded places. A tired reviewer might approve it as a logic change or flag a bug that does not exist.
Now run the same comparison with the histogram algorithm.
git diff --diff-algorithm=histogram main ai-swap -- cart.js
@@ -1,11 +1,3 @@
-function addItem(cart, item) {
- if (!item) {
- return cart;
- }
- cart.items.push(item);
- return cart;
-}
-
function removeItem(cart, id) {
if (!id) {
return cart;
@@ -14,6 +6,14 @@ function removeItem(cart, id) {
return cart;
}
+function addItem(cart, item) {
+ if (!item) {
+ return cart;
+ }
+ cart.items.push(item);
+ return cart;
+}
+
function clearCart(cart) {
cart.items = [];
return cart;
Now the story is obvious. One whole function disappeared from the top and reappeared lower down, unchanged. You can verify that in seconds. The histogram output is longer (16 changed lines against 12) but far easier to read, because each hunk is one coherent unit.
Step 5: Read the numbers for every algorithm. The scorer you build in Step 8 prints this table automatically. The values below come from the demo repo, and you can reproduce the first two rows right now with git diff --numstat --diff-algorithm=myers main ai-swap and the histogram equivalent.
| Algorithm | Hunks | Added lines | Deleted lines | Total changed | Reads as |
|---|---|---|---|---|---|
| myers (Git default) | 4 | 6 | 6 | 12 | Interleaved edits, hides the swap |
| minimal | 4 | 6 | 6 | 12 | Same as Myers on this file |
| patience | 2 | 8 | 8 | 16 | One moved function |
| histogram | 2 | 8 | 8 | 16 | One moved function |
| histogram with -w | 2 | 8 | 8 | 16 | Same, no whitespace differences exist here |
Two lessons fall out of that table. First, fewer changed lines does not mean easier review. Myers wins on line count and loses on comprehension. Second, minimal is no cure for this problem, since it chases the smallest edit script and that is the very thing misleading you. Patience and histogram trade a few extra lines for hunks that follow the structure of the code.
Line-based algorithms still have a ceiling, because they compare text and not syntax. For structure-aware output, the Difftastic guide for Cursor AI edits compares syntax trees instead. The two approaches complement each other. Use the Git algorithms for gating and quick reads, and reach for a structural diff when a line-based view still confuses you.
On the other three branches all algorithms agree, so the choice only matters for moved code. That is fine. You pay nothing for choosing histogram everywhere, and you gain readability on the edits that need it most.
Steps 6 and 7: Filter Whitespace and Highlight Moved Lines
Step 6: Ignore whitespace when reading. The ai-reorder branch moved a function and also left trailing spaces on two print lines. Compare the totals with and without whitespace sensitivity.
git diff --shortstat main ai-reorder
git diff --shortstat -w main ai-reorder
1 file changed, 10 insertions(+), 9 deletions(-)
1 file changed, 8 insertions(+), 7 deletions(-)
Two insertions and two deletions vanish under -w. Those four lines were the whitespace-only edits, and the hunk count in the scorer drops from 3 to 2 for the same reason. Searches for git diff ignore whitespace are common for exactly this reason: AI edits leave stray spaces, tabs and line endings that bury the real change.
Use -w for reading, never for gating. In Python, YAML and Makefiles, whitespace carries meaning, and a change that only touches indentation can alter behavior. The scorer therefore prints the -w row for information and bases its verdict on the whitespace-sensitive numbers. If a rewrite tool converts line endings from LF to CRLF across the file, every line counts as changed, and the dedicated gitattributes setup for Cursor diffs shows how to stop that at the source.
Other whitespace flags are worth knowing. -b ignores changes in the amount of whitespace, --ignore-space-at-eol ignores trailing spaces only, and --ignore-cr-at-eol ignores carriage returns. The narrower the flag, the less likely it hides a real problem.
Step 7: Highlight moved code with color. Git can detect blocks that moved and draw them in different colors from genuine additions and deletions. Run the next command in a terminal that supports color, such as the Cursor code editor’s integrated terminal.
git diff --color-moved=dimmed-zebra --diff-algorithm=histogram main ai-swap
In the output, the moved function appears dimmed on both ends, while the lines that really changed keep the standard red and green. When I captured the raw escape codes, the moved deletions carried the dim attribute, which confirms the feature fires on this swap. Dimming tells your eye to skip the block and spend attention elsewhere.
Moved-line detection pairs well with a second option, diff.colorMovedWS. When you set it to allow-indentation-change, Git still recognizes a block as moved even if you re-indented it on the way. That matters when an agent moves a function into a different class or nesting level.
A caution applies here. Color-moved is a reading aid, not a measurement. It leaves the line counts unchanged and only changes how those lines look. That is why the scorer in the next section counts moved pairs in code instead of relying on colors you cannot gate on.
Steps 8 and 9: Write the Churn Scorer and Run It
Step 8: Save the scorer. Put the next file at tools/aidiff.py. It shells out to Git, so it inherits every algorithm and edge case Git already handles. The Python standard library is the only dependency.
#!/usr/bin/env python3
"""Score a working-tree, staged or branch-to-branch change as patch-, move- or rewrite-like."""
import argparse
import json
import subprocess
import sys
from collections import Counter
ALGOS = ["myers", "minimal", "patience", "histogram"]
def git(*args):
out = subprocess.run(["git", *args], capture_output=True, text=True)
if out.returncode not in (0, 1):
sys.exit(out.stderr.strip() or "git failed")
return out.stdout
def diff_args(opts):
if opts.staged:
return ["diff", "--cached"]
if opts.head:
return ["diff", opts.base, opts.head]
return ["diff", opts.base]
def changed_files(opts):
names = git(*diff_args(opts), "--name-only", "--diff-filter=M").split("\n")
return [n for n in names if n and (not opts.ext or n.endswith(tuple(opts.ext)))]
def parse(patch):
hunks, added, deleted = 0, [], []
for line in patch.splitlines():
if line.startswith("@@"):
hunks += 1
elif line.startswith("+") and not line.startswith("+++"):
added.append(line[1:])
elif line.startswith("-") and not line.startswith("---"):
deleted.append(line[1:])
return hunks, added, deleted
def run(opts, path, *flags):
patch = git(*diff_args(opts), "-U0", "--no-color", *flags, "--", path)
return parse(patch)
def score_file(opts, path):
base_ref = "HEAD" if opts.staged else opts.base
old_lines = len(git("show", f"{base_ref}:{path}").splitlines()) or 1
rows = {}
for algo in ALGOS:
h, a, d = run(opts, path, f"--diff-algorithm={algo}")
rows[algo] = {"hunks": h, "added": len(a), "deleted": len(d)}
h, a, d = run(opts, path, "-w", "--diff-algorithm=histogram")
rows["histogram -w"] = {"hunks": h, "added": len(a), "deleted": len(d)}
_, a, d = run(opts, path, "--diff-algorithm=histogram")
moved = sum((Counter(x.strip() for x in a if x.strip()) & Counter(x.strip() for x in d if x.strip())).values())
gross = len(a) + len(d)
gross_churn = round(gross / old_lines, 2)
net_churn = round((gross - 2 * moved) / old_lines, 2)
moved_share = round(2 * moved / gross, 2) if gross else 0.0
if net_churn >= opts.max_churn:
verdict = "rewrite-like"
elif moved_share >= 0.3:
verdict = "move-heavy"
else:
verdict = "patch-like"
best = min(reversed(ALGOS), key=lambda n: rows[n]["hunks"])
return {
"file": path,
"old_lines": old_lines,
"rows": rows,
"moved_pairs": moved,
"gross_churn": gross_churn,
"net_churn": net_churn,
"moved_share": moved_share,
"fewest_hunks": best,
"verdict": verdict,
}
def main():
p = argparse.ArgumentParser()
p.add_argument("--base", default="HEAD")
p.add_argument("--head")
p.add_argument("--staged", action="store_true")
p.add_argument("--ext", nargs="*", default=[])
p.add_argument("--max-churn", type=float, default=0.5)
p.add_argument("--fail-on-rewrite", action="store_true")
p.add_argument("--json", action="store_true")
p.add_argument("--summary", action="store_true")
opts = p.parse_args()
if subprocess.run(["git", "rev-parse", "--is-inside-work-tree"], capture_output=True).returncode:
sys.exit("aidiff: run this inside a git repository")
results = [score_file(opts, f) for f in changed_files(opts)]
if opts.json:
print(json.dumps(results, indent=2))
elif opts.summary:
for r in results:
print(f"{r['file']:<10} gross {r['gross_churn']:<5} net {r['net_churn']:<5} "
f"moved {r['moved_share']:<5} {r['verdict']}")
else:
for r in results:
print(f"{r['file']} ({r['old_lines']} lines before)")
print(f" {'algorithm':<14}{'hunks':>6}{'added':>7}{'deleted':>9}")
for name, v in r["rows"].items():
print(f" {name:<14}{v['hunks']:>6}{v['added']:>7}{v['deleted']:>9}")
print(f" moved or reformatted pairs: {r['moved_pairs']}")
print(f" gross churn {r['gross_churn']} net churn {r['net_churn']} moved share {r['moved_share']}")
print(f" fewest hunks: {r['fewest_hunks']} verdict: {r['verdict']}")
if opts.fail_on_rewrite and any(r["verdict"] == "rewrite-like" for r in results):
sys.exit(1)
if __name__ == "__main__":
main()
Six ideas carry the script, and you can adapt each one for your own repo.
- One comparison, several lenses. The
runhelper asks Git for a zero-context patch with a chosen algorithm, then counts hunks, added lines and deleted lines. Zero context keeps the counts honest, since context lines never inflate them. - Gross churn is added plus deleted lines divided by the line count of the committed file. A one-line patch on a 39-line file scores about 0.03.
- Moved pairs count lines that appear in both the deleted and added sets after stripping whitespace. Reordered functions, re-indented lines and trailing-space edits all land here.
- Net churn subtracts those pairs, so it approximates how much genuinely new text the change contains.
- The verdict flags rewrite-like when net churn reaches the threshold (0.5 by default), move-heavy when at least 30 percent of changed lines are moved or reformatted, and patch-like otherwise.
- The fewest-hunks pick prefers histogram on ties, because it produced the most readable output in Step 5.
The scorer looks for structural damage after the fact, while a linter looks for style violations. If you also want automated style checks on AI output, linting AI edits for rewrite damage pairs naturally with this script. Run the lint first and the scorer second, and you will catch both kinds of problem.
Step 9: Run it on all four branches. Start with the clean patch.
python3 tools/aidiff.py --base main --head ai-patch
orders.py (39 lines before)
algorithm hunks added deleted
myers 1 1 0
minimal 1 1 0
patience 1 1 0
histogram 1 1 0
histogram -w 1 1 0
moved or reformatted pairs: 0
gross churn 0.03 net churn 0.03 moved share 0.0
fewest hunks: histogram verdict: patch-like
Every algorithm agrees, as expected for a single added line. Now the whole-file rewrite.
python3 tools/aidiff.py --base main --head ai-rewrite
orders.py (39 lines before)
algorithm hunks added deleted
myers 7 23 18
minimal 7 23 18
patience 7 23 18
histogram 7 23 18
histogram -w 7 23 18
moved or reformatted pairs: 0
gross churn 1.05 net churn 1.05 moved share 0.0
fewest hunks: histogram verdict: rewrite-like
A high gross churn here means the diff touches more lines than the original file contained, so the rewrite replaced nearly everything. Moved pairs stay at zero because the model changed the text of the lines it moved (quotes, f-strings, type hints), which also explains why -w cannot rescue this diff. The reflow is real content change, not whitespace.
Here is the full picture across all four branches. The last three columns come from the scorer and the first four from the earlier steps.
| Branch | Hunks (Myers) | Hunks (histogram) | Gross churn | Net churn | Moved share | Verdict |
|---|---|---|---|---|---|---|
| ai-patch | 1 | 1 | 0.03 | 0.03 | 0.0 | patch-like |
| ai-rewrite | 7 | 7 | Not listed | Not listed | 0.0 | rewrite-like |
| ai-reorder | 3 | 3 | Not listed | 0.13 | Not listed | move-heavy |
| ai-swap | 4 | 2 | Not listed | Not listed | Not listed | move-heavy |
The reorder and swap branches show why net churn exists. Their gross churn looks alarming, but their low net churn reveals that almost nothing new was written. A scorer based only on changed lines would punish harmless reorders and miss nothing. The two-number approach separates the cases correctly.
Step 10: Set Git Defaults and a Review Alias
Step 10: Make the better settings permanent. Four configuration lines make every plain git diff in this repo behave like the experiments above. Run them inside the project folder. Swap --local for --global if you want the same defaults in every repository on your machine.
git config diff.algorithm histogram
git config diff.colorMoved dimmed-zebra
git config diff.colorMovedWS allow-indentation-change
git config alias.review '!python3 tools/aidiff.py'
Check the result with a listing.
git config --local --list | grep -E 'diff|alias'
diff.algorithm=histogram
diff.colormoved=dimmed-zebra
diff.colormovedws=allow-indentation-change
alias.review=!python3 tools/aidiff.py
Git lowercases the key names in that listing, and that is normal. The alias begins with an exclamation mark, which tells Git to run it as a shell command from the repository root. That detail means git review works from any subfolder.
Try the alias in working-tree mode, which compares your uncommitted changes against HEAD. This is the mode you will use most, because it checks an agent’s edits before you stage them. Simulate that by checking one file out of the rewrite branch, then reverting.
git checkout ai-rewrite -- orders.py
git review --ext .py
git reset --hard
The report matches the branch comparison, ending in verdict: rewrite-like. The --ext flag restricts scoring to the file types you care about, which also keeps lockfiles and generated assets out of the report. Be careful with git reset --hard in a real repo, since it discards uncommitted work. In this demo it only removes the simulated edit.
The global option deserves one warning. A global diff.algorithm setting changes how every project diffs, including projects whose tools parse diff output. Most tools cope, but a script that expects Myers hunks could behave differently. Start with the local scope, then promote the setting once you trust it.
Steps 11 and 12: Wire the Review Into Your Cursor Workflow
Step 11: Checkpoint before every agent run, then review. The scorer compares against HEAD, so its answer is only meaningful when HEAD holds your last known-good state. Add a tiny shell function to your profile that commits everything before you hand the editor a task.
ckpt() {
git add -A && git commit -qm "checkpoint: ${1:-before agent run}" --no-verify
}
The workflow then has three beats. Run ckpt, let the agent work in the Cursor code editor, and run git review --ext .py .js in the integrated terminal before you accept anything. The checkpoint commit skips the hook with --no-verify on purpose, since it records your own state and does not need gating. The agent’s output is what the gate should judge.
If you prefer a button to a command, add a VS Code style task. Create .vscode/tasks.json with the content below, then open the Command Palette and pick Tasks: Run Task. Cursor is built on VS Code, so this standard feature should appear, but if your build does not list the task, run the same command in the terminal.
{
"version": "2.0.0",
"tasks": [
{
"label": "AI edit review",
"type": "shell",
"command": "python3 tools/aidiff.py --ext .py .js",
"problemMatcher": [],
"presentation": { "reveal": "always", "panel": "dedicated" }
}
]
}
Step 12: Install the pre-commit gate. A reminder you can forget is weaker than a gate you must step around. Save the next script as .git/hooks/pre-commit and make it executable. It scores the staged changes and blocks the commit when the verdict is rewrite-like.
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
python3 tools/aidiff.py --staged --max-churn 0.5 --fail-on-rewrite || {
echo "aidiff: rewrite-like change staged. Review it, or commit with --no-verify." >&2
exit 1
}
EOF
chmod +x .git/hooks/pre-commit
Test both outcomes on a throwaway branch, so that main stays untouched for Step 13. First stage the rewrite and try to commit.
git checkout -b hook-test main
git checkout ai-rewrite -- orders.py
git add orders.py
git commit -m "try rewrite"
echo "exit=$?"
orders.py (39 lines before)
algorithm hunks added deleted
myers 7 23 18
...
gross churn 1.05 net churn 1.05 moved share 0.0
fewest hunks: histogram verdict: rewrite-like
aidiff: rewrite-like change staged. Review it, or commit with --no-verify.
exit=1
The commit fails with exit code 1, which is the point. Reset with git reset --hard, stage the patch version with git checkout ai-patch -- orders.py, and commit again. That one succeeds quietly because the verdict is patch-like. When you finish, run git checkout main and then git branch -D hook-test to throw the test branch away.
The gate pairs well with after-the-fact inspection. If you already use a review tool, the Composer rewrite audit tool works from the other direction by auditing edits as the agent produces them. The hook catches whatever slips through when you are in a hurry, and your own judgment decides what happens next.
Step 13: Run the Whole Project End to End
Step 13: Add the batch runner and run the full check. The last file prints one summary line per branch, which makes a good smoke test for any change to the scorer. Save it as run_all.sh.
#!/bin/sh
for b in ai-patch ai-rewrite ai-reorder ai-swap; do
printf '%-11s' "$b"
python3 tools/aidiff.py --base main --head "$b" --summary
done
The runner relies on the --summary flag, which prints one compact line per scored file. Run it from the project root.
chmod +x run_all.sh
./run_all.sh
ai-patch orders.py gross 0.03 net 0.03 moved 0.0 patch-like
ai-rewrite orders.py gross 1.05 net 1.05 moved 0.0 rewrite-like
ai-reorder orders.py gross 0.49 net 0.13 moved 0.74 move-heavy
ai-swap cart.js gross 0.73 net 0.09 moved 0.88 move-heavy
If your output matches those four lines, the project works end to end. Any difference points to a typo in the scorer, so compare your copy against the listing in Step 8 line by line.
To use the project on a real repository, copy tools/aidiff.py into it, commit the file this time, and point the scorer at any pair of refs. For example, python3 tools/aidiff.py --base main --head my-feature --ext .ts .tsx --summary scores a TypeScript feature branch against main. Tune --max-churn after you collect a week of scores from your own history, because 0.5 suits small files and may be too strict or too loose elsewhere.
For a clean slate, delete the .git directory and rerun make_demo.sh. The scorer itself never writes to your repo, so cleanup is only needed for the demo branches.
Model and Plan Notes for Cursor Users in October 2026
The scorer is model-agnostic, but the model you pick affects how often you meet rewrites. Cursor’s own model picker currently lists Grok 4.7, GPT-5.6 Sol, Claude Opus 5.5, Gemini 3.1 Pro and Composer 2.5 among its options, according to the company’s site. Cursor’s release notes also record Claude Sonnet 5.5 as available, and Claude Haiku 5.5 arrived as an opt-in model you switch on in Cursor Settings under Models.
Anthropic announced Haiku 5.5 on October 7, 2026 and positions it as its fastest small model for high-volume work such as summarization and subagents. A small, fast model is a reasonable fit for boring edits where you want a patch and not a rewrite, but I did not measure any model’s rewrite rate here. Use the scorer on your own repo and let the numbers decide.
What the plan changes mean for edit habits
Cursor’s plan structure shifted in October. The X Subscription pricing page lists the new tiers, with Super at $100 a month aimed at daily agent users and Ultra at $200. Reports say legacy plans continue for existing subscribers who do not switch, and that the old $20 Pro tier is closed to new customers. Treat the billing details as reported until you read your own account page, because forum posts and trackers disagree on specifics.
The practical effect is that output tokens now deserve attention. Third-party model usage on the new plans is reportedly billed at API list prices from the first request, and users already struggle to reconcile the numbers. A forum thread dated September 30 on the Cursor community forum describes an Other Models meter at 100 percent while an export priced at published API rates rebuilt only about 65 percent of it.
The review step costs zero tokens, since it only runs Git and a Python script. It also shortens retry loops. When the gate rejects a rewrite, you can reprompt for a minimal patch before you invest time reading 41 changed lines. For a spending-side counterpart, the Cursor Composer cost guardrail tutorial builds a cap, and the two tools together cover both money and attention.
Common Pitfalls When Reviewing AI Rewrites
Pitfall 1: Judging by line count alone. The swap diff shows Myers at 12 changed lines and histogram at 16, yet histogram is clearly the one you can review. Compare hunks and structure, not only totals.
Pitfall 2: Using -w as a gate. Whitespace-blind comparison hides real edits in indentation-sensitive languages. Keep it as a reading aid, and let the gate use whitespace-sensitive numbers, as the scorer does.
Pitfall 3: Skipping the checkpoint. If HEAD still contains yesterday’s half-finished edits, the scorer mixes those with the agent’s changes and the verdict misleads. Commit a checkpoint first, every time.
Pitfall 4: Trusting one threshold everywhere. Churn is a ratio of the original file length, so a one-line change to a 5-line file already scores 0.2. Small files produce jumpy scores. Either ignore files under about 20 lines or raise the threshold for them.
Pitfall 5: Treating the verdict as a ruling. A rewrite-like label on a brand-new module or a planned refactor is expected. The gate exists to make you look, so use --no-verify when you have looked and agree.
Pitfall 6: Forgetting hooks do not travel. Everything under .git/hooks stays on your machine and never reaches a teammate’s clone. Share the gate through a tracked folder and core.hooksPath, covered in the advanced tips.
Pitfall 7: Committing the scorer in the demo. If you commit tools/aidiff.py on main after creating the branches, a plain git diff between main and any branch shows the file as removed. In a real repo you commit it first and branch afterward.
Pitfall 8: Ignoring new and renamed files. The scorer reads modified files only, using --diff-filter=M. An agent that deletes a file and recreates it elsewhere slips past, so skim git status as well.
For more developer tooling walkthroughs in the same vein, the Software section collects the editor, language and workflow guides, and several of them pair well with the review habits above.
Troubleshooting: 10 Problems and Fixes
1. git: 'review' is not a git command. The alias is missing from this repository. Rerun the git config alias.review line from Step 10 inside the project folder, or set it with --global to use it everywhere.
2. aidiff: run this inside a git repository. You ran the script from a folder that Git does not track. Change into the project root and try again. The guard exists because Git otherwise prints a long usage dump that hides the real cause.
3. fatal: ambiguous argument 'nope'. The ref you passed to --base or --head does not exist. Run git branch and copy the exact name, remembering that remote branches need the origin/ prefix.
4. The scorer prints nothing. No tracked file is modified relative to the base. New files, renames and deletions are skipped by design because the script filters on modified files. Check git status for the other cases.
5. The hook never fires. Either the file is not executable (run chmod +x .git/hooks/pre-commit), or a core.hooksPath setting points somewhere else, or the commit came from a client that passes --no-verify. Check git config core.hooksPath first.
6. python3: command not found on Windows. The Python launcher uses py -3 or plain python. Edit the alias and the hook to match, or move to WSL where python3 exists.
7. Moved lines show no color. Moved-line coloring needs color output. Add --color=always when piping, and use less -R as your pager so escape codes render. Also confirm diff.colorMoved is set in the scope you are running.
8. Patience and histogram print identical hunks. That is normal for small edits. The two algorithms diverge on files with many repeated or low-frequency lines, so identical output simply means your change was easy to diff.
9. The report includes changes you did not expect. Your baseline is stale. Commit a checkpoint before the agent runs, or pass an explicit --base that points to the last good commit.
10. Scoring feels slow on a large repo. The script makes about seven Git calls per modified file, so a hundred changed files means hundreds of processes. Narrow it with --ext, exclude lockfiles and generated bundles, or score only the files in the current task.
If none of those match your symptom, run the underlying Git command by hand. For instance, run git diff -U0 --diff-algorithm=histogram main ai-swap and count the @@ lines yourself. The scorer adds no magic, so a manual run shows exactly what it sees.
Advanced Tips for Large Repositories
Anchor the lines that must not move. Git’s --anchored option takes a piece of text and forces lines that start with it to stay put in the diff. It works well for files with a stable header, such as a license block or an import section, where a rewrite might otherwise produce a confusing alignment.
Switch to word-level diffs for prose and configuration. git diff --word-diff and --color-words show changes inside lines. They excel when an agent rewrites a sentence in documentation or a value in a long YAML line.
Keep only the patch-sized hunk. When an agent returns a rewrite that contains one good change, stage only that piece with git add -p. Then discard the noise with git restore -p or git checkout -p. You keep the useful hunk and reject the reformatting without retyping anything.
Hide formatting commits from blame. If a bulk reformat lands, list its commit hash in a .git-blame-ignore-revs file and point Git at it with git config blame.ignoreRevsFile .git-blame-ignore-revs. Blame then skips the commit and credits the real authors.
Compare two attempts on the same branch. If you reprompt the agent and amend the work, git range-diff shows how version two differs from version one. It is the cleanest way to see whether the second try actually shrank the change.
Run the scorer in CI. A pull request check can call python3 tools/aidiff.py --base origin/main --head HEAD --fail-on-rewrite. Treat failures as a request for a second reviewer, not as a block, since legitimate refactors will trip it.
Make the hook shareable
Move the hook into a tracked folder so your whole team gets it. Create .githooks/pre-commit with the script from Step 12, mark it executable, and then run the next command once per clone.
git config core.hooksPath .githooks
Add that line to your onboarding notes or a setup script, because Git cannot apply configuration from a clone automatically. That limitation is a safety feature, and the one-time step is a small price for it.
Finally, record the scores. Append the --json output of each agent run to a log file for a few weeks, and you will learn which tasks and prompts produce rewrites in your codebase. That history beats any generic advice about models, because it describes your repo, your prompts and your tolerance for churn.
FAQ: Reviewing Cursor Edits With Git Diff
Does the Cursor code editor tell me whether an edit was a rewrite or a patch? As of October 29, 2026, I found no documentation of per-edit telemetry that labels edits that way. This project infers the style from the result in your working tree, which works regardless of how the editor applied the change.
Which Git diff algorithm should I use for AI edits? Histogram is a safe default, and patience gives similar results. Both beat Myers on moved blocks, as the function swap showed with 2 hunks against 4. For small edits all four agree, so switching costs nothing.
Will this work with Claude Code, Copilot or another agent? Yes. The scorer reads Git state only, so it judges any tool’s output the same way. Run it after any agent finishes and before you stage the result.
What churn threshold should I pick? Start at 0.5 for net churn, then tune it against a few weeks of your own scores. Small files need a higher threshold because a single line moves their ratio a lot.
Does ignoring whitespace hide real bugs? It can, especially in Python, YAML and Makefiles. Use -w to read a diff and never to approve one. The scorer’s verdict uses whitespace-sensitive counts for that reason.
Can I run this in CI instead of a local hook? Yes. Call the scorer with --base origin/main --head HEAD --fail-on-rewrite in a pull request job. Treat a failure as a prompt for human review, since planned refactors will legitimately trigger it.
Does the scorer use tokens or cost money? No. It runs Git and plain Python, makes no model calls and needs no API key. That makes it a free check you can run after every single agent task.
Which Cursor plan do I need for the tutorial? Any plan, including free, because the demo simulates the edits with a script. Plan and billing details for real agent runs depend on your account, so confirm them on your own pricing page.





