Cursor Plan Mode: Catch Edit Drift, 13 Steps, 45 Min [2026]

Cursor Plan mode lets the agent research your repo, ask clarifying questions and write a plan before it touches a single file, and it is still an active workflow in Cursor. The plan is only a promise, though. Nothing in the editor checks that promise against the diff the agent produces once you press Build.

This tutorial closes that gap in 13 steps and about 45 minutes. You will write a small Python tool, plan_drift.py, that reads a machine-readable manifest from the plan, runs git diff against the commit the agent started from, and fails when the agent edits files the plan never named, blows its line budget, or rewrites most of a file it promised to patch. Earlier guides on this site audit what Cursor did after the fact. This one adds the missing half, a contract written before the first edit.

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

Why Cursor Plan Mode Still Needs a Drift Check

Cursor describes the feature in plain terms. Its help documentation says: “Plan mode lets you generate and review a detailed plan before Agent writes any code.” Its launch announcement adds that “Plan mode gives the model new tools to create and update plans, as well as an interactive editor to modify plans inline.” Both statements are accurate, and neither promises that the build step will stay inside the plan.

That is not a flaw in the product. A plan is prose, and prose is flexible. The agent can read “update the discount logic” as two lines or as a whole module, and either reading fits the sentence. The model that wrote the plan also writes the code, so it grades its own homework. Cursor’s release notes say edits to large files now fail safely instead of truncating them, and that loose one-line replacements no longer splice the wrong text into a file. Those fixes cut the worst failures. They do not tell you whether a 12-line change turned into a 200-line one.

Three kinds of drift show up in practice, and each one leaves a different fingerprint in git diff:

  • Scope drift. The agent edits files the plan never mentioned, such as a neighboring helper, a config file or a lockfile. The fingerprint is a changed path that is absent from the plan.
  • Budget drift. The agent edits the right file but changes far more lines than the plan implied. The fingerprint is a high added-plus-removed count on one path.
  • Rewrite drift. The agent regenerates most of a file, often renaming variables or reflowing code on the way. The fingerprint is a large share of the original lines touched, sometimes with whitespace-only churn on top.

If you have read our lint pass for AI edits or the edit tracer, you already know how to detect those fingerprints after the fact. The difference here is the reference point. Those tools judge the diff against generic thresholds. This one judges it against what you approved, file by file, number by number. A 60-line change is fine when the plan budgeted 80 and a violation when it budgeted 10.

The approach also keeps working as models change. Cursor’s picker now lists Composer 2.5, Grok 4.7, Claude Sonnet 5.5, GPT-5.6 variants and more, and each one edits a little differently. A contract in the plan lets you compare them on the same footing without trusting any vendor’s claim about how targeted its edits are.

What You Will Build and What You Need

The finished project is one Python file with no third-party dependencies, plus a test suite, a Cursor rule, a pre-commit hook and a GitHub Actions workflow. You need a terminal, a Git repository you can experiment in and Cursor installed. The checker never calls Cursor or any model, so it runs anywhere Python and Git run.

ComponentVersionNotes
CursorCurrent Cursor 3 build (3.23 or newer)Use a local agent, not a cloud agent, for the interactive steps
Python3.10 or newer (tested on 3.12.3)Standard library only. Dictionary merge syntax needs 3.9 or later
GitNot listedNeeds diff --numstat -z, -w and --no-renames
OSLinux, macOS, or Windows with Git Bash or WSLThe hook script is POSIX shell
GitHub ActionsOptionalactions/checkout@v4 and actions/setup-python@v5

Your Cursor build may differ from the one other guides use, and nothing in this tutorial depends on a specific build. If your plan pane behaves differently from the screenshots other guides show, jump to the troubleshooting section, because a few plan-pane quirks were still open in late October.

One cost note before you start. Running the checker is free. Running Cursor is not, and on the newer X Subscription plans, Claude, GPT and Gemini usage is billed at API list prices through prepaid credits rather than drawn from the included allowance. Every aborted build costs tokens, which is one more reason to catch drift cheaply.

Set Up the Repo, the Rule and the Plan Manifest (Steps 1-3)

Step 1: Create the project and a clean baseline

