Everything below comes from one session on 2026-10-02: Node 22.23.3, React 19.3.0, TypeScript 5.9.3, ESLint 9.39.5 with typescript-eslint 8.71.0, in a throwaway project outside this repo. I didn't run Cursor or Copilot. What they do with these files comes from their docs, which I fetched the same day. What I did run is the file formats and the glob matching, and I'll mark which is which.
TL;DR: Cursor reads
.cursor/rules/*.mdc, Copilot reads.github/copilot-instructions.mdplus.github/instructions/*.instructions.md, and all three tools below document readingAGENTS.md. The scoping keys differ (globs,applyTo,paths). In my check, one rule glob also matched test files, and an unquoted glob was invalid YAML.
What Does Each Tool Read?
I'll keep the table to what the docs state, nothing more.
| Always on | Scoped to files | Scope key | |
|---|---|---|---|
| Cursor | .cursor/rules/*.mdc with alwaysApply: true | same folder, alwaysApply: false | globs |
| Copilot | .github/copilot-instructions.md | .github/instructions/NAME.instructions.md | applyTo |
| Claude Code | CLAUDE.md | .claude/rules/*.md | paths (YAML list) |
| Shared | AGENTS.md, nested allowed, nearest wins | none | none |
Two details trip people up. Cursor's rules docs say project rules must use the .mdc extension, and a plain .md file in .cursor/rules is ignored. And Claude Code reads AGENTS.md itself (v2.1.277 or later), but by default only when no CLAUDE.md exists above your working directory. A CLAUDE.md containing the single line @AGENTS.md is the documented bridge.
One List of Conventions
I picked five that fit React and TypeScript projects and can be checked by a tool: no any, function components only, named exports, hooks at the top level, and test names that describe behavior. This is the whole AGENTS.md for the test project, 21 lines.
# Project instructions
React 19 + TypeScript 5 (strict). Check your work with `npx tsc --noEmit` and `npx eslint src`.
## TypeScript
- Never write `any`. Use `unknown` and narrow it.
- Annotate the return type of every exported function that is not a component.
## React components (src/components)
- Function components only. No classes.
- Named exports only. No default exports.
- Declare props as `type <Name>Props` directly above the component.
The hooks and tests sections follow the same shape. Short on purpose. Cursor's own best-practice list says to keep rules under 500 lines, and its "what to avoid" list says not to copy entire style guides because a linter does that job.
How Do You Write the Cursor Version?
Split by scope. The always-on file carries the two TypeScript rules, and each folder-specific file gets globs:
---
description: React component conventions
globs: src/components/**/*.tsx
alwaysApply: false
---
- Function components only. No classes.
- Named exports only. No default exports.
- Declare props as `type <Name>Props` directly above the component.
The docs give four combinations of the three frontmatter fields. alwaysApply: true includes the rule every time. false with globs attaches it when a matching file is in context. false with only a description lets the agent decide. With none of the three, you have to @-mention it. For several patterns, the docs say to separate them with commas.
And the Copilot Version?
Same content, different wrapper. The repository-wide file has no frontmatter. The scoped one uses applyTo:
---
applyTo: "src/components/**/*.tsx"
---
- Function components only. No classes.
- Named exports only. No default exports.
- Declare props as `type <Name>Props` directly above the component.
GitHub's docs add an excludeAgent key that takes "code-review" or "cloud-agent", which has no Cursor equivalent. If you leave it out, both Copilot code review and the cloud agent read the file. One more thing worth knowing: the "no longer than 2 pages" limit people quote sits inside the docs' sample prompt for generating a copilot-instructions.md, so I'd treat it as a guideline for that prompt, not a hard cap.
Our older posts cover the monorepo side of this in the Copilot Workspace guide and the Cursor tips in 20 Cursor tips for TypeScript. The Claude Code equivalent is in the Claude Code React workflow.
What Could I Actually Verify Locally?
Three things, and none of them involve an AI tool. First, that every frontmatter block parses as YAML. Second, that each glob selects the files I meant. Third, what the compiler and linter say about the files the rules are meant to prevent.
The test project has six source files: a good component, a class component with any and a default export, a good hook, a broken hook, a utility, and a test. This is the glob check, using yaml 2.9.1 and picomatch 4.0.7:
.cursor/rules/react-components.mdc
globs: "src/components/**/*.tsx"
matches 3/6 src files: LegacyBanner.tsx, UserCard.test.tsx, UserCard.tsx
.cursor/rules/testing.mdc
globs: "**/*.test.ts,**/*.test.tsx"
matches 1/6 src files: src/components/UserCard.test.tsx
Look at the first block. The component rule also matched UserCard.test.tsx, because a test for a component ends in .tsx and lives in the same folder. So a test file gets "named exports only" and "declare props as a type", which is noise. The glob tables in both docs list no exclusion syntax, so I can't tell you a negation would work. The workable options are writing component rules that stay true in a test file, or splitting tests into their own folder.
Caveat: picomatch isn't the matcher either tool uses. Treat the output as a check on your intent, not a promise about their behavior.
The second finding is a YAML trap. I wrote the testing rule once with an unquoted glob, the way it reads naturally:
globs: **/*.test.ts,**/*.test.tsx
YAML ERROR: Unexpected scalar at node end at line 1, column 20
A leading * starts a YAML alias, so an unquoted applyTo made of two asterisks fails with "Unresolved alias" too. Cursor's doc examples are unquoted, but they begin with a letter (src/components/**/*.tsx), which parses fine. GitHub's examples all quote. I didn't test whether Cursor's own parser is lenient, so the rule is simply: quote every glob.
Now the part that matters most. With strict: true and noUncheckedIndexedAccess, tsc --noEmit exited 0 on the project, class component and any included. ESLint caught three problems:
LegacyBanner.tsx 4:61 error Unexpected any @typescript-eslint/no-explicit-any
useBroken.ts 5:5 error React Hook "useState" is called conditionally
useBroken.ts 9:6 error React Hook useEffect has a missing dependency: 'id'
A rules file is a request the model can ignore. A lint rule is a gate it can't. Write the convention in both places, but only count the second one.
That's an opinion, and a fair one to argue with. The case against it: a lint rule can't say "prefer composition over prop drilling". Fine. But the rules in this article that a linter can express, it should.
One File or Three?
For unscoped rules, one AGENTS.md plus a one-line CLAUDE.md covers Cursor, Copilot and Claude Code on paper. My test project's eight tool-specific files add up to 60 lines, while AGENTS.md holds the same content in 21. Each extra copy is a place the rules can drift apart.
Scoping is the catch. AGENTS.md has no frontmatter, so the documented route is nesting: put one in src/components/ and another in src/hooks/. Cursor says nested files apply when you work in that directory and below. Copilot says the nearest one takes precedence. I didn't run any of these tools against nested files, so that's the docs talking.
My pick for a small React app is AGENTS.md first, tool-specific files only where globs earn their keep. Is a hooks-only rule worth a separate .mdc and a separate .instructions.md? Usually not, until the file passes about 40 lines.
What I'd Check Before Trusting Any of This
Rules files age. Here's the short list I'd run on a real repo:
- Parse every
.mdcand.instructions.mdfrontmatter in CI. My script was 44 lines. - Print which files each glob matches, and read the list once by eye.
- Put anything a linter can enforce into the linter. Our guide to CI quality gates for AI code covers wiring that into a pipeline.
- Keep
stricton, as covered in the TypeScript strict mode guide.
What I couldn't verify is the part you most want: whether Cursor or Copilot obeys a given rule. No glob check shows that. Run the tool on a task that should trip each rule, and keep the ones that change its output.