← All articles

Articles

Plan Before Coding With AI: Review Changes in VS Code

Last month a freelancer I know asked an AI tool to "add rate limiting to the login route." Eleven minutes later he had a diff touching 23 files, a new dependency, a renamed config module and a rewritten test setup. He did not accept it. He did not reject it either, because he could not tell which of the 23 changes mattered. He closed the tab and did the work by hand.

That is the failure this article is about. The problem is rarely the code the AI writes. The change arrives before anyone agreed on what it should be.

The fix is boring and it works: plan before coding with AI. Ask for a written plan first, review it like you would review a teammate's proposal, and only then let any file change. In this walkthrough we take one small change in an existing Express project, read the code, write acceptance criteria, review the plan, compare a bad plan with a good one, and implement in steps. You can run the same flow in VS Code, Cursor or any VS Code-based IDE. A reusable planning prompt is at the end of the fourth step.

Why a plan comes before the first line of AI-written code

A diff is a bad place to discover your misunderstanding. By the time code exists, you are reviewing implementation details, variable names and import order, and the question that actually matters ("is this the right change?") gets buried.

A plan moves that question to the front. It is a few hundred words, not a few hundred lines. You can read it in two minutes, spot the wrong assumption, and fix it with one sentence instead of a rollback.

There is a second reason, and it only applies when you work alone. Indie hackers and freelancers have no reviewer. Nobody else will catch the 23-file diff. The plan is your review step, and it is the cheapest one you will ever get.

OxCode builds this workflow in: it has a Plan task mode alongside Agent, Debug and Security, and it reads your codebase before it touches it. The plan is grounded in your actual files, not in a generic guess about how Express apps look. The same habit works with any tool, though. The discipline is yours.

Here is the rule we will follow for the rest of this post: no file changes until the plan has been read, questioned and approved. If the plan is vague, the code will be broad. If the plan is bounded, the diff will be small.

Our sample task is deliberately small: add a request-rate limit to a POST /login route in an existing Express and Jest project. Small tasks are where the habit forms. Nobody builds the habit on a rewrite.

Step 1: Inspect the existing code before you describe the change

Do not write the task description first. Open the code first. Ten minutes of reading saves an hour of correcting a plan built on a wrong picture.

For the login rate-limit task, read four things:

  • The route file, to see how POST /login is wired and what middleware already runs before it.

  • The app entry file, to see where middleware is registered globally.

  • The existing login test file, to see the testing style (Jest with supertest, in our case).

  • package.json, to see what is already installed.

Write down what you find in plain sentences. Ours looked like this: "Middleware is registered per-route in routes/auth.js. There is no global rate limiter. Tests use supertest against an exported app. Redis is not installed. Sessions are cookie-based."

That last note matters. If Redis is not installed, any plan that says "store counters in Redis" has just added infrastructure to a one-route change.

Now ask your AI tool to do the same reading and report back before proposing anything. In OxCode you can use Plan mode for this: ask it to list the files it read and what it learned. Compare its summary with yours. If it missed the per-route middleware pattern, correct it now. A wrong assumption caught here costs one message. Caught after implementation, it costs a revert.

This is also where you decide what is out of bounds. Our list: do not touch the session logic, do not rename anything, do not change the test setup. Writing exclusions down is the most underrated part of scoping.

If you want a closer look at what goes wrong when you skip this reading, our guide on how to fix AI-generated code without breaking existing tests covers the failure patterns.

Step 2: Write acceptance criteria the plan must satisfy

A plan without acceptance criteria is a wish. Criteria turn "add rate limiting" into something you can check with a yes or a no.

Write them before you see the plan, so the plan has to meet your bar instead of you bending your bar to fit the plan. For our task:

  1. POST /login returns 429 after 5 failed attempts from one IP within 15 minutes.

  2. A successful login does not count toward the limit.

  3. The 429 response body matches the existing error format: { "error": "..." }.

  4. No other route changes behaviour.

  5. All existing tests still pass, and new tests cover criteria 1 to 3.

  6. No new infrastructure (no Redis, no database tables).

Look at criteria 4 and 6. They are the guardrails, not the feature. Most oversized AI diffs happen because nobody stated what must not change.

Keep each criterion testable. "Login should be more secure" is not a criterion. "Returns 429 after 5 failed attempts" is. If you cannot imagine the test, rewrite the line.

Keep the list short too. Six lines is plenty for a one-route change. If you find yourself writing fifteen, the task is really three tasks. Split it and plan each one separately.

A practical tip for freelancers: paste these criteria into the client ticket or your own task note as well. When the client later asks "what exactly did you change?", the criteria and the plan are your answer. They also make your billable hours easier to justify, because the scope is written down before the work starts.

Now hand the criteria to your AI tool along with the task, and ask for a plan only. Say it outright: do not modify any files yet.

Step 3: Review the proposed files and dependencies

When the plan comes back, read it in a fixed order. Do not start with the clever parts. Start with the footprint.

Files first. Count them. For a one-route change, expect two to four: the route or middleware file, a new test file or additions to an existing one, and maybe package.json. Any file you did not expect needs a stated reason. If a plan says it will "refactor the auth module for clarity", that is scope creep wearing a helpful hat. Strike it.

Dependencies second. Every new package is a permanent decision: licence, maintenance, bundle weight, supply-chain risk. For rate limiting, a small well-known package may be a fair choice, or a 20-line in-memory limiter may be enough for a single-process app. Make the plan justify the choice against your criterion 6. If it proposes something that needs new infrastructure, send it back.