Start with an empty directory and a first commit. The baseline commit matters more than it looks. The checker compares the working tree to a git ref, and that ref is the only definition of “before the agent ran.”

mkdir plan-drift && cd plan-drift
git init
mkdir -p tests plans scripts demo .cursor/rules .github/workflows
git add -A
git commit --allow-empty -m "baseline"

Open the folder in Cursor with cursor . or through the File menu. Keep the working tree clean while you read the rest of the guide. A dirty tree makes every later number ambiguous.

Step 2: Add the always-on Cursor rule

The rule teaches the agent the manifest format so every plan ends with one. Create .cursor/rules/plan-manifest.mdc with the content below. The alwaysApply: true line in the frontmatter makes Cursor attach the rule to every request, so you do not depend on glob matching or on the agent deciding the rule is relevant.

---
description: Every plan must end with a plan-manifest block that lists files, line budgets and edit strategy
alwaysApply: true
---

# Plan manifest

When you write a plan, finish it with exactly one fenced block labelled `plan-manifest`.
The block must be valid JSON with this shape:

- `edits`: one object per existing file you will change, with `file`, `max_changed_lines`
  (your honest upper bound for added plus removed lines) and `strategy`
  (`patch` for targeted edits, `rewrite` only when most of the file must change, `delete` to remove it).
- `new_files`: glob patterns for files you will create.
- `max_total_changed_lines`: an upper bound for the whole change.

Rules while building from an approved plan:

1. Edit only the files listed in `edits` or matching `new_files`.
2. Prefer small targeted replacements. Do not rename, reorder or reformat code the plan does not mention.
3. If the work needs a file or a budget the plan does not allow, stop and ask for a revised plan.

If you want finer control over which edit style the agent prefers inside each file type, our guide to tuning diff versus rewrite behavior with Cursor rules covers that layer. The manifest rule here does something different. It does not tell the agent how to edit. It makes the agent state, in advance, how much it intends to edit.

Step 3: Define the manifest and write a sample plan

The manifest is JSON inside a fenced block labelled plan-manifest. JSON keeps the parser at four lines of standard library code, and the label lets the checker find the block in any markdown file regardless of where Cursor stores plans. Save this sample as plans/example.plan.md. It is a plan for a deliberately small change, raising a volume discount in a pricing module.

{{CODE plan-drift/plans/example.plan.md markdown}}

Read the manifest as a budget. The plan allows 8 changed lines in pricing.py, one new test file matching tests/test_*.py, 20 changed lines overall, and at most 20% of the original file touched. Those numbers are honest for a two-line edit plus a five-line test. You will tighten or loosen them per task, and the review step later in this guide explains how to sanity-check what the agent proposes.

Write the Drift Checker: Parse, Diff and Score (Steps 4-8)

The next five steps build plan_drift.py in the order the data flows. Each step shows one slice of the real file. Paste the slices into one file in order, or copy the finished version from the project section at the end. Every slice below comes straight from the tested script.

Step 4: Parse the manifest out of the plan

The first slice holds the imports, the thresholds and the manifest loader. Two design choices are worth a sentence each. The fence regex tolerates Windows line endings and trailing spaces after the label, because plans pasted from a browser often carry both. And every parsing failure exits with code 2, a different code from the code 1 that means “the diff violated the plan,” so CI can tell a broken plan from a bad build.

{{CODE plan-drift/plan_drift.py python 1 19}}

{{CODE plan-drift/plan_drift.py python 22 48}}

The git helper wraps subprocess.run and exits with code 2 when a command fails, which keeps every later function free of error plumbing. The three constants set the defaults. A file is exempt from the ratio check below 30 lines, because replacing 3 of 5 lines is not a rewrite in any meaningful sense, and a whitespace-only change of 10 or more lines raises a warning.

Step 5: Model files, findings and the report

Three small dataclasses carry the results. FileStat holds the counts for one path, and two of its properties do the real work. ratio is the larger of the added and removed counts divided by the original line count, so a full regeneration approaches 1.0 whether the agent adds or deletes more. churn is the gap between the raw line count and the whitespace-ignoring count, which isolates pure reformatting.

{{CODE plan-drift/plan_drift.py python 51 98}}

