Per-Service Copilot Instructions for GitHub Copilot CLI: Keep Service Rules Close to the Repo

Mehdi Hadeli
@mehdihadeli
On this page
Table of contents
Per-Service Copilot Instructions for GitHub Copilot CLI: Keep Service Rules Close to the Repo
Part of a three-post series on GitHub Copilot CLI for multi-repo .NET work. If you want the earlier pieces first, start with Multi-Repo Workspace Setup for GitHub Copilot CLI: Context Across .NET Service Boundaries, then read Repo Registry for GitHub Copilot CLI: Service Discovery for Multi-Repo .NET Workflows.
Runnable sample: samples/per-service-copilot-instructions. It includes repo-level instruction files and a validator script that checks each file for the required sections.
A shared workspace helps GitHub Copilot CLI see more of the system. That solves one problem. It does not solve all of them.
In a multi-repo .NET setup, each service still has local rules that matter. The API may enforce a specific validation pattern. The Worker may have message compatibility rules you cannot break. The Angular app may rely on generated clients that nobody should edit by hand. The infrastructure repo may have naming rules that exist for a reason and should not be rediscovered every time.
Those details rarely belong in one big workspace note.
They belong with the repo that owns them.
That is the case for per-service Copilot instructions.
If you already have a workspace root for related repos, the next improvement is to give each repo its own instruction file. In the GitHub Copilot world, that usually means keeping repo-specific guidance in AGENTS.md or .github/copilot-instructions.md, while the workspace-level documentation explains how the repos fit together.
The distinction matters. The workspace explains the map. The service-level file explains how not to do damage once you are inside a specific repo.
The Workspace File Cannot Carry Everything
A workspace-level note is useful for shared context:
- which repo owns the contracts
- which repo publishes the shared package
- what order a cross-repo change usually follows
- where the high-value paths live
That is the right level for system-wide context. It is not the right level for service-specific invariants.
Suppose your workspace contains these repos:
~/workspace/platform-workspace/
├── WORKSPACE.md
├── Company.Billing.Contracts/
├── Company.Billing.Api/
├── Company.Jobs.Worker/
├── Company.Admin.Angular/
└── Company.Infrastructure/
This sample does one useful thing: it separates shared context from local editing rules. The workspace root is where you explain how the repos relate to each other. The individual repo folders are where service-specific instructions live. That distinction is what keeps the workspace note from turning into an unreadable catch-all document.
Now imagine you launch Copilot CLI from Company.Billing.Api and ask it to add a new property to a response.
The workspace note might tell the assistant that the contracts repo exists and that the Angular app consumes the API. Useful, but incomplete.
It will not necessarily tell the assistant that:
- response mapping must go through
BillingModelFactory - FluentValidation validators live beside endpoints, not in a shared validators folder
- API errors must use a shared problem-details factory
- direct SQL in handlers is forbidden because the repo uses a domain service plus repository boundary
- contract changes require a compatibility note before merge
Those are the rules that decide whether the suggestion is actually acceptable in that codebase.
If they are undocumented, the assistant will infer from nearby code. Sometimes that is enough. Sometimes it is not. And when the codebase contains both the preferred pattern and the old one that should not be copied anymore, inference gets messy fast.
What Per-Service Instructions Are Really For
I do not think of service-level instruction files as documentation for beginners. I think of them as a short operational brief for anyone, human or agent, making changes inside that repo.
The test I like is simple:
Would an experienced engineer still need to know this before making safe edits here, and is it something they could not reliably learn from a two-minute skim of the code?
If the answer is yes, it probably belongs in the per-service instruction file.
That includes things like:
- architecture decisions that are easy to violate from a local edit
- invariants that must stay true even if the code does not enforce them directly
- testing expectations that differ from the default
dotnet testornpm teststory - generated code boundaries
- forbidden patterns that still exist in old files and should not be copied forward
- local commands and setup steps that save time during normal work
It does not include every fact about the repo.
That is where these files usually go bad. They try to become README files, architecture decision records, onboarding guides, runbooks, and sprint notes all at once. Then nobody trusts them because half the content is stale.
A good per-service instruction file is short, blunt, and biased toward decisions that affect code changes.
A Better Split for Multi-Repo .NET Work
For a .NET-heavy workspace, I like this split:
WORKSPACE.mdat the shared root for cross-repo relationships and change orderAGENTS.mdor.github/copilot-instructions.mdinside each repo for service-local rules- normal codebase docs for everything else, such as API specs, runbooks, and detailed architecture material
That keeps the layers clean.
The workspace file answers questions like:
- Which repo owns this DTO?
- Which consumers need to be checked after a contract change?
- What repo order makes sense for this change?
The per-service file answers questions like:
- Where does new endpoint code go in this repo?
- What patterns are mandatory here?
- What shortcuts are forbidden even if they look faster?
- Which files are generated?
- What tests should run before a change is considered done?
That split is small, but it changes how useful Copilot CLI becomes once you are deep into a particular service.
What I Would Put in a Per-Service File
Here is the material I would actively keep in a service-level instruction file.
1. Structure that is not obvious from the folder tree
If the repo uses a pattern that a quick glance does not explain, say it plainly.
For example:
- endpoints are grouped by feature, with validators and mapping in the same folder
- MediatR handlers exist, but new code should go through feature services instead
- repositories are internal and should only be consumed from domain services
- integration events are published only from the application layer, never directly from controllers
These are the kinds of rules that stop the assistant from copying the wrong nearby example.
2. Critical invariants
This is the highest-value section.
If the repo has rules that must not be broken, put them here.
Examples:
- all API failures return the shared problem-details format
- all timestamps crossing service boundaries are UTC
- queue message contracts must remain backward compatible for one deployed version
- package references to shared contracts must stay aligned with the central version file
- database writes must go through the transaction boundary owned by the application service
Those rules are more important than stylistic preferences. They are the ones that prevent bad edits.
3. Generated-code boundaries
Agents are perfectly capable of editing the wrong thing if you do not tell them where the real source of truth lives.
This is especially common in mixed .NET and frontend work.
Examples:
- OpenAPI clients are generated and must not be edited directly
- TypeScript DTOs are generated from the backend contract package
- Entity Framework migrations are generated artifacts, but the model configuration is the real change surface
- Terraform lock files are committed, but module edits should happen in the reusable module, not the environment copy
If a file is generated, say that directly and say what command regenerates it.
4. Local validation expectations
Do not make the assistant guess what counts as done.
A short section like this is usually enough:
- run
dotnet test tests/Company.Billing.Api.UnitTests - run
dotnet test tests/Company.Billing.Api.IntegrationTestsfor endpoint or mapping changes - run
npm test -- billingafter Angular billing UI edits - run
terraform validatein the touched environment folder after infra changes
That keeps the repo-specific definition of done close to the repo.
5. Forbidden shortcuts
This is where a lot of practical value lives.
There are always patterns that appear in older files but are no longer acceptable.
Write them down plainly:
- do not call shared contracts from the UI through handwritten fetch logic; use the generated client wrapper
- do not read app settings directly inside controllers; use the typed options class
- do not publish queue messages from the controller layer
- do not introduce new AutoMapper profiles in this repo; mapping is explicit by policy
If you do not write these rules down, Copilot CLI may reproduce the exact pattern your team wants to retire.
What I Would Keep Out
Per-service instruction files get weak when they become a dumping ground.
I would keep these out:
- API documentation that belongs in OpenAPI, proto files, or contract docs
- long runbooks that belong in
docs/ - team ownership details that go stale every quarter
- current sprint tasks or ticket numbers
- generic language rules already enforced by linters, analyzers, or formatters
- details that are obvious from the folder structure and current code
The point is not to explain the entire repo. The point is to capture the non-obvious decisions that influence how new code should be written.
A Concrete Example for a Billing API Repo
If I were setting this up in Company.Billing.Api, I would keep the instruction file practical and specific. The sample repo includes a concrete .github/copilot-instructions.md like this:
# Company.Billing.Api Copilot Instructions
## Architecture
This API is organized by feature under `src/Features/`.
Each feature folder keeps the endpoint, validator, request model, and mapping together.
Do not add new cross-cutting "Helpers" folders for feature logic.
## Critical Rules
- All outbound DTO mapping goes through `BillingModelFactory`.
- All API errors must use the shared problem-details builder in `src/Infrastructure/Errors/`.
- New timestamps exposed by the API must be UTC and end with `Utc`.
- Controllers stay thin. Business logic belongs in application services.
## Contracts
- Contract types come from `Company.Billing.Contracts`.
- If a contract changes, check compatibility for `Company.Admin.Angular` and `Company.Jobs.Worker`.
- Do not copy contract types into local API models unless there is a versioning reason.
## Validation
- Validators live in the same feature folder as the endpoint.
- Use FluentValidation. Do not add data annotations to request models.
## Tests
- Run `dotnet test tests/Company.Billing.Api.UnitTests`
- Run `dotnet test tests/Company.Billing.Api.IntegrationTests` for endpoint changes
## Avoid
- Do not access configuration with `IConfiguration` inside controllers.
- Do not query the DbContext directly from endpoints.
- Do not add new shared utility folders unless the pattern already exists in `src/Infrastructure/`.
This sample works because each section gives the assistant a different kind of constraint. Architecture explains where new code belongs, Critical Rules captures invariants that should not be inferred, Contracts explains cross-repo consequences, Validation narrows the local pattern, Tests defines the minimum verification bar, and Avoid blocks shortcuts that might still appear in older files. That is the difference between a helpful instruction file and a generic README.
That is enough to change the quality of suggestions inside the repo.
It does not try to explain every project. It focuses on how this repo expects changes to be made.
The sample also includes a second repo-level file for a worker service:
# Company.Jobs.Worker Copilot Instructions
## Architecture
- Message handlers live under `src/Handlers/`.
- Retry-safe logic belongs in services, not in the transport layer.
## Critical Rules
- Handlers must be idempotent.
- Queue contracts come from shared packages, not local duplicate types.
## Tests
- Run `dotnet test tests/Company.Jobs.Worker.UnitTests`
That second example matters because it shows the pattern adapting to the repo instead of repeating the API template. The worker file is shorter because the repo has different failure modes. Idempotency and contract duplication are the real risks there, so those are the rules worth making explicit.
The Same Pattern Helps the Worker and the Frontend
The same idea works even better when the repos differ a lot.
For a Worker repo, the instruction file might emphasize:
- message compatibility rules
- retry and idempotency expectations
- where handlers live
- what logging shape is required
- whether dead-letter handling has a standard pattern
For an Angular repo, it might emphasize:
- generated API client boundaries
- where feature folders live
- how state is managed
- what must remain in a shared design-system package
- which commands regenerate types after backend contract changes
For an infrastructure repo, it might emphasize:
- naming conventions for resources
- where environment values belong
- what must never be hardcoded
- whether changes are validated locally or only in CI
- what modules are safe to touch versus shared foundations that affect many services
That is the practical reason this pattern works well in multi-repo environments. The workspace-level context is shared, but the editing rules are not.
Keep the Files Short Enough to Trust
I would rather have a 60-line instruction file people actually maintain than a 300-line one people ignore.
The more these files try to do, the faster they rot.
A few habits help keep them useful:
- update them when a pattern changes, not six months later
- remove warnings about patterns that no longer exist
- keep commands current enough that someone can actually run them
- avoid repeating content already maintained elsewhere
- review them whenever a repo changes its preferred structure or boundaries
If you want process support, add a small check to the PR template:
## Copilot Instructions Check
- [ ] If this PR changes a repo-level pattern or invariant, update `AGENTS.md` or `.github/copilot-instructions.md`
- [ ] If this PR removes a deprecated pattern, remove that warning from the instruction file
This is a small sample, but it matters because it ties the documentation to normal delivery flow. Instead of hoping someone remembers to update repo instructions after an architectural change, the PR checklist turns that into an explicit review step.
That is usually enough to keep the file honest.
I also like backing that expectation with a tiny validator script. The sample includes one that scans each repo for AGENTS.md or .github/copilot-instructions.md and fails if the required sections are missing:
#!/usr/bin/env bash
set -euo pipefail
repo_root="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/repos}"
required_sections=("## Architecture" "## Critical Rules" "## Tests")
found_files=0
while IFS= read -r file; do
found_files=$((found_files + 1))
echo "Checking $file"
for section in "${required_sections[@]}"; do
if ! grep -Fq "$section" "$file"; then
echo "Missing section '$section' in $file" >&2
exit 1
fi
done
done < <(find "$repo_root" \( -name AGENTS.md -o -path '*/.github/copilot-instructions.md' \) -type f | sort)
if [[ "$found_files" -eq 0 ]]; then
echo "No instruction files found in $repo_root" >&2
exit 1
fi
echo "Validated $found_files instruction file(s)"
This kind of script is useful because it keeps the convention lightweight. You are not building a policy engine. You are just preventing the most common failure mode, which is that one repo never gets an instruction file or someone deletes a section and nobody notices.
The sample test for that validator is equally simple:
bash ./tests/test-instructions.sh
That detail is worth putting in the article because it shows the instruction files as part of normal engineering hygiene, not as optional prose that only exists for AI demos.
Why This Helps GitHub Copilot CLI Specifically
GitHub Copilot CLI gets better when the operating context is specific.
At the workspace level, that means seeing the related repos together.
At the repo level, it means knowing the local rules before it starts suggesting changes.
Without that second layer, the assistant can still search the codebase. But search is not the same thing as knowing which pattern is preferred, which shortcut is banned, or which generated files are off-limits.
That difference matters most in mature codebases where old and new approaches coexist.
If the repo contains three ways to do mapping and only one is still acceptable, I do not want the assistant guessing. I want the repo to say so.
That is the real value of per-service instruction files. They cut down on re-explanation. They reduce bad local guesses. They help the assistant act more like someone who already knows the repo's working rules.
Closing Thought
A shared workspace gives Copilot CLI the system view. Per-service instructions give it local judgment.
You need both.
If the workspace file explains how the repos relate, the service-level file should explain how to behave once you enter one of them. That is where you put the invariants, boundaries, commands, and forbidden shortcuts that are too local for workspace docs and too important to leave implicit.
For multi-repo .NET teams, that is a practical next step after setting up a shared workspace. It keeps the context layered, keeps the rules close to the code that owns them, and makes Copilot CLI noticeably more useful in day-to-day service work.
If you want the earlier pieces in this series, start with Multi-Repo Workspace Setup for GitHub Copilot CLI: Context Across .NET Service Boundaries, then follow it with Repo Registry for GitHub Copilot CLI: Service Discovery for Multi-Repo .NET Workflows. This article fits after those two: first share the workspace, then expose the repos, then teach each repo how it wants changes to be made.


