Explainer

GitHub Copilot in a Monorepo: Scoped Instructions for Frontend, Backend, and Terraform

A shared copilot-instructions.md plus three applyTo files for a TypeScript frontend, a FastAPI backend and Terraform — how they combine, how to debug a pattern, how to keep them true.

GitHub Copilot in a Monorepo: Scoped Instructions for Frontend, Backend, and Terraform/blog/github-copilot-monorepo-instructions/

Short answer. In a monorepo, keep .github/copilot-instructions.md for the rules that are true everywhere — layout, how work is validated, what never goes in a prompt — and put each stack’s rules in a path-specific file under .github/instructions/ with an applyTo glob that matches only that stack’s files. GitHub’s documentation says a matching path-specific file and the repository-wide file are both used, so the scoped files add to the shared one; they do not replace it, and they must not contradict it. The four complete files below cover a TypeScript frontend, a Python/FastAPI backend and Terraform infrastructure, and the validation section shows how to prove the right file applied — including in the CLI, which combines files rather than ranking them.

What each Copilot client reads was taken from GitHub’s support reference on 18 September 2026 and is stated per client below. The behaviour that differs most between clients — what happens with more than one AGENTS.md — is stated only where the documentation states it.

The repository

A realistic shape, not a toy: three stacks, three toolchains, one repository.

acme-platform/
├── .github/
│   ├── copilot-instructions.md
│   ├── instructions/
│   │   ├── frontend.instructions.md
│   │   ├── backend.instructions.md
│   │   └── infrastructure.instructions.md
│   └── workflows/
│       ├── ci.yml
│       └── copilot-setup-steps.yml
├── frontend/
│   ├── src/
│   ├── package.json
│   └── tsconfig.json
├── backend/
│   ├── app/
│   ├── tests/
│   └── pyproject.toml
├── infrastructure/
│   ├── modules/
│   ├── environments/
│   └── versions.tf
└── README.md

The problem a single instruction file has here is not length; it is relevance. A rule about Decimal for money is noise on a React component; a rule about for_each is noise on a FastAPI route. Noise costs context on every request and, worse, trains the reader to skim the file — and a rule that is skimmed is a rule that stops being maintained.

How the files combine

The mechanism, as GitHub documents it:

  • Repository-wide instructions live in .github/copilot-instructions.md and apply to every request in the repository’s context.
  • Path-specific instructions are NAME.instructions.md files under .github/instructions/ (subdirectories allowed), each starting with a frontmatter block whose applyTo is a glob or a comma-separated list of globs. They apply when the file Copilot is working on matches.
  • When both apply: “If the path you specify matches a file that Copilot is working on, and a repository-wide custom instructions file also exists, then the instructions from both files are used.”

So the design rule is simple: the shared file holds what is true for every file; a scoped file holds only what is true for its subtree and nothing that contradicts the shared file. If the frontend needs an exception to a shared rule, the fix is to narrow the shared rule, not to override it in the scoped file — the model sees both, and GitHub’s advice is to avoid conflicting instructions rather than rely on any ordering.

Which clients read the scoped files matters before you write them. From GitHub’s support reference, for Copilot Chat: VS Code, Visual Studio, JetBrains IDEs, Xcode and Copilot CLI read path-specific files; Chat on GitHub.com and in Eclipse read only the repository-wide file. The cloud agent and code review read path-specific files everywhere they run. The diagnostic article’s support table has the full grid.

The files

Four files, complete. Bracketed values are placeholders; every other line is a rule you should delete if it is not true of your repository.

.github/copilot-instructions.md — shared rules

# Instructions for AI assistants

acme-platform is a monorepo: `frontend/` (TypeScript, React), `backend/`
(Python, FastAPI) and `infrastructure/` (Terraform). Each directory has its
own rules in `.github/instructions/`; this file is what holds everywhere.

## Layout and boundaries

- Do not import across the three top-level directories. The frontend talks
  to the backend over HTTP using the generated client in `frontend/src/api/`;
  the backend never reads Terraform state; Terraform never contains
  application configuration.