Order third. A good plan writes the failing tests first or at least names them, then the implementation. If tests are an afterthought at the bottom of the plan, ask for them to move up.

Risks last. Ask the plan to name what could break. A useful answer sounds like "in-memory counters reset on restart and are not shared across multiple instances." That is a real limitation you can accept or reject. A useless answer is "minimal risk."

Mark up the plan with edits, not vibes: "Remove step 4. Use the in-memory approach. Do not touch config/index.js." Then ask for a revised plan.

This review is where OxCode's approval-first design fits naturally. You approve commands instead of having them fired off unseen, so the plan you approved and the actions that follow can be checked against each other. Whatever tool you use, make that comparison a habit.

Only after the footprint is small and the reasons are clear do you say the words: "Approved. Implement step 1 only."

Overbroad plan vs bounded plan: the same task, two versions

Same task, same codebase, same six criteria. Here are two plans the tool might return. You reject one and approve the other.

Version A: the overbroad plan

  • Install express-rate-limit, rate-limit-redis and ioredis.

  • Create a new middleware/ directory and move existing auth middleware into it.

  • Add a global rate limiter on all routes.

  • Refactor routes/auth.js into controller and service layers.

  • Update the Jest config to support Redis mocks.

  • Update README with deployment notes.

Six bullets, roughly fifteen files, three new dependencies and one piece of infrastructure. It breaks criterion 4 (all routes change), criterion 6 (Redis) and the exclusion on test setup. It also sounds productive, which is why people approve it.

Version B: the bounded plan

  • Add middleware/loginLimiter.js (new, about 25 lines): in-memory map keyed by IP, 5 failures per 15 minutes, resets on success.

  • Register the limiter on POST /login only in routes/auth.js (one line plus one import).

  • Add tests/loginLimiter.test.js (new): five failures return 429, a success does not count, the error body matches the existing format.

  • No new dependencies. No other files touched.

  • Known limitation: counters are per-process and reset on restart.

Three files, zero dependencies, every line traceable to a criterion.

The tell is simple. Version A talks about the codebase. Version B talks about the task. When a plan spends words on things you did not ask about, the diff will too.

Version B is also easier to trust. If you are the only reviewer, a three-file diff is something you can actually read. A fifteen-file diff is something you skim and hope about.

The habit to build: whenever a plan includes the word "also", stop and ask whether the task needs it.

Step 4: Define the tests, then implement in small steps

With Version B approved, resist the urge to say "go." Define the tests first, then implement in slices you can verify one at a time.

Slice 1: tests only. Ask for the three test cases from the plan, and run them. They should fail, and they should fail for the right reason (route returns 200, not an import error). A test that fails for the wrong reason proves nothing.

Slice 2: the limiter. Implement loginLimiter.js alone. Run the new tests. Review a 25-line file, which takes a minute.

Slice 3: the wiring. Register the middleware on POST /login. Run the whole suite, not just the new tests. Criterion 5 says existing tests must pass, and only a full run proves it.

After each slice, check the diff against the plan. Same files, same scope. If anything extra appears, stop and ask why.

This is where an agent that runs its own work earns its keep. OxCode runs what it writes in a sandbox and tries to catch and fix its own bugs before calling a task done, which shortens the loop between slices. Our post on an AI coding agent that runs tests shows what evidence to look for when any tool claims the tests passed.

Here is the reusable planning prompt. Paste it, fill the brackets:

Task: [one sentence].

Before changing any file:
1. List the files you read and what you learned about how this project does [relevant pattern].
2. Propose a plan. For each step give: file, what changes, which acceptance criterion it satisfies.
3. List every new dependency and why a no-dependency option is not enough.
4. Name the tests you will write first.
5. List known risks and limitations.

Acceptance criteria:
[numbered, testable]

Out of bounds (do not touch):
[files, modules, renames, config]

Do not modify any file until I reply "approved".
After approval, implement one step at a time and stop after each for review.

That last line is the one that saves you. One step at a time keeps every diff reviewable.

What to do next: debugging and regression-testing guides

A reviewed plan reduces surprises. It does not remove them. Something will still fail in slice 3 one day, and the quality of your next move depends on how small the change was.

Two follow-ups are worth your time. When a test goes red, work through how to debug AI-generated code in VS Code before you ship it. It covers reading failures before asking for a fix, which pairs well with a plan because you already know what the change was supposed to do.

For regressions, the plan's acceptance criteria are your checklist. Turn each one into a test that stays in the suite. Criterion 4 ("no other route changes behaviour") is the one people forget, and it is the one that bites three weeks later. A single test that hits two unrelated routes and asserts they are unaffected costs ten lines and covers it.

If you are building this habit across several projects, keep the planning prompt in a snippet file and the criteria in the ticket. Within a week it stops feeling like overhead. Within a month you will notice you write the exclusion list first, because that is where the damage happens.

One last note on cost. Planning uses a request, and so does each approved step. On a plan with a daily run allowance, a bounded plan with three small steps is easier to budget than one giant open-ended prompt that needs four rounds of correction. Small scope is cheaper as well as safer.

Conclusion

Pick the next ticket you were about to hand to an AI tool and do not type the task description yet. Read the route, the entry file and one test file. Write six acceptance criteria and an out-of-bounds list. Then ask for a plan and count the files before you read anything else. If the count is higher than you expected, the plan is wrong, not your estimate.

Install OxCode from the VS Code Marketplace, switch to Plan mode, and run exactly this flow on a real change in your project. The Free plan gives you 15 runs a day, which is enough to try the full loop of plan, review and stepwise implementation. My prediction: after three bounded diffs you will stop accepting any plan that touches files you did not name.