Prompt engineering for coding agents
TL;DR
A good coding agent prompt has three components: what you want (precise task), how to validate it (verifiable exit condition), and what you don't want (explicit constraints). Most disappointing results come from instructions missing one of the three.
The difference between a disappointing result and a usable one rarely comes down to the model. It almost always comes down to how the task was formulated. A coding agent is very good at executing precise instructions — and particularly bad at guessing what you meant when instructions were vague.
This guide documents the patterns that produce reliable results, extracted from 4 months of daily usage on Symfony and TypeScript production projects.
What’s the structure of a good agent prompt?
Three components, in this order:
1. The task — what you want Precise, scoped, with an action verb. No vague objectives.
2. The exit condition — how to know it’s done Something the agent can verify autonomously. Ideally: tests pass, the build compiles, a file exists with specific content.
3. The constraints — what you don’t want What the agent must not do, even if it seems logical to it.
# Typical structure
[Action verb] [precise object] [in which file / with which interface]
Exit condition:
- [criterion 1 autonomously verifiable]
- [criterion 2]
Constraints:
- [what the agent doesn't touch]
- [convention to respect]
In practice:
Extract the discount calculation logic from OrderService.php
into a new DiscountCalculator.php in the same namespace.
Exit condition:
- php bin/phpunit --filter OrderServiceTest passes with no test modifications
- DiscountCalculator has its own tests in DiscountCalculatorTest.php
- OrderService's public interface is unchanged
Constraints:
- Don't modify OrderRepository.php
- Use AbstractServiceTest for the tests (see tests/AbstractServiceTest.php)
- DiscountCalculator is final
How do you formulate the task?
Start from the expected result, not the process
The agent chooses how to do it. Your role is to define what. Describing the process (“first do X, then do Y”) is often counterproductive — it constrains the agent on the method without guaranteeing the result.
❌ "First read OrderService.php, then identify the discount logic,
then create DiscountCalculator.php, finally update the tests"
✅ "Extract the discount logic from OrderService into DiscountCalculator.
Existing tests must pass."
Name files explicitly
The agent can search for files, but giving exact paths reduces navigation errors and context window consumption.
❌ "Refactor the payment service"
✅ "Refactor src/Service/PaymentService.php to extract
the retry logic into src/Service/PaymentRetryService.php"
Specify the interface or contract
Especially for refactorings: what must not change.
✅ "PaymentService's public interface stays identical —
public method signatures don't change"
How do you define an exit condition?
The exit condition is what distinguishes a prompt that produces code from a prompt that produces validated code. Without one, the agent stops when the code looks logically correct to it — not when it works.
Prefer conditions the agent can verify itself
✅ "php bin/phpunit --filter OrderServiceTest passes"
✅ "npx tsc --noEmit returns 0 errors"
✅ "npx vitest run --reporter=verbose passes"
✅ "The file src/Service/DiscountCalculator.php exists"
❌ "The code is clean" (not verifiable)
❌ "Everything works" (too vague)
Multiple conditions for complex tasks
Exit condition:
- Existing tests pass: php bin/phpunit --filter OrderTest
- New tests pass: php bin/phpunit --filter DiscountTest
- No TypeScript errors: npx tsc --noEmit
- No circular imports: no DeprecationWarning in the logs
With these conditions, the agent iterates until all are satisfied — with no intermediate intervention.
How do you formulate constraints?
Constraints are the most frequently omitted part and the most impactful. An agent without constraints optimizes for “a solution that looks like what was asked” — not for “a solution that respects the project’s conventions”.
The three types of useful constraints
Scope — what the agent doesn’t touch:
- Don't modify OrderRepository.php
- Don't change the signatures of existing public methods
- Work only within src/Service/ and tests/Service/
Conventions — how the agent should do it:
- Service classes are final
- Constructor injection only, no setter injection
- Use the existing fixtures in tests/fixtures/ rather than creating new ones
References — where to find the patterns to follow:
- Follow the pattern in PaymentService.php for structure
- Tests should look like OrderServiceTest.php
- See AGENTS.md for project conventions
This last type is often the most effective: rather than describing the convention, you point to an existing example. The agent extracts it on its own.
What patterns should you avoid?
The too-short prompt
❌ "Write tests for UserService"
Which framework? What coverage? Which edge cases? The agent will invent answers to all these questions — and probably not the ones you would have chosen.
The too-long prompt without structure
A 30-line block of text with no structure. The agent will treat the first lines with attention and the last ones as vague context. Important constraints should be listed, not buried in a paragraph.
Contradictory instructions
❌ "Refactor PaymentService to simplify it,
but don't change its current behavior,
and improve performance if possible"
“Simplify”, “don’t change behavior” and “improve performance” can be contradictory. When there’s a contradiction, the agent picks — not always in the right direction.
Asking for an opinion and implementation in the same prompt
❌ "Do you think we should use a Value Object for Price?
If yes, implement it."
Split into two prompts: first the analysis with its response, then the human decision, then the implementation. Mixing the two produces an implementation that reflects the agent’s opinion, not yours.
How do you industrialize with custom commands?
Prompts you reuse become custom commands. It’s the way to turn prompt engineering into shared capital rather than repeated individual effort.
A custom command that becomes a team standard deserves the same circuit as a merge request: review by someone else, naming consistent with the existing commands, a one-line doc explaining when to use it. That’s exactly what turns a well-written prompt into shared capital — without review, it’s just one more file in .opencode/commands/ that nobody’s sure is still worth using.
Structure of a well-made custom command:
# .opencode/commands/write-tests.md
Write PHPUnit unit tests for `$ARGUMENTS`.
First analyze the file to identify:
- The public methods to test
- The nominal cases
- The error cases (exceptions, edge values)
- The dependencies to mock
Then create the corresponding test file in tests/
following the pattern from AbstractServiceTest.php.
Exit condition:
- php bin/phpunit --filter [ClassName]Test passes
- Minimum coverage: all error cases are tested
Constraints:
- Use PHPUnit MockObject, no external mock library
- Follow the naming convention of existing tests
Usage:
> /write-tests src/Service/DiscountCalculator.php
The $ARGUMENTS gets replaced by whatever follows the command. A prompt crafted once, reused indefinitely.
Resources
Frequently Asked Questions
- Do you need to formulate differently depending on the model?
- Slightly. Claude Sonnet follows structured instructions with constraint lists very well. Back when I routed some mechanical tasks to MiniMax 2.5 (the Kilo Gateway period, since abandoned), that model was more robust with short, direct instructions. Gemini Flash prefers concrete examples over abstract descriptions. In practice, a well-formulated instruction works across all models — fine-tuning only matters once the basics are solid.
- What's the ideal length for an agent prompt?
- Between 3 and 15 lines for most tasks. Below 3 lines you often miss the exit condition or constraints. Beyond 20 lines the agent starts losing track of requirements. If an instruction needs 30 lines, it's usually a sign you need to split into multiple sessions.
- How do you handle cases where the agent does something unexpected?
- Two answers depending on the situation. If it's a minor deviation (style, naming): correct in the next prompt with "don't do X, do Y". If it's an architectural deviation: don't continue in that session, open a new session with a prompt that reformulates the constraint more explicitly. Trying to correct an agent in a session that has drifted is rarely effective.
- Do custom commands replace prompt engineering?
- No — they encapsulate well-formulated prompts. A custom command is a reusable prompt template. The prompt engineering work is done once to create the command, then you reuse it. That's why investing in custom command prompts is more cost-effective than investing in one-off prompts.
- How do you convey implicit context the agent can't see?
- Via the AGENTS.md file for permanent project conventions, and via code comments for file-specific context. A `// TODO: needs refactoring — see #142` comment in the code is context the agent reads and naturally integrates. Don't hesitate to annotate code to guide the agent.