Step 6: Collect the real diff, including new files

This is the part that touches git. collect runs three commands against the base ref. The first, --name-status, tells us whether each path was added, modified or deleted. The second runs --numstat -w, which ignores whitespace and gives the meaningful change counts. The third runs plain --numstat for the raw counts. A path that appears in the raw output but not in the whitespace-ignoring output had only formatting changes, so its meaningful counts are zero. I got that wrong in my first draft by falling back to the raw numbers, and the unit tests in Step 11 caught it, so keep the (0, 0, False) default exactly as written.

All three commands pass -z so that file names with spaces or non-ASCII characters survive intact, and --no-renames so a moved file shows up as a deletion plus an addition instead of a similarity guess. For untracked files, which git diff never lists, the function asks git ls-files --others --exclude-standard and counts their lines directly. Finally it reads each file’s original line count from git show <base>:<path>.

Step 7: Evaluate the diff against the manifest

The evaluator turns counts into findings. Each rule maps to one failure mode from the first section. A changed path with no manifest entry is OUT_OF_SCOPE. A file that exceeds its own max_changed_lines is OVER_BUDGET. A file that declared strategy: "patch" but had more than the allowed share of its original lines touched is REWRITE. A total above max_total_changed_lines is TOTAL_BUDGET. A deletion the plan did not declare, or a changed binary file, is also an error. Planned files that never changed and large whitespace-only churn are warnings, since neither breaks the contract on its own.

The strategy field is the lever that keeps the checker honest rather than dogmatic. When a task really needs a regeneration, the plan says "strategy": "rewrite" and the ratio check steps aside for that file, while the line budget still applies. The agent can rewrite, but only after saying so where you can see it. If you want to understand why some edit pipelines favor full regeneration at all, our write-up on a fast-apply patch engine explains the latency trade-off behind that design.

Step 8: Render the report, log runs and wire the command line

The last slice prints a compact table, appends optional rows to a CSV for model comparisons, and ties everything to argparse. Exit codes are the interface: 0 for a clean diff, 1 for violations, 2 for a broken plan or git failure. The --staged flag swaps the working-tree comparison for the index, which is what the pre-commit hook needs. The --label and --csv flags exist so you can log which model produced each run.

{{CODE plan-drift/plan_drift.py python 189 199}}

Make it executable with chmod +x plan_drift.py, or call it as python3 plan_drift.py everywhere. That finishes the tool. The remaining steps prove it works and then point Cursor at it.

Prove the Checker Works Before You Trust It (Steps 9-11)

A gate you have never seen fail is a gate you cannot trust. Before involving a model, you will reproduce both outcomes on a throwaway repository with scripted “agents” that behave exactly like the two edit styles you care about.

Step 9: Build a sandbox repo with a patch agent and a rewrite agent

demo/simulate.py creates a repo holding pricing.py (161 lines, twenty small tier functions) and billing.py (2 lines). The patch mode changes two lines in pricing.py and adds a five-line test, which is what a disciplined agent does. The rewrite mode renames qty to quantity and base to subtotal throughout pricing.py and also edits billing.py, which is what an over-eager agent does when it decides to tidy up.

{{CODE plan-drift/demo/simulate.py python}}

Step 10: Run the clean-patch scenario

Create the sandbox, apply the patch agent, and run the checker with the sample plan from Step 3.

python3 demo/simulate.py /tmp/plan-drift-demo setup
python3 demo/simulate.py /tmp/plan-drift-demo patch
python3 plan_drift.py --plan plans/example.plan.md --repo /tmp/plan-drift-demo
echo "exit=$?"

Expected output:

FILE                              ST      +    -  ORIG  TOUCH
pricing.py                        M       2    2   161     1%
tests/test_pricing.py             A       5    0     0     0%


PASS: 0 error(s), 0 warning(s), 9 changed lines in 2 file(s)
exit=0

Nine changed lines across two files, 1% of the original pricing.py touched, exit code 0. The test file passes the tests/test_*.py glob, so it counts as an allowed new file.

Step 11: Run the rewrite scenario and the unit tests

Reset the sandbox, apply the rewrite agent and run the same command.

