- I use Cursor Rules to make Claude follow my stack.
- I add a tiny “done = tests pass” contract.
- I lint AI output hard. Automatically.
- I keep prompts short. Diff-first.
Context
I build small SaaS projects. Usually solo. Usually fast.
Cursor + Claude makes me fast. It also makes me sloppy.
The failure mode is always the same. Claude writes code that looks right. It compiles. Then it breaks edge cases, ignores my existing patterns, or sneaks in a dependency I didn’t want.
I spent 4 hours on this once. Most of it was wrong. Not because Claude was “bad”. Because my workflow was vibes. No guardrails.
So I started treating AI like a junior dev with a very specific contract. Same stack. Same conventions. Same “definition of done”. And I automated the policing.
1) I write Cursor Rules first. Not prompts.
Prompts rot. Fast.
Rules don’t. They sit there. Every request gets them.
In Cursor, I keep a project rule file that forces Claude into my existing choices: Next.js App Router, TypeScript strict, no random state libs, prefer server actions, etc.
This is the exact kind of rules file I use (trimmed, but real). Drop it in .cursor/rules.md.
# .cursor/rules.md
You are coding in a Next.js 15 + TypeScript repo.
Hard rules:
- TypeScript only. No JavaScript files.
- No new dependencies unless I ask.
- Prefer built-in Web APIs over libraries.
- Keep functions small. 30 lines max.
- Every change must include: types, error handling, and tests if logic changed.
Next.js rules:
- App Router only.
- Prefer Server Components.
- Client Components only when needed (use 'use client').
- Prefer Server Actions for mutations.
Output rules:
- Show a diff-style plan first (bullets).
- Then edit minimal files.
- Don’t rewrite unrelated code.
One thing that bit me — if rules are too long, Claude starts “agreeing” and then doing whatever.
So I keep it short. Sharp edges only.
2) I force a “definition of done”: tests + typecheck
Claude will happily say “done” after writing code that doesn’t typecheck.
I stopped trusting completions. I only trust green commands.
In every repo I add a check script that matches what CI should do. No creativity.
{
"scripts": {
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"check": "pnpm lint && pnpm typecheck && pnpm test"
}
}
Then I bake this into my prompts.
Not a wall of text. Just a contract.
Make the minimal diff.
pnpm checkmust pass.
If pnpm check fails, I paste the exact error back.
No paraphrasing. No “it’s failing somewhere”. I paste the stack trace like a robot.
That single habit cut my back-and-forth in half.
3) I run a “no new deps” tripwire
The sneakiest AI failure isn’t wrong code.
It’s new dependencies.
You ask for a tiny feature. You get zod, lodash, and some date library because Claude saw it in training data.
I added a tiny script that fails if package.json changed and I didn’t explicitly allow it.
This runs in pre-commit for me. You can also run it manually.
// scripts/no-new-deps.ts
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
function sha256(s: string) {
return createHash("sha256").update(s).digest("hex");
}
const pkg = readFileSync("package.json", "utf8");
const lock = readFileSync("pnpm-lock.yaml", "utf8");
// Commit the baseline hashes once. Update only when you *intend* to.
const BASE_PKG = process.env.BASE_PKG_HASH;
const BASE_LOCK = process.env.BASE_LOCK_HASH;
if (!BASE_PKG || !BASE_LOCK) {
console.error("Missing BASE_PKG_HASH/BASE_LOCK_HASH env vars");
process.exit(2);
}
const pkgHash = sha256(pkg);
const lockHash = sha256(lock);
if (pkgHash !== BASE_PKG || lockHash !== BASE_LOCK) {
console.error("Dependency files changed. Did you mean to add/update deps?");
console.error({ pkgHash, lockHash });
process.exit(1);
}
console.log("OK: no dependency changes detected");
Yeah, it’s blunt.
But it matches how I work: I want dependencies to be a conscious decision.
To set the baseline once:
node -e 'const fs=require("fs"),c=require("crypto");
const h=s=>c.createHash("sha256").update(s).digest("hex");
console.log("BASE_PKG_HASH="+h(fs.readFileSync("package.json","utf8")));
console.log("BASE_LOCK_HASH="+h(fs.readFileSync("pnpm-lock.yaml","utf8")));'
Then store those in your shell env or .env.local for local use.
When I do want a dependency bump, I update the baseline and move on.
4) I make Claude work diff-first (and I reject big diffs)
My worst AI merges were big.
Not “big feature big diff”. More like: asked for a small refactor, got a rewrite.
Now I force a diff-first flow:
- Claude lists the files it’ll touch.
- Claude explains why each file changes.
- I approve.
- Only then it edits.
If it can’t describe the changes in 5 bullets, it’s about to go off-road.
Also: I cap file touches.
“Fix this bug. Touch max 2 files.”
It sounds restrictive. It’s not.
It keeps the model focused. And it keeps me reviewing.
Here’s a pattern I use a lot in Next.js when Claude wants to sprawl: isolate logic into a single module and test it.
Example: I’ll move tricky parsing/validation into lib/ and write a Vitest spec. Claude stays inside that box.
// lib/parsePagination.ts
export type Pagination = { page: number; pageSize: number };
export function parsePagination(input: Record): Pagination {
const pageRaw = Array.isArray(input.page) ? input.page[0] : input.page;
const sizeRaw = Array.isArray(input.pageSize) ? input.pageSize[0] : input.pageSize;
const page = Math.max(1, Number(pageRaw ?? 1) || 1);
const pageSize = Math.min(100, Math.max(1, Number(sizeRaw ?? 20) || 20));
return { page, pageSize };
}
Test it. Lock behavior.
// lib/parsePagination.test.ts
import { describe, expect, it } from "vitest";
import { parsePagination } from "./parsePagination";
describe("parsePagination", () => {
it("defaults safely", () => {
expect(parsePagination({})).toEqual({ page: 1, pageSize: 20 });
});
it("coerces and clamps", () => {
expect(parsePagination({ page: "0", pageSize: "1000" })).toEqual({ page: 1, pageSize: 100 });
expect(parsePagination({ page: "-2", pageSize: "-5" })).toEqual({ page: 1, pageSize: 1 });
});
it("handles arrays from querystring", () => {
expect(parsePagination({ page: ["2"], pageSize: ["10"] })).toEqual({ page: 2, pageSize: 10 });
});
});
Now when Claude “improves” stuff, it has to respect the tests.
And I can review a 20-line diff instead of a 400-line rewrite.
5) I keep a failure log. One line per screw-up.
This is the unsexy part.
But it’s the part that compounds.
Whenever AI causes a bug or wastes time, I add one line to a local NOTES.md.
Stuff like:
- “Claude used
cookies()in a Client Component. Build failed with:Error: cookies was called outside a request scope.” - “It returned
200with empty JSON on errors. Caused silent UI failures.” - “It changed a Prisma query shape and broke serialization.”
Then I convert the common ones into rules.
Rules are just my scars, written down.
Results
This workflow reduced my rework time a lot.
On my last build week, I tracked 23 AI-assisted coding sessions in a scratchpad. 17 ended with pnpm check passing on the first try. Before rules + contracts, that number was 6 out of 20 sessions on a similar codebase.
I also caught 4 unwanted dependency changes with the tripwire script. All four were unnecessary.
The biggest win wasn’t speed. It was fewer “what did it even change?” moments.
Key takeaways
- Put your stack decisions in Cursor Rules. Keep it short.
- Define “done” as
lint + typecheck + tests. Nothing else counts. - Paste exact errors back to Claude. Don’t translate them.
- Reject big diffs for small requests. Force diff-first planning.
- When AI burns you, write it down. Turn it into a rule.
Closing
I’m still using Claude constantly. I just don’t let it drive unsupervised anymore.
If you already use Cursor Rules: what’s the one rule that saved you the most time (the exact line), and what bug did it prevent?
Top comments (0)