why your AI coding agent keeps getting it wrong (and how to fix it)
At some point, every developer using AI coding tools has this experience: the AI is clearly smart, clearly capable — and keeps doing something wrong. The same mistake, over and over. You correct it, it does it again in the next session.
This is maddening. It’s also almost always a configuration problem, not a model problem.
The AI isn’t stupid. It’s operating without context you haven’t provided. Here’s how to diagnose the most common categories of systematic mistakes and fix them at the source.
Mistake 1: Using the wrong framework or library
The symptom: You’re building with React 19 server components and the AI keeps suggesting useState for data fetching, or you’re using Prisma and it generates raw SQL queries, or you use Tailwind and it adds inline styles.
Why it happens: The AI knows many ways to accomplish any task. Without explicit guidance, it defaults to the most common patterns in its training data — which reflect the past, not your current stack choices.
The fix: Be explicit and specific in your AGENTS.md or equivalent:
# Stack — be specific
- React 19 with Server Components (not client components by default)- Next.js 15 App Router (NEVER Pages Router)- Tailwind CSS (NO CSS modules, NO styled-components, NO inline styles)- Prisma with PostgreSQL (NO raw SQL queries)- Zod for runtime validation (NOT Yup, NOT Joi)The fix for this class of mistake is almost always: name the specific libraries you use and explicitly exclude the alternatives.
Mistake 2: Ignoring existing patterns
The symptom: You have a well-established pattern for how you handle errors, or structure API responses, or name components — and the AI invents a new pattern instead of following yours.
Why it happens: The AI doesn’t know your patterns exist unless you show them. It defaults to patterns that are common in its training data.
The fix: Show, don’t just tell. Include concrete examples of your patterns:
# API response format
Every API response follows this exact shape:
```typescripttype ApiResponse<T> = { data: T | null; error: { message: string; code: string } | null; meta?: Record<string, unknown>;};Example of a correct endpoint:
export async function GET(request: NextRequest) { try { const users = await getUsers(); return NextResponse.json({ data: users, error: null }); } catch (err) { return NextResponse.json( { data: null, error: { message: "Failed to fetch users", code: "FETCH_ERROR" } }, { status: 500 } ); }}Never return a different shape. Never throw from route handlers.
One concrete example in your config is worth ten sentences of abstract description.
## Mistake 3: Missing error handling
**The symptom:** The AI writes `await db.query(...)` without try/catch. Or makes an API call without handling the failure case. Or reads a file without checking if it exists.
**Why it happens:** Error handling adds length to code. The AI often optimizes for the happy path because that's what makes demonstrations clean.
**The fix:** Make error handling a mandatory criterion for "done":
```markdown# Definition of done
No code is complete until:- All async operations have try/catch or use a result type pattern- All external API calls handle failure cases explicitly- All user inputs are validated before use- Error messages are logged appropriately (use our logger, not console.log)And list your specific error handling pattern:
# Error handling
We use a Result pattern for expected failures:```typescripttype Result<T> = { ok: true; value: T } | { ok: false; error: string };Reserve try/catch for unexpected failures that should be logged. Never silently swallow errors.
## Mistake 4: Wrong architectural layer
**The symptom:** The AI puts a database query inside a React component. Or puts business logic in a route handler. Or puts validation in the UI layer.
**Why it happens:** The AI doesn't know your architectural boundaries unless you document them explicitly.
**The fix:** Document where things belong:
```markdown# Architecture boundaries
## Database- Database queries ONLY in `lib/db/*.ts`- Never query the database from components, route handlers, or server actions directly- Always go through the `lib/db/` abstraction layer
## Business logic- Business logic goes in `lib/[domain]/`- Route handlers and server actions call into lib/, they don't contain logic
## Validation- Input validation in the layer closest to the user (server actions, route handlers)- Never validate in components- Always use Zod schemas from `lib/validations/`Mistake 5: Generating code that doesn’t pass linting
The symptom: The AI writes code that looks right but has 20 ESLint errors — unused variables, wrong quote style, missing return types, banned patterns.
Why it happens: The AI doesn’t know your linting rules unless you tell it, and it’s not running your linter on its output.
The fix: Tell it to. And tell it your key rules:
# Linting requirements
Before considering any code complete:- Run `pnpm lint` and fix ALL errors (not just warnings)- Run `pnpm check` for TypeScript errors
Key rules in our ESLint config:- No `any` type — TypeScript strict mode is on- No unused variables- No `console.log` — use our logger from `lib/logger.ts`- Prefer const over let- Arrow functions for callbacks and utility functionsMistake 6: Generating tests that don’t actually test anything
The symptom: The AI writes tests with expect(result).toBeDefined() — tests that pass regardless of behavior. Or tests that mock everything and assert nothing meaningful.
Why it happens: The AI writes tests that look like tests without deeply reasoning about what behavior is being verified.
The fix: Document your testing philosophy with examples:
# Testing principles
Tests must verify behavior, not implementation.
Bad test (tests nothing):```typescriptit("should work", async () => { const result = await getUser("123"); expect(result).toBeDefined();});Good test (verifies actual behavior):
it("returns null for non-existent user", async () => { const result = await getUser("non-existent-id"); expect(result).toBeNull();});
it("returns user with correct shape for valid id", async () => { const user = await getUser(testUserId); expect(user).toMatchObject({ id: testUserId, email: expect.stringContaining("@"), createdAt: expect.any(Date), });});Every test must: set up a specific scenario, call the function under test, assert a specific expected outcome.
## Mistake 7: Not following naming conventions
**The symptom:** The AI creates `fetchUser` when your codebase uses `getUser`, or `UserCard` when your convention is `UserCardComponent`, or `user-service.ts` when your convention is `userService.ts`.
**Why it happens:** Naming conventions aren't obvious from code context. The AI invents names based on general patterns.
**The fix:** Be explicit about naming in every category:
```markdown# Naming conventions
Functions:- Database queries: `get[Entity]`, `get[Entity]By[Field]`, `list[Entities]`- Mutations: `create[Entity]`, `update[Entity]`, `delete[Entity]`- Utilities: verb + noun (`formatDate`, `parseConfig`, `validateEmail`)
Files:- Components: PascalCase (`UserCard.tsx`, `AuthButton.tsx`)- Utilities: camelCase (`dateUtils.ts`, `stringHelpers.ts`)- Types: camelCase with `.types.ts` suffix (`user.types.ts`)
Variables:- Boolean flags: `is`, `has`, `should` prefix (`isLoading`, `hasError`, `shouldRedirect`)- Arrays: plural nouns (`users`, `items`, `filteredResults`)Diagnosing your specific mistakes
When the AI makes a systematic mistake, the diagnosis process is:
- Identify the pattern. Is this a one-off or does it keep happening?
- Ask: what context is missing? The AI can only work with what you’ve told it.
- Write the specific rule. Not vague guidance — a specific, verifiable instruction.
- Test the rule. Ask the AI to do the same task again and see if it applies the new guidance.
- Refine if needed. If the mistake still happens, the instruction is too vague or missing something.
The feedback loop between “AI made a mistake” and “I updated my config” is where the compounding value of good AI configuration comes from.
Keeping your config as a mistake catalog
The most effective CLAUDE.md or AGENTS.md files read, in part, like a catalog of things the AI should avoid in this specific project. Every “Don’t do X” in your config is usually a past mistake you’re preventing from recurring.
Embrace this. When the AI does something wrong, add the specific prohibition. Over time, your config becomes a precise description of how your project works — and your AI assistant becomes progressively more accurate.
Using spaget to manage configurations
If you’re tracking these config improvements across multiple AI tools (CLAUDE.md, cursor rules, copilot-instructions.md), keeping them all updated when you fix a mistake is its own overhead.
spaget lets you maintain one configuration and export to all formats at once. Add the fix in the builder, re-export, and every tool gets the updated guidance. No account required — try it here.