DEV Community

Sathish
Sathish

Posted on

Cursor + Claude: my vibe-coding safety net

  • I don’t trust AI edits unless tests + types pass.
  • I keep a tiny prompt template that forces diffs.
  • I use scripts to catch “works on my machine” bugs.
  • You’ll get 4 copy-paste snippets for Next.js.

Context

I build small SaaS projects. Usually solo. Usually fast.

Cursor + Claude makes me faster. Also makes me sloppy.

My failure mode is consistent. I accept a “clean” refactor. It compiles. Then a week later I find a broken edge case in prod logs. Brutal.

So I stopped treating AI like a pair programmer. I treat it like a code generator that must go through gates.

This post is the exact safety net I use. Not theory. It’s the stuff that saved me after spending 4 hours on a “simple” refactor where most of it was wrong.

1) I start with a diff-only prompt. No essays.

Cursor chats love to explain. Explanations don’t ship.

I want a patch. Or nothing.

So I keep a prompt template in a file. Then I paste it into Cursor chat. Every time.

It forces: constraints, file list, and a diff output.

# prompts/patch-only.txt

You are modifying an existing Next.js + TypeScript repo.

Rules:
- Output ONLY a unified diff (git patch). No explanation.
- If you need new files, include them in the diff.
- Don’t change formatting or rename things unless required.
- Keep functions pure when possible.
- Do not add new dependencies.

Context:
- The failing behavior: 
- The expected behavior: 

Files you may edit:
- - Acceptance criteria:- `pnpm typecheck` passes- `pnpm test` passes- No unused imports```

This sounds strict. It is.One thing that bit me — if I let the model “clean up” unrelated code, the review cost explodes. The strict diff rule keeps the blast radius small.And yeah. Sometimes it refuses. Good. That means my request was vague.## 2) I add “typecheck” and “lint” scripts firstVibe coding without scripts is just gambling.I want one command that tells me “safe enough.”In Next.js + TS repos, I standardize on 4 scripts. I add them even in tiny weekend projects.

```json{  "scripts": {    "dev": "next dev",    "build": "next build",    "start": "next start",    "lint": "next lint",    "typecheck": "tsc -p tsconfig.json --noEmit",    "test": "vitest run",    "check": "pnpm -s lint && pnpm -s typecheck && pnpm -s test"  }}```

That `check` script is the whole point.Cursor can generate code fast. My job is making sure it doesn’t silently degrade the codebase.Also. I don’t run `next build` as my typecheck.Because `next build` can pass while I still have type holes in non-imported files. `tsc --noEmit` catches stuff that never gets bundled.## 3) I force the model to write tests for the bugIf I don’t write the test, I forget the bug.If the model doesn’t write the test, it “fixes” symptoms.My pattern: reproduce bug → lock it in with a test → then refactor.Here’s a real example I hit a lot: unsafe env parsing.AI-generated code loves `process.env.X as string`. That turns “missing env” into “undefined flows into prod.”I use a tiny env parser. Then I test it.

```ts// src/env.tsexport function requiredEnv(name: string): string {  const value = process.env[name];  if (!value || value.trim().length === 0) {    throw new Error(`Missing required env: ${name}`);  }  return value;}export function optionalEnv(name: string, fallback: string): string {  const value = process.env[name];  return value && value.trim().length > 0 ? value : fallback;}```

And the test. Small. Fast.

```ts// src/env.test.tsimport { describe, expect, it } from "vitest";import { optionalEnv, requiredEnv } from "./env";describe("env", () => {  it("throws when required env is missing", () => {    delete process.env.MY_REQUIRED;    expect(() => requiredEnv("MY_REQUIRED")).toThrow(      "Missing required env: MY_REQUIRED"    );  });  it("uses fallback for optional env", () => {    delete process.env.MY_OPTIONAL;    expect(optionalEnv("MY_OPTIONAL", "fallback")).toBe("fallback");  });});```

This is boring code. That’s why it works.Then in my app code, I only import from `src/env.ts`. No direct `process.env` reads.AI will still try. I reject the diff.## 4) I run a “dead import” check because AI loves leftoversClaude writes code fast. It also leaves crumbs.Unused imports. Unused variables. Half-removed helpers.Lint catches some of it. Not all.So I add a tiny Node script to fail CI if TypeScript has “unused” diagnostics.No extra dependencies. Just `tsc` output parsing.

```js// scripts/typecheck-strict.mjsimport { execSync } from "node:child_process";const cmd = "pnpm -s tsc -p tsconfig.json --noEmit";try {  execSync(cmd, { stdio: "pipe" });  process.exit(0);} catch (err) {  const output = (err.stdout?.toString() || "") + (err.stderr?.toString() || "");  // These show up constantly after AI edits.  const noisy = [    "TS6133", // 'x' is declared but its value is never read.    "TS6196"  // 'Foo' is declared but never used.  ];  const hasUnused = noisy.some((code) => output.includes(code));  // Print full output so CI logs show the file/line.  process.stderr.write(output);  process.exit(hasUnused ? 2 : 1);}```

Then I wire it in.

```json{  "scripts": {    "typecheck:strict": "node scripts/typecheck-strict.mjs",    "check": "pnpm -s lint && pnpm -s typecheck:strict && pnpm -s test"  }}```

Exit code `2` is intentional.When I’m debugging CI, I can tell the difference between “real type errors” vs “AI left junk.”Yeah, it’s picky.That’s the point. I don’t want to ship a refactor with 7 unused imports that confuse me later.## 5) My Cursor workflow: chat to propose, composer to apply, terminal to judgeI don’t let chat directly “decide” correctness.My loop is mechanical:1. Chat proposes a patch.2. I paste it into Composer.3. I scan the diff like it’s a PR.4. I run `pnpm check`.When it fails, I paste the exact failure back.Not “it doesn’t work.”I paste the actual error. Like:- `TS2322: Type 'string | undefined' is not assignable to type 'string'`- `ReferenceError: window is not defined`- `TestingLibraryElementError: Unable to find an element by...`Then I say: “Fix only what’s needed. Patch only.”This prevents the worst pattern I used to do.I’d accept an AI fix… then ask for “cleanup.”Cleanup is where bugs breed.If I want cleanup, I do it after the tests are green. In separate diffs. Small pieces.## ResultsThis process reduced my rollback moments from “a couple times a week” to 0 in the last 14 days.Real numbers:- I ran `pnpm check` 62 times across 6 branches.- I rejected 19 AI patches because they changed unrelated files.- I caught 11 type errors that `next build` didn’t surface in my local flow.The biggest win wasn’t speed. It was fewer surprise regressions.And my code reviews got faster because diffs stayed small.## Key takeaways- Make the model output diffs, not explanations.- Add `typecheck` and `check` scripts on day 1.- Don’t accept refactors without a test that reproduces the bug.- Treat unused imports as a failed build, not “later cleanup.”- Keep changes small. One behavior per patch.## ClosingCursor + Claude works best for me when it’s boxed in.Strict prompts. Strict scripts. Strict diffs.What’s your non-negotiable gate for AI-generated code: `tsc`, unit tests, E2E, or something else entirely?
Enter fullscreen mode Exit fullscreen mode

Top comments (0)