- A change that touches two directories is two pull requests unless the
  change is a contract change (an API schema, an output the app reads), in
  which case say so in the description.

## Definition of done

- A change is done when the checks for its directory pass — the commands are
  in each directory's instruction file — and you have run them, not assumed
  them. Report what you ran and what it printed.
- Every behaviour change adds or updates a test in the same change.

## Secrets and data

- Secrets come from the environment or a secret manager, never from a file in
  this repository and never from a prompt. Use `REPLACE_ME` in examples.
- Do not paste customer data, credentials or tokens into a chat.

## Working in this repository

- Follow the conventions in these files even where surrounding code does not;
  note the inconsistency in your response rather than copying it.
- If a rule here conflicts with what you were asked, say so and ask.

Under sixty lines, and every rule has an observable effect. Nothing about TypeScript, Python or HCL — those belong below.

.github/instructions/frontend.instructions.md

---
applyTo: "frontend/**/*.ts,frontend/**/*.tsx"
---
# Frontend rules

- `strict: true` is on. No `any`; use `unknown` and narrow. No `as` casts to
  silence the compiler — fix the type.
- Components are functions. State lives in hooks; side effects in
  `useEffect` with a complete dependency array. No class components.
- Data comes through the generated API client in `src/api/`. Do not call
  `fetch` from a component; do not hand-write a type the client already
  exports.
- Every user-visible string goes through `t()`; no literal UI text in JSX.
- Tests: `*.test.tsx` next to the component, testing behaviour through the
  rendered output, not implementation details. No snapshot tests for logic.

## Checks

    npm --prefix frontend run lint
    npm --prefix frontend run typecheck
    npm --prefix frontend test

.github/instructions/backend.instructions.md

---
applyTo: "backend/**/*.py"
---
# Backend rules

- Every request and response body is a Pydantic model. No `dict` in a route
  signature; no untyped JSON out.
- Money and quantities that must not drift are `Decimal`, never `float`.
- Errors that reach a client are `HTTPException` with the right status: 422
  for invalid input, 404 for a missing resource, 409 for a conflict. Never
  500 for a predictable failure.
- Routes are thin: validate, call one service function, shape the response.
  Database access goes through `app/repositories/`; a route never builds SQL.
- No mutable default arguments, no bare `except:`, no `print` in library code.

## Checks

    cd backend && ruff check . && mypy . && pytest -q

.github/instructions/infrastructure.instructions.md

---
applyTo: "infrastructure/**/*.tf,infrastructure/**/*.tfvars"
---
# Infrastructure rules

Terraform is applied by CI from `main`. State is remote and locked; nobody
runs `apply` locally against a real backend.

- Provider and module versions are pinned in `versions.tf`. Do not relax a
  constraint to make something work; say what needs a newer provider.
- Every variable has `type`, `description` and, where bounded, `validation`.
- No hardcoded region, account id or ARN; those are variables or data
  sources. Names use `local.name_prefix`.
- Secrets come from a secret-manager data source. Never a variable with a
  default, never a committed `.tfvars` with a secret in it.
- `count` for conditional creation, `for_each` for collections.
- Storage blocks public access explicitly; every ingress rule names a source;
  IAM names actions and resources — no `"*"`, not even in a comment.

## Checks

    cd infrastructure && terraform fmt -check -recursive && terraform init -backend=false && terraform validate && tflint

Reading the applyTo patterns

Three details in those globs carry the design.

