Teaching Copilot Your Project: copilot-instructions.md, *.instructions.md with applyTo, AGENTS.md and Reusable Prompt Files
Keep the rules in one place in AGENTS.md, copy them exactly into copilot-instructions.md and guard the copy with a script, because some Copilot surfaces still do not read AGENTS.md and Copilot CLI loads CLAUDE.md too. applyTo and excludeAgent, which surface reads what, copilot instruction list output, and the status of prompt files in Visual Studio and VS Code.
Version note: The files and code in this post were tried in the Bookstore repo with .NET 10; the list of instruction sources was taken with the copilot instruction list command of GitHub Copilot CLI 1.0.112, without signing in. Information about which surface reads which file was taken from docs.github.com, the Visual Studio 2026 18.12 release notes and the Visual Studio Code (VS Code) 1.146 documentation on 15 November 2026; the effect of the instructions on a Copilot answer was not tried in this post.
Guess First
The team wrote Copilot instructions for Bookstore, the online bookstore used as the shared example across this site's series; the instruction for .NET files has the rule "use explicit types instead of var". A developer who uses Claude Code in the same repo adds a CLAUDE.md with their own notes to the root and writes a single line in it: "Use var everywhere."
What is your guess? Which of these two files does Copilot read? If it reads both, which one wins? We will see part of the answer in Copilot CLI's own output.
Instruction Layers
In the third post we rebuilt the context for every question. The parts of a project that do not change (build and test commands, architecture rules, naming) should not be explained again in every chat; Copilot reads them from files. As of 15 November 2026 the layers are these:
Repository-wide instructions: .github/copilot-instructions.md. It is added to every request in the context of the repo; plain Markdown, free format.
Path-specific instructions: files under .github/instructions/ whose names end in .instructions.md. The applyTo glob in the frontmatter at the top says which files they apply to; several globs can be given with commas. Globs are written relative to the repo root, not to the instruction file's folder. On GitHub.com the cloud agent and code review read these files; the excludeAgent field with the value "code-review" or "cloud-agent" hides the file from one of these two. A matching path-specific instruction and the repository-wide instruction are used together.
Agent instructions: AGENTS.md can be placed in any folder of the repo; when Copilot works on a file, the nearest AGENTS.md in the directory tree takes precedence. A single CLAUDE.md or GEMINI.md in the root is also read as agent instructions.
Personal and organization instructions: Copilot CLI and VS Code's Copilot harness read personal instructions from ~/.copilot/copilot-instructions.md, Visual Studio from %USERPROFILE%/copilot-instructions.md. Organization owners can also define instructions on GitHub that apply to all repos. On GitHub.com the priority is personal, then repository, then organization; but all three are sent to the model. The VS Code documentation says it plainly: there is no precedence rule that fits every harness, and Copilot combines the applicable instructions.
We explained the AGENTS.md concept and why we chose it as the single source in the second post of the AI-Assisted Development series. This post is how that decision is applied in Copilot.
Which Surface Reads Which File
This is where the real confusion is. According to GitHub's "custom instructions support" table (15 November 2026):
| Surface | copilot-instructions.md | *.instructions.md | AGENTS.md | CLAUDE.md, GEMINI.md | Personal | Organization |
|---|---|---|---|---|---|---|
| GitHub.com Chat | Yes | No | No | No | Yes | Yes |
| GitHub.com cloud agent | Yes | Yes | Yes | Yes | No | Yes |
| GitHub.com code review | Yes | Yes | Yes | Yes | No | Yes |
| VS Code Chat | Yes | Yes | Yes | No | No | No |
| VS Code code review | Yes | No | No | No | No | No |
| Visual Studio Chat | Yes | Yes | No | No | No | No |
| Visual Studio code review | Yes | No | No | No | No | No |
| JetBrains Chat | Yes | Yes | No | No | Yes | No |
| Copilot CLI | Yes | Yes | Yes | Yes | Yes | No |
This table can lag behind the IDEs' release notes. The Visual Studio 2026 18.12 release notes of 10 November 2026 say that the new Copilot Chat experience uses the AGENTS.md at the solution root and organization instructions; VS Code's own documentation also lists the user-level ~/.copilot instructions for the Copilot harness. Two notes for code review too: a review reads the instructions from the pull request's head branch, and code review on GitHub.com also accepts a REVIEW.md file as instructions. Review-specific instructions are the topic of the eighth post.
The practical result of the table: there are still surfaces that do not read AGENTS.md (GitHub.com Chat, code review in the IDEs, older Chat experiences). If you write the rules only in AGENTS.md, these surfaces do not see them; if you write them only in copilot-instructions.md, Claude Code and Codex do not see them.
Bookstore's Instructions
Our decision: the rules are written in one place, the root AGENTS.md; copilot-instructions.md carries an exact copy of that section, and a script prevents the copy from drifting. The root AGENTS.md:
# Bookstore
Online bookstore used as the shared example of the mertozen.com series.
.NET 10 Web API in `src/Bookstore.Api`, xUnit v3 integration tests in `tests/Bookstore.Api.Tests`,
Angular 22 client in `web/bookstore-web` (see its own AGENTS.md).
## Commands
- Build: `dotnet build`
- Test: `dotnet test` (needs SQL Server; connection string `ConnectionStrings__BookstoreDb`, see README.md)
- New migration: `dotnet ef migrations add <Name> --project src/Bookstore.Api --output-dir Data/Migrations`
## Rules
<!-- rules:start -->
- Modules live in folders under `src/Bookstore.Api` (Catalog, Ordering, Inventory, Institutional); one SQL schema per module.
- A module never reads another module's tables; it calls that module's service (`ICatalogService`, `IInventoryService`).
- Money is `decimal`, currency `TRY`; round line totals with `MidpointRounding.AwayFromZero`.
- Times are UTC (`TimeProvider`, `DateTime` in UTC).
- Endpoints return `TypedResults` and ProblemDetails for errors; no exceptions for expected outcomes.
- Every behavior change comes with a test; never weaken or delete an existing assertion to make a test pass.
- Never commit secrets or connection strings; use user-secrets or environment variables.
<!-- rules:end -->
.github/copilot-instructions.md contains only the commands and the same rules block:
# Copilot instructions for Bookstore
Build with `dotnet build`, test with `dotnet test`; the Angular client is in `web/bookstore-web` (`npm test`).
These rules are a copy of the Rules section in AGENTS.md for Copilot surfaces that do not read AGENTS.md.
Edit AGENTS.md first; `dotnet run eng/check-instructions.cs` fails when the two copies differ.
<!-- rules:start -->
- Modules live in folders under `src/Bookstore.Api` (Catalog, Ordering, Inventory, Institutional); one SQL schema per module.
- A module never reads another module's tables; it calls that module's service (`ICatalogService`, `IInventoryService`).
- Money is `decimal`, currency `TRY`; round line totals with `MidpointRounding.AwayFromZero`.
- Times are UTC (`TimeProvider`, `DateTime` in UTC).
- Endpoints return `TypedResults` and ProblemDetails for errors; no exceptions for expected outcomes.
- Every behavior change comes with a test; never weaken or delete an existing assertion to make a test pass.
- Never commit secrets or connection strings; use user-secrets or environment variables.
<!-- rules:end -->
Two path-specific instruction files carry the .NET and Angular rules only to the relevant files. This way, Entity Framework Core (EF Core) rules do not fill the context while you work on an Angular component:
---
applyTo: "src/**/*.cs,tests/**/*.cs"
---
- C# 14 on .NET 10: file-scoped namespaces, primary constructors, collection expressions, explicit types instead of `var` (except anonymous types).
- EF Core configuration goes in an `IEntityTypeConfiguration<T>` class next to the entity, with `ToTable("<Name>", "<schema>")`, `HasMaxLength` on every string and `HasPrecision` on every `decimal`.
- Read-only queries use `AsNoTracking()` and project to a record with `Select`; do not return entities from endpoints.
- Pass `CancellationToken` to every async EF Core and HTTP call.
- Integration tests use `BookstoreApiFactory` and `TestContext.Current.CancellationToken`; name tests as sentences (`Placing_an_order_reserves_stock`).
---
applyTo: "web/bookstore-web/src/**/*.ts,web/bookstore-web/src/**/*.html"
---
- Standalone components with `inject()`; templates use `@if` and `@for` with `track`.
- Keep component state in signals; derive values with `computed` instead of methods called from the template.
- Show money with the `currency: 'TRY' : 'symbol-narrow'` pipe.
- Specs use Vitest globals (`describe`, `it`, `expect`) with `TestBed`; mock HTTP with `provideHttpClientTesting`.
The third file is only for review: excludeAgent: "cloud-agent" hides it from the cloud agent, while code review reads it. These rules are aimed at the reviewer reading a change, not at the agent writing code:
---
applyTo: "**"
excludeAgent: "cloud-agent"
---
When reviewing a pull request in this repository, check these first:
- A query inside a loop, or `Include` chains that load more than the endpoint returns.
- A new `decimal` property without `HasPrecision`, or a new string without `HasMaxLength`.
- A migration that drops or renames a column without a data step.
- A changed or deleted assertion in `tests/`; ask why the expected value changed.
- In Angular, a subscription without cleanup or state kept outside signals.
Finally, the Angular client has a second AGENTS.md in its own folder: npm commands and signal rules. When the agent works under web/bookstore-web, this is the nearest file.
Checking the Instructions
Instruction files break like code: a folder is moved and applyTo matches no file, someone updates AGENTS.md but forgets the copy. Both are silent failures; Copilot gives no error, it just does not apply the instruction. For this we added a check script to the repo:
#:property PublishAot=false
// Checks Copilot instruction and prompt files: frontmatter, applyTo globs, AGENTS.md copy, size.
// Run from the repository root: dotnet run eng/check-instructions.cs
using System.Diagnostics;
using System.Text.RegularExpressions;
const int AlwaysLoadedBudget = 8_000; // our own budget in characters, not a Copilot limit
List<string> errors = [];
ProcessStartInfo git = new("git", "ls-files") { RedirectStandardOutput = true };
using Process process = Process.Start(git)!;
string[] tracked = (await process.StandardOutput.ReadToEndAsync()).Split('\n', StringSplitOptions.RemoveEmptyEntries);
await process.WaitForExitAsync();
foreach (string file in tracked.Where(f => f.StartsWith(".github/instructions/") && f.EndsWith(".instructions.md")))
{
Dictionary<string, string> meta = Frontmatter(file);
if (!meta.TryGetValue("applyTo", out string? applyTo))
{
errors.Add($"{file}: no applyTo, the file is never applied automatically");
continue;
}
foreach (string glob in applyTo.Split(',', StringSplitOptions.TrimEntries))
{
int matches = tracked.Count(f => GlobToRegex(glob).IsMatch(f));
Console.WriteLine($"{file}: applyTo {glob} -> {matches} files");
if (matches == 0)
{
errors.Add($"{file}: applyTo '{glob}' matches no tracked file");
}
}
if (meta.TryGetValue("excludeAgent", out string? agent) && agent is not ("code-review" or "cloud-agent"))
{
errors.Add($"{file}: excludeAgent must be code-review or cloud-agent, not '{agent}'");
}
}
foreach (string file in tracked.Where(f => f.StartsWith(".github/prompts/") && f.EndsWith(".prompt.md")))
{
Dictionary<string, string> meta = Frontmatter(file);
Console.WriteLine($"{file}: /{meta.GetValueOrDefault("name", Path.GetFileName(file).Replace(".prompt.md", ""))} (agent: {meta.GetValueOrDefault("agent", "current")})");
if (!meta.ContainsKey("description"))
{
errors.Add($"{file}: description is missing");
}
}
string agentsRules = Rules("AGENTS.md");
string copilotRules = Rules(".github/copilot-instructions.md");
if (agentsRules != copilotRules)
{
errors.Add(".github/copilot-instructions.md: rules differ from AGENTS.md, copy the Rules section again");
}
long alwaysLoaded = new FileInfo("AGENTS.md").Length + new FileInfo(".github/copilot-instructions.md").Length;
Console.WriteLine($"Always loaded (AGENTS.md + copilot-instructions.md): {alwaysLoaded:N0} characters, budget {AlwaysLoadedBudget:N0}");
if (alwaysLoaded > AlwaysLoadedBudget)
{
errors.Add($"always-loaded instructions are {alwaysLoaded:N0} characters, over the {AlwaysLoadedBudget:N0} budget");
}
foreach (string error in errors)
{
Console.Error.WriteLine($"error: {error}");
}
Console.WriteLine(errors.Count == 0 ? "OK" : $"{errors.Count} error(s)");
return errors.Count == 0 ? 0 : 1;
static Dictionary<string, string> Frontmatter(string path)
{
string[] lines = File.ReadAllLines(path);
Dictionary<string, string> values = [];
if (lines.Length == 0 || lines[0] != "---")
{
return values;
}
foreach (string line in lines.Skip(1).TakeWhile(l => l != "---"))
{
int colon = line.IndexOf(':');
if (colon > 0)
{
values[line[..colon].Trim()] = line[(colon + 1)..].Trim().Trim('"', '\'');
}
}
return values;
}
static string Rules(string path)
{
Match match = Regex.Match(File.ReadAllText(path), "<!-- rules:start -->(.*?)<!-- rules:end -->", RegexOptions.Singleline);
return match.Success ? match.Groups[1].Value.Trim() : "";
}
// "**" spans folders, "*" and "?" stay inside one path segment.
static Regex GlobToRegex(string glob) =>
new("^" + Regex.Escape(glob)
.Replace(@"\*\*/", "(.*/)?")
.Replace(@"\*\*", ".*")
.Replace(@"\*", "[^/]*")
.Replace(@"\?", "[^/]") + "$");
$ dotnet run eng/check-instructions.cs
.github/instructions/angular.instructions.md: applyTo web/bookstore-web/src/**/*.ts -> 10 files
.github/instructions/angular.instructions.md: applyTo web/bookstore-web/src/**/*.html -> 5 files
.github/instructions/code-review.instructions.md: applyTo ** -> 90 files
.github/instructions/dotnet.instructions.md: applyTo src/**/*.cs -> 29 files
.github/instructions/dotnet.instructions.md: applyTo tests/**/*.cs -> 5 files
.github/prompts/add-migration.prompt.md: /add-migration (agent: agent)
.github/prompts/new-endpoint.prompt.md: /new-endpoint (agent: agent)
Always loaded (AGENTS.md + copilot-instructions.md): 2,334 characters, budget 8,000
OK
We tried the script with two common mistakes: we wrote the HTML glob in angular.instructions.md as src/app/**/*.html, thinking relative to where the instruction file lives, and we changed a rule in AGENTS.md without updating the copy:
$ dotnet run eng/check-instructions.cs
.github/instructions/angular.instructions.md: applyTo web/bookstore-web/src/**/*.ts -> 10 files
.github/instructions/angular.instructions.md: applyTo src/app/**/*.html -> 0 files
.github/instructions/code-review.instructions.md: applyTo ** -> 90 files
.github/instructions/dotnet.instructions.md: applyTo src/**/*.cs -> 29 files
.github/instructions/dotnet.instructions.md: applyTo tests/**/*.cs -> 5 files
.github/prompts/add-migration.prompt.md: /add-migration (agent: agent)
.github/prompts/new-endpoint.prompt.md: /new-endpoint (agent: agent)
Always loaded (AGENTS.md + copilot-instructions.md): 2,346 characters, budget 8,000
error: .github/instructions/angular.instructions.md: applyTo 'src/app/**/*.html' matches no tracked file
error: .github/copilot-instructions.md: rules differ from AGENTS.md, copy the Rules section again
2 error(s)
The first error is a reminder that globs are written relative to the repo root: src/app/** matches no file in Bookstore. When this check runs in CI, an instruction that Copilot would silently not apply is caught at the pull request stage.
The second check tool is Copilot CLI itself: copilot instruction list lists which instruction sources a session would load, without needing a login. In Bookstore's root and in the Angular folder:
$ copilot instruction list
Repository instructions:
• .github/copilot-instructions.md
from .github/copilot-instructions.md
• AGENTS.md
from AGENTS.md
• angular.instructions.md
from .github/instructions/angular.instructions.md
• dotnet.instructions.md
from .github/instructions/dotnet.instructions.md
$ cd web/bookstore-web
$ copilot instruction list
Repository instructions:
• .github/copilot-instructions.md
from .github/copilot-instructions.md
• AGENTS.md
from AGENTS.md
• angular.instructions.md
from .github/instructions/angular.instructions.md
• dotnet.instructions.md
from .github/instructions/dotnet.instructions.md
Working directory instructions:
• AGENTS.md
from web/bookstore-web/AGENTS.md
Two things stand out. In the Angular folder, the nested AGENTS.md is loaded separately as "Working directory instructions". And code-review.instructions.md is not in the list: CLI 1.0.112 leaves out a file with excludeAgent: "cloud-agent" for itself too. When we changed the value to "code-review" or removed the field, the file entered the list. We did not find this behavior in the documentation; we observed it in the CLI, and a review-only file staying out of the agents that write code is what we want anyway. The --json option also shows each source's applyTo list; the two globs we wrote with a comma were read separately.
Now back to Guess First. We put a CLAUDE.md with "Use var everywhere." in the root and took the list again:
$ copilot instruction list
Repository instructions:
• .github/copilot-instructions.md
from .github/copilot-instructions.md
• AGENTS.md
from AGENTS.md
• CLAUDE.md
from CLAUDE.md
• angular.instructions.md
from .github/instructions/angular.instructions.md
• dotnet.instructions.md
from .github/instructions/dotnet.instructions.md
The CLI loads CLAUDE.md too. Given that Copilot combines instructions and has no general precedence rule, the answer is: both enter the context, and which one is followed is up to the model. A conflicting instruction is worse than the worst instruction, because it makes behavior unpredictable. The solution, if several tools are used in the same repo, is to keep the rules in AGENTS.md and limit CLAUDE.md to small differences specific to Claude Code.
Prompt Files: Repeated Work
Instructions apply to every request; prompt files are repeated tasks that you invoke. We added two to Bookstore: a new endpoint with a test, and adding a migration.
---
name: new-endpoint
description: Add a minimal API endpoint with an integration test
agent: agent
argument-hint: "[module] [HTTP method and route]"
---
Add the endpoint ${input:route:GET /api/...} to the ${input:module:Ordering} module.
1. Read the module's `*Module.cs` and follow how its existing endpoints are mapped and typed.
2. Return `TypedResults`; describe expected failures with ProblemDetails.
3. Add an integration test in `tests/Bookstore.Api.Tests` that fails before the change and passes after it.
4. Run `dotnet build` and `dotnet test` and report the test summary.
Do not change existing tests. Stop and ask if the endpoint needs a new table.
The agent field in the frontmatter sets which mode the prompt runs in, and argument-hint sets the hint shown in the chat box; ${input:route:...} asks you for a value when it runs. The rules in the last line, "do not change existing tests" and "stop and ask if a new table is needed", will matter in the next post when we work with the agent.
The status of prompt files differs by surface. In Visual Studio, files under .github/prompts/ appear at the top of the list when you type / in chat, or are added with #prompt:; /savePrompt creates a prompt file from the current conversation, and /generateInstructions examines the project and produces a draft copilot-instructions.md. In VS Code, as of 1.146, prompt files only run in the deprecated Local harness; a .prompt.md written in the Copilot harness is not runnable, and the documentation recommends converting prompt files to agent skills before the Local harness is removed in February 2027. Skills are the topic of the sixth post; Bookstore's two prompt files stay in place for Visual Studio users, and we will write their skill counterparts in the sixth post.
In Production
An instruction is a request, not a security control. Writing "do not commit secrets" does not mean the agent cannot do it; access control, content exclusion and secret scanning are separate layers.
Changes to instruction files need review too. Because code review reads instructions from the pull request's head branch, a pull request can change its own review rules. Assigning an owner to changes under .github/instructions/ and to AGENTS.md with CODEOWNERS makes this visible.
Measure the size. Every line enters the context on every request; in the calculation in the first post we saw that a 20-thousand-token starting context is resent at every step. In Bookstore the two always-loaded files total 2,334 characters; the 8,000-character budget in the script is our decision, not a Copilot limit.
Common Pitfalls
Bloating the instruction file. Instead of copying the wiki, write the rules the agent cannot work out from the code; the agent reads the rest from the code.
Conflicting instructions when Claude Code and Codex are also used in the same repo. Copilot CLI loads CLAUDE.md too; the two files must not have different rules on the same topic.
Writing applyTo relative to the instruction file's folder. Globs are relative to the repo root; the check script catches this mistake.
Thinking path-specific instructions work on every surface. GitHub.com Chat and code review in the IDEs do not read these files.
Continuing to write new prompt files in VS Code. They do not run in the Copilot harness; write a skill for new repeated work.
A Small Experiment
★☆☆ Run copilot instruction list in your own repo. Is there a source you did not expect (an old CLAUDE.md, a personal instruction)?
★★☆ Add a separate tests.instructions.md for tests/**/*.cs to Bookstore and move the test rules there from dotnet.instructions.md. Run the check script; then break the glob on purpose and see the error.
★★★ Run eng/check-instructions.cs as a GitHub Actions step and add a CODEOWNERS line for AGENTS.md and .github/instructions/. Then change only code-review.instructions.md in a pull request and see how this shows up during review.
The instructions and prompt files are ready. In the next post we do multi-step work with the agent that uses them: tool approvals, terminal commands, checkpoints, and adding the "at least 20 books for institutional orders" rule to Bookstore with the API, Angular and tests.