The 2025 Stack Overflow Developer Survey captured the mood around AI coding tools well. 84% of respondents said they use or plan to use AI tools in their development process, and about half of professional developers use them daily. Yet only around 3% said they highly trust the output, and 46% said they actively distrust its accuracy. The most common complaint, named by 66%, was “AI solutions that are almost right, but not quite.”
That gap between adoption and trust is the whole challenge. “Almost right” code is arguably worse than obviously wrong code, because it survives a quick glance and fails later. The workflow below is designed around one goal: get the speed of an LLM without letting “almost right” into your main branch.
Step 1: Give the model a standing brief
Most bad AI output comes from missing context, not from a weak model. The model doesn’t know your framework version, your folder conventions or the library your team banned last year. So write it down once, in a file the tool reads automatically.
Most tools support this now: CLAUDE.md for Claude Code, AGENTS.md for several agents, .cursor/rules for Cursor and .github/copilot-instructions.md for Copilot. A useful one is short and specific:
# Project brief
- Angular 21, standalone components, signals for state. No NgModules.
- Backend: NestJS 11, PostgreSQL via TypeORM. Never write raw SQL in services.
- Tests: Jest for the API, Vitest for the frontend. Every bug fix needs a test.
- Commands: npm run lint, npm test, npm run build (run all three before finishing).
- Don't add new dependencies without asking.
- Errors: throw domain errors from src/common/errors, never plain Error.
Update it every time you correct the same mistake twice. Over a few weeks it becomes the most valuable prompt you own.
Step 2: Plan before any code gets written
The biggest quality jump comes from separating thinking from typing. For anything bigger than a one-line change, start with a request like:
Read the files involved in user registration. Don’t write code yet. Explain how it currently works, then propose a plan for adding email verification, including which files change, new database fields, edge cases and how we’ll test it.
Then read the plan properly. This is where you catch the wrong assumptions: “it’s going to store tokens in plain text”, “it wants a new library we don’t need”, “it forgot the existing rate limiter”. Fixing a plan takes a minute. Fixing 400 lines of code built on a bad plan takes an afternoon.
For bigger features, ask the model to save the agreed plan to a markdown file. That gives you a checkpoint to return to if the implementation goes sideways, and a record for reviewers.
Step 3: Implement in small, reviewable pieces
Ask for one step of the plan at a time: the migration, then the service method, then the controller, then the UI. Small diffs are easy to review. A 1,500-line change produced in one go is not reviewed. It’s skimmed, and skimming is how “almost right” gets merged.
A good rhythm is: implement one step, run the tests and linter, review the diff, commit, then move on. Git gives you cheap save points, so use them. If a step goes badly, git reset and try again with a better prompt rather than asking the model to patch its own mess repeatedly.
Step 4: Make tests the contract
Tests turn “looks right” into “is right.” Two patterns work especially well with LLMs.
Test first. Describe the behaviour, have the model write the tests, check that they fail for the right reason, then ask for the implementation. The tests are small enough for you to review carefully, and they then check the larger implementation for you.
Bug reproduction first. For a bug, ask for a failing test that reproduces it before any fix. If the model can’t write a failing test, it probably doesn’t understand the bug yet, and neither may you.
// Example: reproduce before fixing
it('rejects expired verification tokens', async () => {
const token = await createToken({ expiresAt: minutesAgo(5) });
await expect(service.verifyEmail(token.value))
.rejects.toThrow(TokenExpiredError);
});
Be careful with one trap: models under pressure to make tests pass sometimes “fix” the test instead of the code. Put a rule in your brief (“never weaken or delete existing tests to make them pass”) and check test diffs as closely as code diffs.
Step 5: Review like the author is a smart new hire
Treat AI-generated code the way you’d treat code from a capable developer who joined last week: probably competent, missing context, and occasionally very confident about something wrong. When reviewing, look specifically for:
- Invented APIs. Functions or options that sound right but don’t exist in the version you use. Check anything unfamiliar against the real docs.
- Security shortcuts. Missing authorisation checks, string-built SQL, secrets in code, overly permissive CORS, disabled certificate checks “to make it work”.
- Silent error handling. Empty
catchblocks and default values that hide failures. - Scope creep. Unrequested refactors, renamed variables and “improvements” in files that weren’t part of the task.
- Duplicate logic. A new helper written because the model didn’t see the existing one.
Step 6: Use the model for the tedious parts you usually skip
Some of the best uses of LLMs aren’t writing features at all:
- writing the tests you’d otherwise leave for later,
- explaining an unfamiliar legacy module before you change it,
- drafting docstrings, READMEs and migration notes,
- generating realistic test data and fixtures,
- first-pass code review of your own PR before a human sees it.
These are low-risk, high-value tasks where “almost right” is easy to spot and fix.
Step 7: Know when to stop prompting
If you’ve asked for the same fix three times and it’s still wrong, stop. Either the model is missing context you haven’t provided, or the problem needs actual thought that you’re better placed to do. Read the code yourself, figure out what’s really going on, then give the model a precise instruction, or just write it.
Choosing tools
The workflow matters more than the brand. Roughly, though:
- Inline completion (Copilot, Cursor’s autocomplete) is best for small, local edits where you’re typing anyway.
- Chat in the editor suits explanations and targeted changes to a file or two.
- Agentic CLI tools (Claude Code, Codex CLI, Gemini CLI, Aider) suit multi-file tasks where the model needs to run tests and iterate. They’re powerful, so they need the planning and small-step discipline above most of all.
The short version
Brief the model once. Make it plan before it codes. Take small steps. Let tests be the judge. Review everything like it came from a new colleague. None of this is new engineering practice. It’s the same discipline that makes human teams work, applied to a very fast collaborator who never gets tired and never quite knows your codebase as well as you do.