Skip to content
Sudip KC writing notes in a journal at a desk
SK.
← All articles

AI · · 6 min read

How I Write Better Prompts for Coding Agents

  • AI
  • Productivity
  • Opinion
  • Architecture

Most bad agent output is not the model's fault. It is a vague ticket handed to a very fast contractor who cannot ask follow-up questions. Fix the ticket and the code gets noticeably better.

I use Claude and ChatGPT, plus coding agents in the terminal, every working day. Some days they save me hours. Other days they confidently rewrite the wrong file and I lose the morning cleaning up. Over time I noticed the difference between those days was mostly me: how much context I gave, how clearly I scoped the work, and whether I told the agent how to know it was done.

This is the prompt structure I have settled on. It is not magic wording. It is the same thing I would put in a good task description for a developer on my team at NovaNext.

Treat the prompt like a ticket

When I write a task for a junior developer, I include what we are building, why, where it lives, what not to touch, and how we will verify it. Agents need the same thing, arguably more, because they will never walk over to my desk and say "wait, did you mean the admin view or the waiter view?"

My default template has five parts:

  1. Goal: one or two sentences on the outcome, in user terms.
  2. Context: the files, patterns, and decisions that matter.
  3. Constraints: what must not change, which libraries to use or avoid.
  4. Steps or plan: optional, for anything bigger than a small change.
  5. Done when: how the agent should verify the work.

Here is a real-shaped example from a restaurant platform like NovaRestro:

Goal: Waiters should see a "Sent to kitchen" badge on an order item
once its kitchen order ticket is created.

Context:
- Order items render in src/features/orders/components/OrderItemRow.tsx
- KOT status comes from the existing useOrder() query (TanStack Query),
  field: item.kot_status ("pending" | "sent" | "done")
- Badges must use the existing <StatusBadge> in src/components/ui

Constraints:
- Do not change the API or the query hook.
- No new dependencies.
- Keep the row height the same on mobile.

Done when:
- Badge shows for "sent" and "done", hidden for "pending"
- npm run lint and npm run typecheck pass
- Tell me which files you changed and why.

That takes me maybe three minutes to write. Without it, I have watched agents invent a new badge component, add a status field to the backend serializer, and restyle the row. All technically reasonable. All wrong for my codebase.

Context is the whole game

The model knows React. It does not know your React. The most valuable thing you can give it is where things live and which patterns are already established.

  • Point to files, not concepts. "Use the existing form pattern" is weak. "Follow the pattern in src/features/menu/MenuItemForm.tsx" is strong.
  • Name the conventions. Folder structure, naming, how errors are handled, where API calls live. I wrote about my layout in How I Structure React + Django Projects, and the agent should follow it just like a new hire would.
  • Say what is off limits. Generated files, migrations, shared UI primitives, the auth layer. Agents love tidying things that were not part of the task.
  • Paste the error, the whole error. Stack trace, the command you ran, what you expected. A screenshot of half a terminal is not a bug report for a human either.

For the stuff that is true across every task, I keep a project instructions file in the repo so I do not repeat myself. Build commands, test commands, code style, the folders that matter. The task prompt then only carries what is specific to this change.

The model knows the framework. Your job is to tell it about your project, because that is the part it cannot guess.

Scope small, then chain

One of my early mistakes was asking for a whole feature in one go: "Build table reservations with availability checks, a booking form, and admin approval." The agent would produce something that looked complete and was wrong in a dozen small ways, spread across fifteen files. Reviewing that is worse than writing it.

Now I break work into slices that I can review in one sitting:

  1. Model and migration, nothing else.
  2. Serializer and endpoint, with tests.
  3. The query hook and types on the frontend.
  4. The form UI using existing components.
  5. Loading, error, and empty states.

Each slice gets its own prompt, and I read the diff before moving on. It feels slower. It is not. The total time drops because I catch a bad assumption in step one instead of discovering it baked into step five.

Ask for a plan before code

For anything non-trivial I add one line: "Before writing code, list the files you plan to change and your approach. Wait for my OK."

This is the cheapest review I will ever do. Reading a ten-line plan takes thirty seconds, and it is where I catch the big problems: the agent wants to add a library we do not use, or it misread which screen the feature belongs to, or it plans to duplicate logic that already exists in a hook. Correcting a plan costs one message. Correcting an implementation costs a revert.

Tell it how to check its own work

Agents get much better when they have a way to verify. "Done when" is the part of my template I would keep if I could only keep one.

  • Commands to run: lint, typecheck, the relevant test file.
  • Behavior to confirm: "The endpoint returns 403 for a waiter role and 200 for a manager."
  • A request to summarize what changed and anything it was unsure about.

That last request is underrated. When I ask the agent to flag uncertainty, it often tells me exactly where to look: "I assumed kot_status can be null for legacy orders." That one sentence saves me from a bug in production.

Things I stopped doing

  • Stacking adjectives. "Write clean, scalable, production-ready, best-practice code" adds nothing. Specific constraints beat vibes.
  • Shouting. All caps and "IMPORTANT!!!" do not make an instruction clearer. If a rule matters, state it plainly and explain why.
  • Hiding the why. "Do not use a modal here" works better as "Do not use a modal here, because waiters use this on small phones with one hand." The reason helps the agent make good calls on the parts I did not specify.
  • Letting a bad conversation run. If the agent has gone down a wrong path for three or four turns, I start fresh with a better prompt. A long, confused thread keeps dragging old mistakes forward.
  • Accepting code I cannot explain. If I would not be able to defend it in a code review, it does not get merged. More on that in How I Use AI Without Letting AI Write My Entire Codebase.

A quick prompt checklist

Before I hit enter on anything bigger than a one-line fix, I run through this:

  • Did I state the goal in terms of what the user sees?
  • Did I point to the exact files and existing patterns?
  • Did I say what must not change?
  • Is the task small enough to review in one sitting?
  • Did I ask for a plan first, if it touches more than two or three files?
  • Did I define "done" with commands and behaviors?

Where I've landed

Prompting a coding agent well is mostly the same skill as writing a good ticket, which is mostly the same skill as thinking clearly about the work before you start. The agents keep getting more capable, and I wrote about how that changed my day in How AI Changed the Way I Build Software. But the better they get, the more the bottleneck becomes the quality of the instructions. Give them a real ticket, and they do real work.