** crosses directories; * does not. frontend/**/*.ts matches frontend/src/api/client.ts and frontend/src/a/b/c/d.ts. frontend/*.ts matches only files directly under frontend/. GitHub’s own example — ** matching foo.py, subdir/bar.py and deeper — is the reference for the semantics.

Comma-separated lists, no spaces around the commas. GitHub: “You can specify multiple patterns by separating them with commas.” The frontend file needs both .ts and .tsx; the infrastructure file needs .tf and .tfvars. One file with two patterns is clearer than two files with one rule set duplicated.

Patterns are relative to the repository root. backend/**/*.py matches backend/app/main.py; it does not match tools/backend-helper.py, which is the point. A pattern with no directory prefix — **/*.py — would apply the backend’s rules to every Python file in the repository, including the one-off scripts under infrastructure/scripts/, which is usually not what was meant.

Two patterns worth having that the examples above leave out, because they depend on your layout:

  • Tests as their own scope. backend/tests/**/*.py can carry test-only rules (fixtures build state, tests assert; no real network) that would be wrong in application code. Both the backend file and the tests file apply to a test file, which is fine as long as they do not disagree.
  • Generated code excluded. There is no negation in applyTo; the way to keep rules off frontend/src/api/generated/ is to say so in the rule (“do not edit files under src/api/generated/; regenerate them”) rather than to try to carve the glob.

Conflicts, and how to avoid them

The model sees every file that applies. A rule in the shared file and a contradicting rule in a scoped file are both in the prompt, and which one is followed on a given request is not something you can rely on. GitHub’s guidance is to avoid providing conflicting instructions; the CLI documentation says the same and adds that it does not define a precedence order between the files it combines.

The three shapes conflicts take in monorepos, and the repair for each:

