Skip to content

Rules Files for React and TypeScript: Cursor, Copilot, AGENTS.md

Cursor, Copilot and AGENTS.md rules files for one set of React and TypeScript conventions, with file formats and glob matches checked locally.

· · 7 min read
Two orange lines and white edge lines painted on dark asphalt

Quick Take

Three tools, three file formats, one list of conventions. Here's how each format works per its current docs, and which parts I could check on my own machine.

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.md plus .github/instructions/*.instructions.md, and all three tools below document reading AGENTS.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 onScoped to filesScope key
Cursor.cursor/rules/*.mdc with alwaysApply: truesame folder, alwaysApply: falseglobs
Copilot.github/copilot-instructions.md.github/instructions/NAME.instructions.mdapplyTo
Claude CodeCLAUDE.md.claude/rules/*.mdpaths (YAML list)
SharedAGENTS.md, nested allowed, nearest winsnonenone

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.

Share this Post on X Bluesky

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.

Curved tunnel road with white direction arrows and double lane lines under warm light
Photo by Richard Heinen on Unsplash

What I'd Check Before Trusting Any of This

Rules files age. Here's the short list I'd run on a real repo:

  1. Parse every .mdc and .instructions.md frontmatter in CI. My script was 44 lines.
  2. Print which files each glob matches, and read the list once by eye.
  3. Put anything a linter can enforce into the linter. Our guide to CI quality gates for AI code covers wiring that into a pipeline.
  4. Keep strict on, as covered in the TypeScript strict mode guide.
Empty city street with painted lane arrows and a gantry over the road
Photo by viktor rejent on Unsplash

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.

Frequently Asked Questions

Is .cursorrules still the way to configure Cursor?
The current Cursor rules page doesn't mention .cursorrules at all. It describes project rules as .mdc files in .cursor/rules and AGENTS.md as the plain-Markdown alternative. I found no statement about the old file's status, so for a new project use .cursor/rules.
What is the Copilot equivalent of a Cursor rule with globs?
A file named NAME.instructions.md under .github/instructions with an applyTo line in its frontmatter. GitHub's docs say multiple patterns are separated by commas, and that when a path-specific file and the repository-wide file both match, both are used.
Can one AGENTS.md replace all of them?
For unscoped rules, yes: Cursor, Copilot and Claude Code each document reading it. It has no frontmatter, so it can't say 'only for hooks files'. Nested AGENTS.md files in subdirectories are the documented way to scope it by folder.
Why did my glob file fail to parse?
In my check, an unquoted value starting with an asterisk is invalid YAML. The yaml 2.9.1 parser read it as an alias and threw an error. Quote every glob.