git -C /tmp/plan-drift-demo checkout -q . && git -C /tmp/plan-drift-demo clean -fdq
python3 demo/simulate.py /tmp/plan-drift-demo rewrite
python3 plan_drift.py --plan plans/example.plan.md --repo /tmp/plan-drift-demo
FILE                              ST      +    -  ORIG  TOUCH
billing.py                        M       1    1     2    50%
pricing.py                        M     100  100   161    62%

[ERROR] OUT_OF_SCOPE  billing.py: changed but not in the plan (2 lines)
[ERROR] OVER_BUDGET   pricing.py: 200 changed lines, plan allowed 8
[ERROR] REWRITE       pricing.py: 62% of the original 161 lines touched, limit 20%
[ERROR] TOTAL_BUDGET  *: 202 changed lines, plan allowed 20

FAIL: 4 error(s), 0 warning(s), 202 changed lines in 2 file(s)
exit=1

The run reports errors and exits with code 1. The edit to billing.py is out of scope. pricing.py changed far more lines than its budget allows, and a large share of the original file was touched, well past the limit. The combined total blows the line cap. The variable rename deserves a second look. It changed no behavior, yet it produced a large block of added and removed lines. That is the price of a gratuitous cleanup, and a tired reviewer skims past exactly this kind of change.

The table below summarizes the two scenarios, plus a third run where a script converts every four-space indent in pricing.py to a tab.

ScenarioFiles changedMeaningful lines changedShare of original touchedVerdict
Patch agent291%Pass
Rewrite agent2Not listedNot listedFail, 4 errors
Whitespace-only reindent10 (240 raw)0%Pass with FORMAT_CHURN warning

Now lock the behavior in with tests. The suite builds a fresh repo per test, so it never touches your working tree. It covers the clean pass, the failing rewrite, the exact set of error codes, the whitespace case, the untouched-file warning and the missing-manifest exit code.

{{CODE plan-drift/tests/test_plan_drift.py python}}

python3 -m unittest discover -s tests -v
test_clean_patch_passes (test_plan_drift.PlanDriftTests.test_clean_patch_passes) ... ok
test_full_rewrite_fails (test_plan_drift.PlanDriftTests.test_full_rewrite_fails) ... ok
test_missing_manifest_exits_2 (test_plan_drift.PlanDriftTests.test_missing_manifest_exits_2) ... /tmp/claude-1016/tmp76ag2x1j.md: no plan-manifest block found
ok
test_rewrite_reports_expected_codes (test_plan_drift.PlanDriftTests.test_rewrite_reports_expected_codes) ... ok
test_untouched_file_warns (test_plan_drift.PlanDriftTests.test_untouched_file_warns) ... ok
test_whitespace_only_change_is_churn_not_budget (test_plan_drift.PlanDriftTests.test_whitespace_only_change_is_churn_not_budget) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.529s

OK

All six pass in about half a second. The missing-manifest test prints its stderr message once, which is expected.

Run Cursor Plan Mode Against the Checker (Step 12)

Step 12: Plan, approve, commit the plan, build, then check

Now the real loop. Copy the rule, the checker and a copy of the sandbox files into a repo you can afford to experiment on, or point Cursor at /tmp/plan-drift-demo after copying the tool into it. Commit the tooling first. If you skip that, the checker flags the tool files themselves as out of scope, which the pitfalls section covers.

  1. Open the repository in Cursor and start a new agent chat on a local agent. Cursor’s docs say Shift+Tab toggles Plan mode from the agent input, and you can also pick it from the mode menu.
  2. Choose a model and write it down. Use the same model for the whole experiment, then repeat with another. Composer 2.5, Claude Sonnet 5.5 and Grok 4.7 are all in the picker as of this week.
  3. Send a deliberately narrow prompt, such as: Raise the tier 1 bulk multiplier and the tier 1 discount by one small step each in pricing.py, and add one test.
  4. Let the agent research and ask its questions. According to Cursor’s own guidance, the agent searches the codebase, asks clarifying questions, writes a plan with file paths and waits for your approval before building.
  5. Review the plan, and review the manifest at the bottom first. Check three things: every file in edits is one you expected, every max_changed_lines is plausible for the work described, and strategy says patch unless you agree a rewrite is needed. Edit the numbers inline if they look padded.
  6. Save the plan into the workspace as plans/current.plan.md. If your build has a save-to-workspace button, use it. If not, copy the plan text into that file by hand.
  7. Commit the plan before building: git add plans && git commit -m "approved plan". This makes the plan part of the base, so it does not show up as an untracked change.
  8. Switch to Agent mode, tell it to build from the plan, and wait for it to finish. Do not commit the result yet.
  9. Run the checker: python3 plan_drift.py --plan plans/current.plan.md --label composer-2.5 --csv runs.csv.
  10. On PASS, review the diff as usual and commit. On FAIL, paste the report into the chat, ask for a revised minimal edit, or run git restore . and tighten the plan.