ConflictExampleRepair
Shared rule with a stack exceptionShared: “no console.log”. Frontend: a logging wrapper that calls itNarrow the shared rule: “no console.log outside frontend/src/log.ts
Two scoped files matching one pathbackend/**/*.py and **/*.py both match backend/app/x.pyGive every scoped pattern a directory prefix; delete the catch-all
An AGENTS.md and an applyTo file saying different thingsRoot AGENTS.md says tabs; frontend.instructions.md says two spacesOne source of truth per rule; the other file links to it

The test for a conflict is mechanical: for one file path, list every instruction file whose pattern matches it, then read them side by side. The validation section below shows how to get that list from each client.

Validating that the right file applies

Writing the files is half the work; proving they apply is the other half, and it is different in each client.

VS Code. Open a file under frontend/, ask Chat anything, and expand the References section of the response: copilot-instructions.md and frontend.instructions.md should both be listed, and backend.instructions.md should not. VS Code’s documentation adds that a .instructions.md file without applyTo “is not applied automatically”, and that chat.includeApplyingInstructions must be enabled for pattern-based files. Repeat in a file under backend/ and infrastructure/.

JetBrains. The Chat panel’s settings icon opens Customizations, which lists the workspace instructions the IDE sees; the references on a response show which applied to that request.

Copilot CLI. Run /instructions in a session started at the repository root: it lists every instruction file discovered and lets you disable each. Path-specific files “are included only when their applyTo value matches a file that Copilot CLI is working with”, so the list shows what is available; which scoped file was included for a given task depends on the files that task touched. Two rules from the CLI documentation matter here: edits to instruction files “are not immediately available for use in active CLI sessions” (exit and copilot --continue), and the CLI discovers instruction files in “the repository root, the current working directory, intermediate directories between them, and any directories nested in the path of a file it is working on” — so starting a session inside backend/ and at the root can discover different files.

Then test one rule per file. Ask, in a frontend file, for a component with a literal string in the JSX; the rule says it goes through t(). Ask, in a backend route, for a money field; the rule says Decimal. Ask, in Terraform, for a variable; the rule says it needs a validation block. A client that lists the file and produces the rule-following output has proven the scope. A client that lists the file and does not is a discovered-but-not-followed problem, and the diagnostic article takes it from there.

Illustrative, not a transcript — the shape of a passing check in VS Code:

References
  .github/copilot-instructions.md
  .github/instructions/backend.instructions.md

Debugging a pattern that does not match

The symptom is “the rules apply in some files and not others”, and the causes are nearly always in the glob.

  1. Print the full relative path of the file you are editing, from the repository root, and test the pattern against it by hand. backend/app/api/orders.py against backend/**/*.py matches; services/backend/app/main.py does not, because the pattern is anchored at backend/.
  2. Check the frontmatter is first. A blank line or a comment before the --- makes it body text, not frontmatter, and the file is then a Markdown file with no scope.
  3. Check the suffix and directory. .github/instructions/backend.md is not an instruction file; backend.instructions.md outside .github/instructions/ is not either.
  4. Check the client reads path-specific files at all. Chat on GitHub.com and in Eclipse does not; the scoped files are simply absent there, and the shared file is all that applies.
  5. Look for a second file matching the same path. A leftover **/*.py from before the split will still be applying its rules alongside the new one.

The site’s Setup Kit generates a repository-wide file and two scoped files for one stack with tested applyTo patterns; for a monorepo, generate once per stack and merge the shared files by hand, keeping one repository-wide file.

Maintenance

Instruction files rot in a specific way: a convention changes, the code changes with it, and the file keeps stating the old convention, which the model then follows. Three habits prevent it.

  • Change the file in the same pull request as the convention. If the backend moves from Decimal to a money type, the backend instruction file changes in that pull request, not later.
  • Own each file where its code is owned. Put the three scoped files under the same CODEOWNERS entries as their directories, so the frontend team reviews frontend.instructions.md.
  • Re-run the one-rule tests when something feels off. A five-minute check in each stack, quarterly or after a client update, catches both rot and a client that changed what it reads. GitHub’s clients changed that this year; the changes log tracks it.

For teams, the team standards lesson covers taking one repository’s rules to many; the shape above is the one-repository version of that.

Frequently asked questions

Should the scoped files repeat the shared rules? No. Both apply; repeating a rule doubles its context cost and creates two places for it to drift. A scoped file may reference a shared rule (“the definition of done is in the shared file”) if a reader would otherwise miss it.

Can one file scope to two directories? Yes — a comma-separated applyTo. Whether it should depends on whether the rules are really the same; two stacks that share a file will eventually get a rule that is true for one of them.

Do I still need AGENTS.md? Only for contexts that read it and not path-specific files — GitHub’s reference lists the cloud agent and code review as reading both, and Copilot Chat on GitHub.com as reading neither. If you keep one, keep it consistent with the scoped files; do not make it a third place the same rule lives.

What about .github/instructions/ subdirectories? Allowed — GitHub says the files can be in that directory “or subdirectories”. Grouping by stack (instructions/frontend/) is fine; the applyTo in each file is still what scopes it, not the directory it sits in.

Is there a limit on how many scoped files apply at once? GitHub does not document one. The practical limit is context: every applying file is in the prompt, so keep each short and keep the patterns non-overlapping.

Recap and next step

One shared file for what is true everywhere; one scoped file per stack, with an applyTo anchored at that stack’s directory and covering all of its file types; no rule in two places; conflicts repaired by narrowing the shared rule, never by overriding it. Prove each scope with the references list (or /instructions) and one rule that has a right and a wrong answer.

The custom instructions lesson is the reference for the mechanism, and the TypeScript, Python and Terraform lessons are where the rules in each scoped file come from. Generate a first version for any one stack with the Project Setup Kit; if you want the long-form files — sixty lines per stack, with the reasoning kept and revised when GitHub changes what a client reads — the Pro library publishes its Terraform file in full so you can judge before buying.

Sources

Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.

Primary sources

Go deeper in the Academy

  • GitHub Copilot Custom Instructions ExplainedSix instruction mechanisms, which of the nine Copilot surfaces support each, and why 'custom instructions work everywhere' is wrong in both directions.
  • How to Create a copilot-instructions.md FileA step-by-step build of the one instruction file every Copilot surface reads, and the difference between a durable rule and today's task.
  • GitHub Copilot for TypeScriptWhat types change: better suggestions in, a compiler that rejects wrong output, and the assertions and `any` casts Copilot reaches for when it cannot satisfy them.
  • GitHub Copilot for PythonType hints as context, virtual environments, pytest generation, Ruff and mypy as the review layer — and the mutable-default and unsafe-subprocess patterns to watch for.
  • GitHub Copilot for TerraformVariables, for_each, modules and state, with fmt, validate and a policy scan as the gate — and why a security fix can add three new findings.