How to Write Better GitHub Copilot Prompts
The advice “be more specific” is true and nearly useless, because it does not say specific about what. This lesson is about what.
Every prompt that reliably works is answering a small set of questions the model would otherwise answer for you. Naming those questions turns prompt-writing from a knack into a checklist — and a checklist you can run down when something is not working.
Key takeaways
- Five elements: goal, context, constraints, output, validation. Not every prompt needs all five; a prompt that is failing is usually missing one.
- “Explain the root cause before changing anything” is the highest-value single sentence you can add to almost any prompt.
- Constraints are what prevent the technically correct answer you did not want — the one that changes a signature, adds a dependency, or edits the test.
- Decompose. A prompt asking for six things produces six mediocre things.
- Correct specifically. “That’s wrong, try again” produces a differently-wrong answer; naming the deviation produces a fix.
The five elements
Applied:
Goal: Add pagination to the customer list endpoint.
Context: FastAPI, SQLAlchemy, Python 3.12. The endpoint is in app/routes/customers.py and currently returns every row.
Constraints: Keep the existing response shape for callers that pass no pagination parameters. Do not change the database schema. Do not add dependencies.
Output: Modify the route and add tests to tests/test_customers.py.
Validation: Run pytest and show me the actual output.
That is longer than most people’s prompts and shorter than it looks, because three of those five lines are things you would otherwise have to fix afterwards.
Goal: say what “done” means
The weakest prompts describe a symptom and leave the goal implicit.
“This is slow” does not say whether you want it profiled, optimised, or explained. “Fix the tests” does not say whether the tests or the code are wrong — and an agent that picks the wrong one produces a green suite that verifies nothing.
State the outcome you want, and — where it helps — the outcome you do not:
Weak: This endpoint is slow.
Better: This endpoint takes about 4 seconds for 10,000 rows. Identify the cause. Do not change any code yet — I want to see the diagnosis before deciding what to do about it.
Context: name things, do not describe them
The single biggest quality jump available is naming files instead of describing the situation.
“The user service has a bug” makes the model search. “The bug is in
validate_email in app/services/users.py” makes it read. Searching is
approximate; reading is not.
The same applies to errors. Paste the actual output — the traceback, the failing assertion, the command line. A described error is a lossy summary of an error, and the lost part is often the part that mattered.
Constraints: rule out the answers you do not want
Constraints are the least-used element and the one that prevents the most rework, because most disappointing output is not wrong — it is a legitimate reading of an underspecified request.
The constraints worth stating almost every time:
Scope. “Only modify src/parser.py.” Agents expand scope; a named boundary is
the cheapest way to prevent it.
Compatibility. “Preserve the existing function signature.” Otherwise a local fix becomes a change to every caller.
Dependencies. “Do not add dependencies.” A model will reach for a library to solve a twenty-line problem, and it cannot see your supply-chain policy.
Tests. “Do not modify tests to make them pass.” There are two routes to a green suite and only one of them is the one you want.
Behaviour. “Do not change behaviour for input the tests already cover.” This one is easy to forget and expensive to miss, because a behaviour change that no test covers will not announce itself.
Output: say what should come back
Ambiguity here produces a wall of prose when you wanted a diff, or a diff when you wanted an explanation.
Useful specifications: which files may change; whether tests should be added; whether you want an explanation, a plan, a patch, or a list; and what format — “a bulleted list with file and line for each finding” beats “tell me about the issues”.
Validation: build the check into the request
The last element is the one most often left off, and the one that turns an assertion into evidence.
“Run pytest and show me the actual output” is different from “make sure the tests pass”. The first produces output you can read; the second produces a claim.
After the change, run:
.venv/bin/python -m pytest -q .venv/bin/python -m ruff check .
Paste the real output of both. If either fails, fix the cause and run them again rather than telling me it is fine.
Explain before you modify
The highest-leverage pattern in this lesson, and it is one sentence.
Analyse the problem first. Do not change any files yet.
Explain:
- The root cause
- Which files are involved
- What you propose to change
- What could go wrong
- How we will verify it worked
Why it works: it separates understanding from acting, and understanding is cheap to check. If the diagnosis is wrong, you have spent one turn. If you let it act on a wrong diagnosis, you have spent a review cycle and possibly a revert.
This matters most where mistakes are expensive — production code, infrastructure, anything security-sensitive, anything you did not write.
Decompose
A prompt asking for six things gets one attempt at each and no opportunity to check any of them.
Refactor this module, add tests, update the documentation, and make it faster.
Four tasks, four sets of criteria, one response. What comes back will be plausible and shallow, and reviewing it means reviewing four unrelated changes in one diff.
Sequence the work instead. Explain the module. Agree the structure. Make the change. Add the tests. Update the docs. Each step is small enough to evaluate, and a wrong turn costs one step.
Prompting for specific kinds of work
The five elements apply everywhere; the emphasis shifts by task.
Explaining code
The easiest category and the one where prompts are most often too thin. “What does this do” gets you a paraphrase of the code. Ask for what you actually want to know:
Explain what this module does, then answer specifically: what
happens if config is missing a timeout key, and is that
deliberate?
The second half is the useful part. A paraphrase is easy to produce and easy to get right; a question about an edge case is where you find out whether the reading was real.
Generating code
The failure mode is code that works and does not fit — wrong error-handling convention, wrong logging, wrong structure. Constrain by pointing at an existing example rather than describing your conventions:
Add a delete_customer handler.
Follow the structure of update_customer in the same file:
same validation approach, same error handling, same logging.
Debugging
Include the evidence, not the summary: the exact error, the traceback, what you expected, what happened, and what changed recently. And ask for the cause before the fix.
Refactoring
The category most prone to scope expansion, so the boundary matters most:
Extract the validation logic from parser.py into a new
validation.py.
Behaviour must not change — the existing tests must pass unmodified. Only those two files and their imports may change. Do not rename any public function.
Run the suite before and after and show me both results.
“Before and after” establishes the suite was green to begin with, which changes the conversation if it was not.
Writing tests
Name the cases. “Add tests” produces a happy-path test; the failure cases are where the value is:
Add pytest tests for parse_quantity covering: a valid integer
string, zero, a negative number, a non-numeric string, an empty
string, and None.
For each, assert the specific exception type or return value.
Then break the implementation on purpose and confirm the new tests fail. A test that passes regardless of whether the code is correct is worse than no test, because it carries the authority of a green suite.
Code review
Scope by concern rather than asking for everything:
Review the changes on this branch for one thing only: places where user input reaches a query, a file path, or a subprocess call without validation.
For each, give file and line and say what an attacker could do. Ignore style and naming.
Three focused passes beat one general pass, and the results are short enough to act on.
The anti-patterns
Five recurring shapes, and what to do instead.
Too vague
“Build an API.” Nothing here constrains the output: which framework, which endpoints, which data, what auth. You will get a generic scaffold that matches nothing about your project.
Instead: name the framework, the resource, the operations, and where the code should go.
Too broad
“Rewrite this entire project.” Even where this succeeds it produces a diff nobody can review, which means nobody will.
Instead: pick the one module that hurts most. Then the next.
Missing constraints
“Fix security.” Fix which security, against what threat, with what constraints? This tends to produce a scattering of unrelated hardening, some of it breaking things.
Instead: “Review app/routes/ for places user input reaches a query or the
filesystem without validation. Report findings with file and line. Do not change
anything yet.”
Assumed context
“Use our normal style.” The model does not know your normal style unless it is written down. This is the anti-pattern that instruction files exist to solve — if you catch yourself writing it, that is the signal to write a copilot-instructions.md file.
Instead: state the convention, once, in a file.
Unbounded autonomy
“Do whatever is necessary and deploy it.” Every word of that is a permission you probably did not mean to grant.
Instead: bound the work, and keep deployment a human decision.
A note on what prompts are not
Prompts written in natural language are requests, not commands. This distinction mattered in Cluster 1 and it matters here: nothing in “do not modify the tests” is enforced by the system. It is a sentence a model usually honours.
That has two consequences worth holding.
A prompt is not a permission boundary. If something must not happen, the mechanism is configuration — a tool permission, a restricted agent, a protected branch. Cluster 5’s custom agents draws the line clearly: prose describes intent, and capability decides what is possible.
Placeholders are a convention, not syntax. The [BRACKETED] style used in the
100-prompt reference marks a blank for
you to fill in. Copilot does not interpret brackets specially, and leaving one
unreplaced means asking the model to work with a literal placeholder — which it
will sometimes do quite cheerfully.
Iterate by correcting, not restarting
When an answer is wrong, name the deviation:
That changed the function signature, which I asked you not to do.
Revert that part. Keep the validation you added, and achieve the same result without altering the signature.
“That’s not right, try again” gives the model nothing to act on. It will produce a different answer, which is not the same as a better one.
Two other moves worth having:
Ask what is ambiguous. “Before you write code, tell me what is underspecified about this request.” Surfaces the assumptions before they become a diff.
Ask for the reasoning. “Why did you choose that approach over the alternative?” Frequently reveals that it did not consider one.
Length is not the variable
A common misreading of “be specific” is “write more”. The two are unrelated, and past a point they work against each other.
A long prompt dilutes. If the important constraint is in sentence eleven of a fourteen-sentence paragraph, it competes with thirteen other sentences for attention, and the model has no way to know which one you would defend. Structure beats volume: five labelled lines are read more reliably than the same content as prose.
The prompts in this lesson are mostly short. What makes them work is that each line is doing a job — and a line that is not doing a job is costing you the attention of the lines that are.
Words that carry weight
A few phrasings are reliably worth their space, because each closes off a specific failure.
“Do not change anything yet.” Converts an action into an analysis. The single most useful phrase in this lesson.
“Show me the actual output.” Turns a claim into evidence.
“If you are unsure, say so rather than guessing.” Models default to answering. An explicit permission to express uncertainty is one of the few things that reliably surfaces it.
“Explain why you chose that over the alternative.” Frequently reveals that there was no comparison.
“Stop and tell me” — as in “if this needs a dependency, stop and tell me rather than adding one”. Gives a defined action for the blocked case, which prevents the model finding a creative route to the outcome you did not want.
“Only modify X.” The boundary that prevents scope expansion.
None of these are magic words, and none of them are Copilot syntax — they are ordinary English that happens to remove an ambiguity. Which is what all of prompt engineering is.
Where prompts stop being the answer
If your prompts have grown a standard preamble — the same three sentences about your stack, your test runner, your conventions — you have outgrown prompting. That content belongs in instructions, where it applies automatically and stops costing you typing.
The sign that you got it right is that your prompts get shorter rather than more elaborate.
Practising this
The fastest way to get better at prompting is not to read more about it. It is to keep the prompts that worked.
When a prompt produces a genuinely good result, save it. When one produces a frustrating result and a correction fixes it, save the corrected version rather than the original. Within a few weeks you will have a personal set of shapes that fit your codebase, and most of them will look like the examples here with your project’s specifics filled in.
That collection is also the raw material for the next two lessons. Prompts you reuse frequently become prompt files; rules that appear in every prompt become instructions. Both start as a note somewhere of something that worked.
Next
100 prompts applies everything here to concrete tasks. Context engineering is the deeper treatment of the element most people underuse, and the pillar puts prompting in its place among the other four levers.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Your progress
Saved in this browser only. No account, no server, and nothing leaves your device. Clearing site data resets it.Sync across devices if you want it everywhere.
Saved in this browser and synced to your account, so it follows you between devices. Manage or delete it.
Was this lesson helpful?
We record which lesson you rated and whether it helped. Nothing identifies you — no account, no cookie, no session.