Cursor’s help documentation describes the plan as something you review before the agent writes code, so step 5 is the human half of the contract and step 9 is the machine half. Neither replaces the other. For a deeper look at how Composer decides between patching and regenerating, see our Composer diff-versus-rewrite setup guide, and for raw speed numbers across models, the CLI benchmark is the right companion.

Repeat the loop for each model you care about, with the same prompt and a fresh reset between runs. The CSV gets one row per run with the file count, changed lines, error count and error codes. After five or six runs you will see patterns that no benchmark headline shows you, for example which models routinely add a helper file nobody asked for. I am deliberately not publishing sample numbers for specific Cursor models here, because the demo above uses scripted agents and the real results depend on your repo, prompts and settings.

Gate Every Commit and Pull Request (Step 13)

Step 13: Install the pre-commit hook and the CI job

Local discipline wears off, so enforce the contract mechanically. The hook below does nothing when no plan exists, so it never blocks unrelated work. When plans/current.plan.md is present, it checks only the staged changes.

{{CODE plan-drift/scripts/pre-commit sh}}

cp scripts/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

With the hook installed and the rewrite scenario staged, a commit stops cold. This is the real output from the sandbox after committing the tooling first.

FILE                              ST      +    -  ORIG  TOUCH
billing.py                        M       1    1     2    50%
pricing.py                        M     100  100   161    62%

[ERROR] OUT_OF_SCOPE  billing.py: changed but not in the plan (2 lines)
[ERROR] OVER_BUDGET   pricing.py: 200 changed lines, plan allowed 8
[ERROR] REWRITE       pricing.py: 62% of the original 161 lines touched, limit 20%
[ERROR] TOTAL_BUDGET  *: 202 changed lines, plan allowed 20

FAIL: 4 error(s), 0 warning(s), 202 changed lines in 2 file(s)
exit=1

Nothing was committed, and git log still shows only the baseline and the tooling commit. To commit anyway after a legitimate plan change, update the plan, re-commit it and try again. Bypassing with --no-verify defeats the point, so avoid it.

For pull requests, add the workflow below. It fetches full history so git merge-base works, finds the newest plan in plans/, runs the checker with JSON output and then runs the unit tests. A failed check blocks the merge once you mark the job as required in branch protection.

{{CODE plan-drift/.github/workflows/plan-drift.yml yaml}}

Note the || STATUS=$? pattern. It captures the checker’s exit code without aborting the shell, prints the JSON for the log, and then exits with the saved code. Without it, a failing check would end the step before the report was printed.

How to Read Each Finding Code

Every finding has a code, a level and a message. Treat errors as blockers and warnings as prompts for a second look.

CodeLevelTriggered whenFirst fix to try
OUT_OF_SCOPEErrorA changed path is not in edits and does not match new_filesRevert that file, or revise the plan if the change is justified
OVER_BUDGETErrorOne file changed more lines than its max_changed_linesAsk for a smaller edit, or raise the budget in a re-approved plan
REWRITEErrorA patch file had more than the allowed share of its original lines touchedReject the output, or change the strategy to rewrite with review
TOTAL_BUDGETErrorAdded plus removed lines across all files beat max_total_changed_linesSplit the task into two plans
DELETEDErrorA file was removed without strategy: "delete"Restore the file with git restore
BINARYErrorA binary file changedConfirm intent, then declare it in a revised plan
UNTOUCHEDWarningA planned file did not changeCheck whether the agent skipped a step
FORMAT_CHURNWarning10 or more lines changed only in whitespaceAdd a formatter step, or fix line endings

