copilot-instructions.md Examples and a Template
Three annotated .github/copilot-instructions.md files, for a TypeScript web app, a Python service and a .NET API, plus a path-specific .instructions.md file, a template to copy, and the lines that should never be in any of them.
7 min read
A good .github/copilot-instructions.md tells Copilot four things it cannot work out quickly on its own: what the repository is, the exact commands to build and test it, where things live, and the conventions that differ from what a model would do by default. It is short, written as plain bullet points, and says nothing about the task in front of you today. Below are three realistic files, for a TypeScript web app, a Python service and a .NET API, each followed by why its lines are there. Then a path-specific file, a template to copy, and the lines that do not belong. Which Copilot surface reads which file, and what happens when several apply, is covered in does GitHub Copilot support AGENTS.md?; this post is only about what to write.
What GitHub asks the file to do
GitHub’s guide to adding repository custom instructions (opens in a new tab) frames the file around an agent seeing the repository for the first time. Its suggested content is a summary of what the repository does, the languages and runtimes, the steps to bootstrap, build, test, run and lint with the versions of the tools, the layout, and the checks a change must pass. Its two limits: no longer than two pages, and not task-specific. Whitespace between instructions is ignored, so short bullets under headings are as valid as a paragraph, and far easier to maintain.
Example 1: a TypeScript web app
# Orders dashboard React 19 single-page app built with Vite and TypeScript (strict). Talks to the orders API over REST; no server-side rendering. Node 22, npm. ## Build and test - Install: `npm ci` (never `npm install` in CI or when the lockfile is unchanged) - Dev server: `npm run dev` on http://localhost:5173 - Before any commit: `npm run lint && npm run typecheck && npm test` - One test file: `npx vitest run src/orders/filters.test.ts` - End-to-end: `npm run e2e` (needs `npm run dev` running in another terminal) ## Layout - src/features/<feature>/ holds components, hooks and tests for one feature - src/api/client.ts is the only place that calls fetch() - src/generated/ is written by `npm run codegen` from openapi.yaml; never edit it ## Conventions - Function components and hooks only. No default exports. - Server state goes through TanStack Query; do not copy it into useState. - Money arrives as integer pence; format it only with formatMoney(). - Every new component gets a test next to it: Thing.tsx, Thing.test.tsx.
- The opening lines are the summary GitHub asks for: framework, language mode, runtime, package manager. “No server-side rendering” heads off a whole class of wrong suggestions.
- Build and test lists commands in the order they run, and flags the one that silently needs something else running. That dependency is exactly what an agent otherwise discovers by failing.
- The single-test command matters as much as the full suite. An agent checking one change should not wait for everything.
- Layout names three places, not the whole tree. The generated folder is the classic rule no one can infer from the code.
- Each convention is checkable. “Use good component design” is not; “no default exports” is.
Example 2: a Python service
# Notifications service FastAPI service that sends email and SMS for other internal services. Python 3.12, managed with uv. PostgreSQL via SQLAlchemy 2.0; migrations with Alembic. ## Build and test - Set up: `uv sync`, then `docker compose up -d db` (tests need the database) - Tests: `uv run pytest`; one test: `uv run pytest tests/test_send.py -k retry` - Lint and types: `uv run ruff check . && uv run mypy app` - New migration: `uv run alembic revision --autogenerate -m "<what changed>"`, then read the generated file before committing; autogenerate misses renames ## Layout - app/api/ routes, app/services/ business logic, app/providers/ one module per email or SMS provider - CI runs .github/workflows/ci.yml: ruff, mypy, pytest. All three must pass. ## Conventions - Routes stay thin: validate, call a service, return. No SQL in app/api/. - Type hints on every function signature; mypy runs in strict mode. - Never call a provider in tests. Use the fakes in tests/fakes/. - Log with the structured logger in app/log.py, never print().
- The database line explains why tests fail on a clean machine. Without it, an agent will try to mock the database or install PostgreSQL itself.
- The migration line carries a warning with a reason. Instructions that say why are followed more sensibly at the edges than bare rules.
- Naming the CI workflow and its three checks tells the agent what “done” means here, which is the check GitHub specifically asks you to document.
- “Never call a provider in tests” pairs a prohibition with what to do instead. A bare “never” leaves the agent stuck or guessing.
Example 3: a .NET API
# Bookings API ASP.NET Core Web API on .NET 10, EF Core with SQL Server. Solution: Bookings.sln. ## Build and test - Build: `dotnet build Bookings.sln` - Tests: `dotnet test`; one class: `dotnet test --filter "FullyQualifiedName~RefundTests"` - Format check (CI fails without it): `dotnet format --verify-no-changes` - New migration: `dotnet ef migrations add <Name> --project src/Bookings.Data --startup-project src/Bookings.Api` ## Layout - src/Bookings.Api controllers and startup, src/Bookings.Domain entities and rules, src/Bookings.Data DbContext and migrations, tests/ one project per src project ## Conventions - Nullable reference types are on; do not add ! to silence a warning. - Async all the way down; pass CancellationToken from the controller. - Errors return ProblemDetails; never return a bare string. - Never edit a migration that has been merged. Add a new one. - Local secrets live in `dotnet user-secrets`, not appsettings.Development.json.
- The filter syntax for one test class is the line people look up every time. Written once, it saves the agent a search on every change.
- The format check is flagged as a CI failure, so the agent runs it before it declares the work done rather than after a red build.
- The migration command carries both project flags. Getting them wrong produces a migration in the wrong assembly, which is easy to miss in review.
- The nullable rule forbids the shortcut an agent under pressure is most likely to take. Naming the shortcut is more effective than a general “fix warnings properly”.
A path-specific file with applyTo
Rules that only make sense for some files go in .github/instructions/NAME.instructions.md, with an applyTo glob in the frontmatter. Several patterns are separated by commas, and excludeAgent can keep a file away from "code-review" or "cloud-agent". This one applies to the .NET example’s test projects:
--- applyTo: "tests/**/*.cs" --- - xUnit with FluentAssertions. One behaviour per test; name it Method_Condition_ExpectedResult. - Build test data with the builders in tests/Bookings.Testing/Builders; never new up an entity with a long constructor call. - Integration tests use the Testcontainers fixture in IntegrationFixture.cs. Do not point a test at a shared database. - A bug fix starts with a failing test that reproduces it.
Keep each path-specific file to one concern: tests, migrations, a component library. In VS Code, its custom instructions page (opens in a new tab) also accepts name and description in the frontmatter, and an agent can load a file on demand when its description matches the task, which is useful for rules tied to a kind of work rather than a folder.
A template to copy
# <Project name> <One or two sentences: what this is, who uses it.> <Language and version, framework, package manager, database.> ## Build and test - Install: `<command>` - Run locally: `<command>` (<port or URL>) - Before committing: `<lint> && <typecheck> && <test>` - One test: `<command for a single file or test>` - <Anything that must be running first, and how to start it> ## Layout - <folder>: <what lives there> - <generated or vendored folder>: never edit; regenerate with `<command>` ## Conventions - <A rule that differs from the default, stated so it can be checked> - <Another, with the reason if it is not obvious> ## Checks a change must pass - <CI workflow name>: <what it runs> ## Work tracking - <Where tasks live, and when to read and update them>
Fill it in, then delete every line an agent could learn in ten seconds from the code. What remains should fit on one screen for most repositories.
Lines that do not belong
- Task-specific notes: this week’s bug, the feature in progress, who is on leave. The file is read on every request and goes stale in days.
- Vague quality wishes: “write clean code”, “be accurate”, “do not miss anything”. GitHub’s tutorial on writing custom instructions for code review (opens in a new tab) lists these as noise that does not change the result.
- Links to standards held elsewhere. The same tutorial says Copilot code review does not follow external links; paste the few rules that matter instead.
- Instructions about the look of Copilot’s own output, such as the format of review comments. Code review does not support them.
- The whole directory tree or dependency list. The agent can read those; describe only the parts it would get wrong.
- Secrets, connection strings or customer details. The file is committed and read by every session.
- Rules that must never be broken. Instructions are guidance a model tries to follow, not enforcement; protect the branch or the file with settings that do not depend on a model reading a line.
On length, two pages is GitHub’s ceiling for this file, and its code review tutorial puts about 1,000 lines as the most any single instruction file should hold. Copilot code review used to stop reading an instructions file at 4,000 characters, but GitHub removed that limit (opens in a new tab) in June 2026, so the reason to stay short is attention, not truncation. Aim far lower. Every line competes with the others for attention, and the rule that matters is easier to miss in a long file.
Checking the file works
- Draft it rather than starting blank. GitHub’s cloud agent can write one from the Agents tab, and
/initin the Copilot CLI writes or improves.github/copilot-instructions.mdfrom the codebase; how to run the CLI is in GitHub Copilot CLI. Then cut hard. - Run every command in it yourself on a clean checkout. A wrong command in this file is worse than none.
- In Copilot Chat, expand the references on a response and check the instructions file is listed.
- Open a small pull request and ask Copilot for a review. Code review reads the instructions from the pull request’s branch, so you can test a change to the file in the same pull request.
- When Copilot repeats a mistake the file already covers, shorten the file before adding emphasis.
The work list is not a line in this file
The template ends with “Work tracking” for a reason: the one thing people most often paste into instructions is a list of tasks, and it is the thing that goes stale fastest. Keep it to a pointer. With fenbs connected to Copilot over MCP, the line names the board’s project and says when to use it: “Before starting, call fenbs_get_context and read the task you were given with fenbs_get_item; when you stop, comment on it with what changed and how it was tested.” The tasks, their lanes (To Do, Next Up, In Progress, Completed) and the record of who changed what stay on the board, where a colleague using a different assistant sees the same list. If the repository also has an AGENTS.md, put the fuller version of those lines there; the AGENTS.md equivalents of these examples are in AGENTS.md examples.
Related
Connect Copilot in VS Code to a board: GitHub Copilot integration. The same exercise for Claude Code: CLAUDE.md examples. Keeping instructions lean alongside everything else Copilot reads: context engineering with GitHub Copilot.