Prompt Engineering for AI Coding Agents: What Actually Works
Most advice on prompt engineering is about chatbots: "be specific," "provide context," "use system prompts." AI coding agents are a different problem. They don't just generate text — they read your codebase, execute commands, modify files, and run tests. A prompt that works well for ChatGPT might produce terrible results when given to Claude Code or Aider operating on a real repository.
After months of running multiple agents daily on production codebases, certain patterns consistently produce better outcomes. This guide covers the prompting strategies that actually work for AI coding agents, with real examples you can adapt to your own workflow.
Why Agent Prompts Are Different
When you prompt a chatbot, you're asking for text output. When you prompt a coding agent, you're delegating a task that involves:
- Reading and understanding existing code
- Making decisions about file structure and architecture
- Writing code that integrates with existing patterns
- Running commands and interpreting their output
- Deciding when to stop and ask for input vs. continuing
The agent has autonomy. A vague prompt doesn't just produce a vague answer — it produces an agent that wanders through your codebase making questionable decisions. A well-structured prompt constrains the agent's behavior in ways that channel its capabilities toward your actual goal.
Pattern 1: Scope First, Task Second
The single most impactful prompting pattern is defining scope before describing the task. Most developers do the opposite — they describe what they want, then maybe mention where.
Weak prompt:
Add input validation to the user registration endpoint.
Strong prompt:
Working in src/api/routes/auth.ts and src/api/validators/:
Add input validation to the POST /register endpoint.
Validate email format, password strength (min 8 chars, one number, one special char),
and username (3-30 chars, alphanumeric and underscores only).
Use the existing zod schemas in src/api/validators/ as a reference for style.
Do not modify any other routes or files.
The strong prompt does three things the weak one doesn't:
- Constrains the file scope — the agent knows exactly where to look and where to make changes
- References existing patterns — instead of inventing a validation approach, the agent will follow your established conventions
- Sets boundaries — "do not modify any other routes" prevents the agent from going on a refactoring adventure
The Scope Template
For any task, start with this structure:
Working in [FILE_PATHS]:
[TASK_DESCRIPTION]
Reference [EXISTING_PATTERN] for style/conventions.
Do not modify [OUT_OF_SCOPE_AREAS].
This takes five seconds to write and eliminates the most common failure mode: agents making changes in unexpected places.
Pattern 2: Incremental Task Decomposition
Large tasks fail more often than small ones. This isn't about the agent's capability — it's about error accumulation. An agent making 20 file changes has 20 opportunities for something to go wrong. If the first change introduces a subtle bug, every subsequent change builds on that bug.
Weak prompt:
Implement a complete WebSocket notification system with channels,
authentication, rate limiting, and a React frontend component.
This prompt will produce code. It might even work. But you'll spend more time reviewing and fixing it than if you'd broken it into steps.
Strong approach — sequential prompts:
Step 1: Create the WebSocket server module in src/ws/server.ts.
Set up a basic WebSocket server using the ws library that accepts connections
on /ws and echoes messages back. Include connection/disconnection logging.
Run the existing test suite after to make sure nothing breaks.
Review. Approve. Then:
Step 2: Add channel-based message routing to the WebSocket server.
Clients should subscribe to channels by sending {"type": "subscribe", "channel": "notifications"}.
Messages sent to a channel should broadcast to all subscribers of that channel.
Add unit tests in src/ws/__tests__/channels.test.ts.
Review. Approve. Continue.
Each step is:
- Small enough to review in under a minute
- Independently testable
- Building on verified working code
When to Use Single vs. Multi-Step Prompts
Single prompt works for:
- Changes under ~50 lines
- Modifications to a single file
- Well-defined, unambiguous tasks (e.g., "add a loading spinner to the submit button")
- Tasks with clear success criteria
Multi-step prompts for:
- New features spanning multiple files
- Refactoring with dependencies between changes
- Anything involving both backend and frontend
- Tasks where you're not 100% sure of the right approach
Pattern 3: Specify the Verification Step
Agents that verify their own work catch errors before you have to. Most coding agents can run tests, linters, and build commands. But they won't do it unless you tell them to.
Without verification:
Add a retry mechanism to the API client in src/lib/api-client.ts.
Support configurable retry count and exponential backoff.
With verification:
Add a retry mechanism to the API client in src/lib/api-client.ts.
Support configurable retry count and exponential backoff.
After making changes:
1. Run `npm test -- --grep "api-client"` to verify existing tests still pass
2. Add tests for the retry behavior in src/lib/__tests__/api-client.test.ts
3. Run the new tests to confirm they pass
4. Run `npm run lint` and fix any issues
The agent now has a built-in quality gate. It writes the code, tests it, and fixes problems — often without needing your intervention. This pattern works especially well for refactoring tasks where the existing test suite serves as a safety net.
Common Verification Commands to Include
After changes:
- Run `npm test` (or pytest, cargo test, go test ./...)
- Run `npm run lint` (or ruff check, clippy)
- Run `npm run build` to verify no type errors
- Run `npm run typecheck` if separate from build
Pick the ones relevant to your project and include them consistently. If you find yourself copying the same verification steps, add them to a CLAUDE.md or project-level configuration file so the agent picks them up automatically.
Pattern 4: Provide Examples of Desired Output
Agents are excellent at pattern matching. If you show them what "good" looks like in your codebase, they'll replicate that pattern far more reliably than if you describe it in words.
Describing style (fragile):
Write a new API endpoint for fetching user notifications.
Use async/await, proper error handling, and return typed responses.
Showing style (robust):
Write a new API endpoint for fetching user notifications.
Follow the exact same pattern as the GET /users/:id endpoint in src/api/routes/users.ts.
Same error handling approach, same response format, same middleware chain.
The new endpoint should be GET /users/:id/notifications in a new file
src/api/routes/notifications.ts.
The agent reads users.ts, understands your conventions (error handling, response shapes, middleware usage, naming), and replicates them precisely. You get consistency without writing a style guide.
When You Don't Have an Existing Example
If you're building something genuinely new, write a skeleton first:
I want API endpoints that follow this structure:
// Route handler
export async function getNotifications(req: Request, res: Response) {
// 1. Validate input with zod schema
// 2. Call service layer (never access DB directly)
// 3. Return { success: true, data: ... } or { success: false, error: ... }
}
Implement the full notifications CRUD following this pattern.
Even a rough skeleton communicates more than paragraphs of description.
Pattern 5: Constraint-Based Prompts
Sometimes the most important part of a prompt is what the agent should not do. Constraint-based prompts are particularly useful for refactoring and cleanup tasks where agents tend to get overly ambitious.
Unconstrained (risky):
Refactor the database module to use connection pooling.
An unconstrained agent might restructure your entire data access layer, rename functions, change error handling patterns, and update every file that imports the database module.
Constrained (controlled):
Refactor the database module (src/db/connection.ts) to use connection pooling.
Constraints:
- Keep the existing exported function signatures identical
- Only modify src/db/connection.ts and src/db/pool.ts (create if needed)
- Do not change how other files import or use database functions
- Do not rename any existing functions or types
- If the pool library needs configuration, use environment variables
matching our existing naming pattern (DB_POOL_MIN, DB_POOL_MAX)
The constraints act as guardrails. The agent can still make creative decisions within those boundaries, but it can't accidentally break your API surface or trigger a cascade of changes across the codebase.
Useful Universal Constraints
These work well for almost any task:
Constraints:
- Do not modify files outside [SCOPE]
- Keep existing function signatures and exports unchanged
- Follow existing naming conventions in the codebase
- Do not add new dependencies without asking first
- Preserve existing test coverage
Pattern 6: Context-Loading Prompts
Agents operate on what they can see. If context is spread across multiple files or requires understanding a specific architecture decision, help the agent find it.
Without context loading:
Fix the race condition in the notification system.
The agent has to figure out which notification system, where the race condition is, and what the expected behavior should be.
With context loading:
There's a race condition in the notification system.
Relevant files:
- src/notifications/dispatcher.ts (the core dispatch logic)
- src/notifications/queue.ts (the message queue)
- src/notifications/__tests__/dispatcher.test.ts (the failing test)
The bug: when two notifications are dispatched simultaneously for the same user,
both pass the duplicate check because neither has been persisted yet.
The test "should not send duplicate notifications" in the test file demonstrates this.
Fix the race condition. The solution should use the existing Redis lock
in src/lib/redis-lock.ts — don't add a new locking mechanism.
This prompt gives the agent:
- The exact files to read
- A description of the bug with root cause
- A pointer to the test that reproduces it
- A constraint on the solution approach
The agent can now focus on writing the fix instead of spending time investigating.
Pattern 7: The Review-and-Revise Loop
Don't treat agent prompts as one-shot interactions. The most effective workflow is a conversation where you guide the agent through revisions:
# Initial prompt
Create a rate limiter middleware for Express using a sliding window algorithm.
Store counters in Redis. Limit to 100 requests per minute per IP.
Put it in src/middleware/rate-limiter.ts.
# After reviewing output
Good structure. Two changes:
1. The Redis key should include the route path, not just the IP.
Different endpoints should have independent limits.
2. Add a X-RateLimit-Remaining header to responses.
# After second review
Looks good. Now add tests in src/middleware/__tests__/rate-limiter.test.ts.
Mock Redis using the existing mock in src/__mocks__/redis.ts.
Each prompt builds on the previous one. The agent retains context from earlier in the conversation, so you don't need to repeat yourself. This iterative approach consistently produces better code than trying to specify everything upfront.
Anti-Patterns to Avoid
1. The Kitchen Sink Prompt
Build a complete user management system with registration, login, OAuth,
password reset, email verification, role-based access control, audit logging,
profile management, account deletion, and admin dashboard.
No agent will do this well in a single pass. Break it into features, implement them one at a time.
2. The Vague Improvement Prompt
Make the code better.
Better how? Faster? More readable? More maintainable? Different developers have different definitions of "better." Be specific about what improvement means.
3. The Copy-Paste-From-Docs Prompt
Use React Query to fetch data. React Query is a data fetching library
that provides caching, automatic refetching, and... [500 words of documentation]
The agent already knows what React Query is. Instead of explaining the library, explain how you want to use it:
Add React Query for the user profile fetch. Use the same query key pattern
as the existing useProducts hook in src/hooks/useProducts.ts.
4. The No-Context Prompt
Fix the bug.
Which bug? Where? What's the expected behavior? Agents need enough context to act. If you can't describe the bug, include a stack trace, error message, or failing test.
Putting It All Together: A Real Workflow
Here's how these patterns combine in practice. You're adding a search feature to an existing API:
Prompt 1 — Scope and foundation:
Working in src/api/routes/search.ts and src/services/search/:
Create a search endpoint GET /api/search?q=<query>&type=<type>&page=<number>.
Follow the same route handler pattern as src/api/routes/products.ts.
For now, just set up the route, input validation, and return a mock response
with the correct shape: { results: [], total: 0, page: 1 }.
Run npm test after to verify nothing breaks.
Prompt 2 — Core logic:
Now implement the actual search logic in src/services/search/search-service.ts.
Use the existing PostgreSQL full-text search (see how src/services/products/
uses ts_vector columns). Support searching across products and users.
Add tests in src/services/search/__tests__/search-service.test.ts.
Run them before moving on.
Prompt 3 — Integration and polish:
Connect the search service to the route handler in src/api/routes/search.ts.
Replace the mock response with real results. Add pagination using the same
pattern as the products list endpoint.
Run the full test suite with npm test and fix any failures.
Three focused prompts. Each builds on verified work. Each is reviewable. The result is production-quality code that matches your existing patterns.
Project-Level Configuration
If you find yourself repeating the same constraints and verification steps, most agents support project-level configuration:
CLAUDE.md (for Claude Code):
## Project conventions
- All API routes follow the pattern in src/api/routes/products.ts
- Use zod for input validation
- Service layer handles business logic, routes handle HTTP concerns
- Tests live in __tests__/ directories adjacent to source files
## After making changes
- Run `npm test` to verify
- Run `npm run lint` to check style
- Run `npm run build` to verify types
Aider conventions (.aider.conf.yml):
conventions: |
Follow existing patterns in the codebase.
Run tests after changes.
Project-level configuration applies to every prompt automatically, so you don't have to repeat foundational constraints in every session.
Conclusion
Effective prompting for AI coding agents comes down to reducing ambiguity and increasing structure. Scope your tasks, break them into steps, include verification, reference existing patterns, set constraints, and iterate.
None of these patterns are complicated. The hard part is making them habitual. Start with scope-first prompts tomorrow. Add verification steps next week. Gradually build up to the full workflow as it becomes natural.
The agents are only as good as the instructions they receive. Clear, structured prompts consistently outperform clever ones.