Common Pitfalls With Cursor Plan Mode and Edit Budgets

These are the mistakes that waste the most time. Each one comes from a failure mode that is easy to hit on a first run.

Pitfall 1: Padding the budget until it never fails. If every plan says max_changed_lines: 500, the gate passes everything and teaches you nothing. Set the budget to roughly twice what you expect, and make the agent justify more.

Pitfall 2: Letting the agent approve its own manifest. The model that wrote the plan has every incentive to write numbers it can meet. You are the reviewer. Skim the manifest first, because it takes ten seconds and sets the limits for everything else.

Pitfall 3: Building on a dirty tree. Uncommitted edits you made before the agent ran will show up as agent drift. Commit or stash first, every time, then record the base.

Pitfall 4: Forgetting to commit the plan and the tooling. Untracked files count as additions. An uncommitted plans/current.plan.md or plan_drift.py appears as OUT_OF_SCOPE, which reads like an agent mistake when it is yours.

Pitfall 5: Treating lockfile and generated-file changes as noise. A single dependency bump can rewrite thousands of lines in a lockfile. Either list the lockfile in edits with an explicit, large budget and strategy: "rewrite", or keep dependency changes out of agent tasks entirely.

Pitfall 6: Ignoring retry cost. A failed 200-line rewrite is not free. Each attempt spends model tokens, and that matters more now that third-party models on the newer X Subscription tiers bill at API list prices. Our Composer cost guardrail shows how to cap that spend, and catching drift in seconds with a local script avoids paying for a second attempt you did not need.

Pitfall 7: Expecting rename detection. The checker passes --no-renames, so a moved file appears as a delete plus an add. That is on purpose, because similarity matching hides how much the agent actually changed. If a task involves a move, declare both paths in the plan.

Troubleshooting: 10 Problems and Fixes

1. “no plan-manifest block found.” The fence label is missing, misspelled or indented. The line must start with three backticks followed by the word plan-manifest and nothing else. Check that your editor did not swap the backticks for tildes.

2. “manifest is not valid JSON.” Trailing commas and comments are the usual culprits. Validate quickly with python3 -m json.tool on the block’s contents.

3. Everything shows as OUT_OF_SCOPE, including your tooling. You did not commit plan_drift.py, the rule and the plan before the build. Commit them, then rerun with --base HEAD.

4. ORIG shows 0 for a file that clearly exists. The base ref does not contain that file, or --base points at the wrong commit. In CI this also happens with shallow clones, so set fetch-depth: 0.

5. Build output or node_modules inflates the counts. Untracked files count unless Git ignores them. Add them to .gitignore, because the checker uses --exclude-standard.

6. A huge FORMAT_CHURN warning on Windows. Line-ending conversion is rewriting files. Set core.autocrlf consistently across your team and keep a .gitattributes file with explicit eol rules.

7. The plan pane is empty, or the plan will not populate. Reports from October describe plan-pane bugs and a separate Build model picker. Copy the plan from the chat panel into plans/current.plan.md by hand. The checker only needs the markdown.

8. Plan mode is missing in a cloud agent. Reports from October say cloud agents did not consistently expose a selectable Plan mode. Run this workflow with a local agent until that settles.

9. The agent ignores the manifest rule. Confirm alwaysApply: true is in the frontmatter, start a fresh chat so the rule loads, and make sure the file lives at .cursor/rules/plan-manifest.mdc. If the plan comes back without a manifest, ask for one in the same chat, then check that it is valid.

10. The pre-commit hook never runs. It must be executable and named exactly pre-commit inside the hooks directory that Git actually uses. Run git config core.hooksPath to check. On Windows, save the hook with LF line endings, or Git Bash will choke on a carriage return after the shebang.

Advanced Tips for Teams

Once the loop works, a few upgrades pay off quickly.

Compare models on equal terms. Run the same plan against several models and append every result to one CSV with --label. A pivot of error codes by model shows which ones drift toward scope creep and which ones drift toward reformatting. Because every run starts from the same committed plan, the comparison is fair in a way that screenshots are not.

Pair an expensive planner with a cheaper builder. Cursor’s September release reportedly added support for pairing a more expensive planning model with a cheaper implementation model through subagents. A tight manifest is what makes that split safe, since the cheap builder has hard limits to respect. Pricing also moved in October, and one trade report says the entry tier is now $8 a month, so check your plan’s current terms before assuming old cost math still holds.

Feed the report back. The most effective retry prompt is the checker’s own output. Paste the table and findings into the chat with a short instruction:

The build violated the approved plan. Revert every change outside pricing.py,
then redo the edit as a minimal patch of at most 8 changed lines.
Do not rename or reformat anything. Report: <paste plan_drift output>

Use the JSON for pull request comments. The --json output includes every file’s counts and every finding, so a short script can post a summary on the pull request. Reviewers then see the plan-versus-diff story without running anything.

Add rollback. A drift gate tells you something went wrong. It does not undo it. Pair it with the one-command rollback and pre-apply baseline from our Fast Apply safety net guide so a failed build costs you seconds. More tutorials in this vein live in the software section.

Tighten per directory. The sample uses one global ratio. Real repos deserve stricter limits on core modules and looser ones for tests and docs. Adding glob matching for edits entries is a ten-line change using fnmatch, the same helper already imported for new_files.

The Complete Working Project

The finished tree has seven files. Everything above is the full source, so assembling it takes a few minutes.

plan-drift/
  plan_drift.py                    237 lines, the checker
  plans/example.plan.md            sample plan with a manifest
  tests/test_plan_drift.py         six unit tests
  demo/simulate.py                 sandbox builder with patch and rewrite agents
  scripts/pre-commit               git hook
  .cursor/rules/plan-manifest.mdc  always-on rule
  .github/workflows/plan-drift.yml CI job

Verify your assembly with three commands. If all pass, your copy matches the tested one.

python3 -m unittest discover -s tests
wc -l plan_drift.py
python3 plan_drift.py --plan plans/example.plan.md

You should see the tests pass and, on a clean tree, a report with one UNTOUCHED warning for pricing.py and exit code 0. The tool makes a few git calls plus one git show per changed tracked file, and it never touches the network.

FAQ

Does Cursor Plan mode guarantee the agent will follow the plan?

No. Plan mode produces a reviewable plan and waits for your approval, but the build step is still a model generating edits. The plan guides it and does not constrain it. That is the gap this checker covers.

Do I need Cursor for this to work?

No. The checker reads a markdown file and a git diff. Any agent, including a human, can be held to a manifest. The Cursor rule only makes the manifest appear automatically at the end of each plan.

Why JSON instead of YAML for the manifest?

Python ships a JSON parser and no YAML parser. Keeping the project dependency-free means it runs in CI and in locked-down environments without a pip install step.

What touch ratio should I set?

Start at the default threshold and lower it for focused fixes like the example. Raise it only for a refactor, and when you do, say so by setting the file’s strategy to rewrite.

Why does the checker count added and removed lines separately?

A replacement of one line shows up as one addition and one removal. Using the larger of the two for the ratio treats that as one touched line, and it keeps a pure insertion from looking like a rewrite.

Will it catch semantic changes hidden in a small diff?

No. It measures size and scope, not meaning. A two-line change can still break production. Keep your tests and code review. The checker removes the cheap-to-detect failures so review time goes to the hard ones.

Can I change the plan after the agent starts building?

You can, but treat it as a new contract. Stop the build, update the manifest, commit the revised plan and rerun the check against the new base. Editing the budget after seeing a failure defeats the purpose.

Marcus Chen

Marcus Chen

Gaming & Consumer Tech Editor

Marcus Chen is a senior editor at Tech Insider, where he leads coverage of the US online gaming market, including sweepstakes and social casinos, alongside consumer technology. He evaluates operators on their published terms, licensing and RNG certifications, stated redemption policies, and corroborating independent reporting, and writes plainly about what the evidence supports. Tech Insider does not run first-party money tests and does not gamble with reader funds. Marcus has reported on the technology and online-gaming industries for more than a decade.

View all articles