# Snippet management in Cursor

URL: https://developertoolkit.ai/en/cursor-ide/build/snippet-management/
Description: Split Cursor boilerplate between VS Code snippets, scoped rules and agent prompts, and gate it in CI so the scaffolds compile and never drift from your code.

Snippet management in Cursor splits boilerplate across three layers: VS Code snippet files insert the fixed shell instantly and identically, a scoped project rule carries the conventions the agent must follow, and an anchored agent prompt fills in the codebase-dependent parts. Cursor Tab, which finishes the current line, is not a managed layer. A CI check keeps all three compiling and in step.

You scaffold your fourth data-fetching component this week. The snippet you copied from a colleague still imports the old fetch hook, the agent invents a third way to render the error state, and the reviewer spends the whole review on boilerplate instead of behaviour. Nobody changed the convention on purpose; the snippet, the rule and the code drifted apart one edit at a time.

This page is for **developers** who write the same shapes every day in Cursor, and for **tech leads** who own the team's shared snippets and rules. It assumes you already use project rules; if not, start with [project rules in Cursor](/en/cursor-ide/quick-start/project-rules/).

## What you'll set up

- A decision table for which layer should produce a given piece of boilerplate.
- A committed `.vscode/*.code-snippets` file with conventions that keep prefixes findable.
- A scoped rule and four copy-paste prompts that turn a snippet shell into finished, convention-following code.
- Two CI checks: one proves every snippet expands to code that type-checks, the other catches rules that point at deleted files.
- The failure modes of a snippet library and how to recover from each.

## Which layer should produce this boilerplate?

Each layer trades speed and determinism against knowledge of your code. Put each piece of boilerplate in the cheapest layer that can produce it correctly.

| Layer | Where it lives | Speed | Same output every time? | Knows your code? | Use it for |
| --- | --- | --- | --- | --- | --- |
| VS Code snippet | `.vscode/*.code-snippets` | Instant, no model call | Yes | No, only the file name and your tab stops | The typed shell: props interface, export, test skeleton, the four render states |
| Project rule | `.cursor/rules/*.mdc` | Applies to every matching Agent request | No | Through the files it names | Conventions the agent must follow whenever it touches those files |
| Anchored prompt or Agent Skill | The chat, a saved command, or a skill | One agent run | No | Yes, it reads the anchor file | Multi-file scaffolds and bodies that depend on real types and hooks |
| Cursor Tab (not a layer you manage) | Built in | As you type | No | Recent edits and nearby code | Finishing the line you are already writing; see [Tab autocomplete](/en/cursor-ide/build/tab-autocomplete/) |

Two rules of thumb follow from the table. If the output should never vary, it is a snippet, not a prompt. If the output must match a file that changes over time, it is a rule or a prompt that names that file, not a snippet that copies it.

## Build the snippet layer the team shares

Cursor is built on VS Code, so it reads the same snippet files. The syntax below is from VS Code's snippet documentation (`microsoft/vscode-docs`, checked on 2026-09-26). After you create the file, confirm Cursor picks it up with the **Insert Snippet** command from the Command Palette.

1. Run **Snippets: Configure Snippets** from the Command Palette and choose **New Snippets file for '&lt;folder&gt;'**. VS Code creates a `.code-snippets` file in the project's `.vscode` folder, which you commit so every teammate gets the same snippets.

2. Add snippets with a `scope`, a `description`, and a prefix in the form `category-name`. Without `scope`, a project snippet is offered in every language. The category prefix lets you type `comp-` to filter the list to components. The optional `include` and `exclude` properties take glob patterns and narrow a snippet to matching files, so `comp-query` below appears only under `src/components`, the same files the rule's `globs` cover. Path patterns match the full file path, hence the leading `**/`. The properties are recent in VS Code, so confirm Cursor honours them: the snippet should not appear in a `.tsx` file outside `src/components`.

   ```json
   {
     "Component with query states": {
       "scope": "typescriptreact",
       "prefix": "comp-query",
       "include": ["**/src/components/**/*.tsx"],
       "description": "Component that renders loading, error, empty and data states",
       "body": [
         "import { EmptyState, ErrorState, Skeleton } from '@/components/ui/states';",
         "import { ${2:useNotifications} } from '@/hooks/queries';",
         "",
         "interface ${TM_FILENAME_BASE}Props {",
         "  ${1:userId}: string;",
         "}",
         "",
         "export function ${TM_FILENAME_BASE}({ ${1:userId} }: ${TM_FILENAME_BASE}Props) {",
         "  const query = ${2:useNotifications}(${1:userId});",
         "",
         "  if (query.isPending) return <Skeleton />;",
         "  if (query.isError) return <ErrorState onRetry={query.refetch} />;",
         "  if (query.data.length === 0) return <EmptyState>${3:Nothing here yet}</EmptyState>;",
         "  return ${4:null};$0",
         "}"
       ]
     },
     "Vitest file": {
       "scope": "typescript,typescriptreact",
       "prefix": "test-file",
       "description": "Vitest file for the module next to it",
       "isFileTemplate": true,
       "body": [
         "import { describe, expect, it } from 'vitest';",
         "",
         "describe('${1:${TM_FILENAME_BASE}}', () => {",
         "  it('${2|returns,renders,throws|} ${3:the expected result}', () => {",
         "    ${4:expect(true).toBe(false);}$0",
         "  });",
         "});"
       ]
     }
   }
   ```

   `${TM_FILENAME_BASE}` inserts the file name without its extension, so naming the file `InvoiceList.tsx` names the component. `${2|returns,renders,throws|}` offers a choice. `$0` is where the cursor ends. A snippet with `isFileTemplate` appears under **Snippets: Fill File with Snippet**, which fills a new, empty file. The test snippet's default assertion fails on purpose, so a skeleton nobody filled in turns the suite red instead of passing. Give every tab stop a default that exists in your codebase, such as a real hook name: the CI check below expands each snippet with its defaults and type-checks the result.

3. Keep the file small. Snippets earn their place for shapes you type daily; everything else belongs to a rule or a prompt. Delete a snippet when nothing written in the last quarter matches its shape.

Bootstrapping the file by hand from memory produces the generic templates you were trying to get away from. Let the agent read your code instead, then review the result the same way you would review any config change. Add the two files from the [verification section](#how-do-you-verify-snippet-boilerplate-without-reading-every-line) below first (`scripts/check-snippets.mjs` and `tsconfig.snippets.json`), or drop the last paragraph of the prompt.

**Copy-paste prompt: generate snippets from the code you already have**

```text
Read the components in @src/components and the tests in @src/components/__tests__.
Find the five shapes that repeat most often (props interface, export style, data-fetching
hook, render states, test layout) and write them as VS Code snippets to
.vscode/project.code-snippets.

Rules:
- Strict JSON, no comments. Every snippet has "scope", "description" and a prefix in the
  form category-name (comp-, hook-, test-, api-).
- Use ${TM_FILENAME_BASE} for the component or module name, numbered tab stops for the
  parts that vary, and $0 for the final cursor position.
- Copy only what is identical across the examples. Anything that differs between files
  becomes a tab stop, not a guess.
- Imports use the path aliases from tsconfig.json, never relative paths.

Then run `node scripts/check-snippets.mjs` and `npx tsc --noEmit -p tsconfig.snippets.json`
and report the output of both. For each snippet, name the two source files it was derived from.
```

The last line matters: naming the source files lets you check the derivation against two real files instead of reading the whole JSON.

## Keep the conventions in a scoped rule, not in the snippet

A snippet cannot know that your team moved from one fetch hook to another. A rule scoped to the same files can, because it names the canonical example the agent reads every time. Create `.cursor/rules/components.mdc`. The frontmatter keys below (`description`, `globs`, `alwaysApply`) are the format the [project rules page](/en/cursor-ide/quick-start/project-rules/) documents, and Cursor's rules documentation (checked 2026-10-12) lists the same three keys. Cursor's help says rules apply only to Agent (Chat), not to Tab completion, Inline Edit or Bugbot PR reviews (checked 2026-10-12), so the finishing prompt further down attaches the rule file by name.

```markdown
---
description: Conventions for React components in src/components
globs: src/components/**/*.tsx
alwaysApply: false
---

- New components start from the comp-query snippet in .vscode/project.code-snippets.
- Data comes from the hooks in src/hooks/queries.ts. Never call fetch in a component.
- Render loading, error and empty states with Skeleton, ErrorState and EmptyState from
  src/components/ui/states.tsx.
- The reference implementation is src/components/NotificationList.tsx. Match it.
- Every component gets a test file created from the test-file snippet.
```

The rule and the snippet now point at each other. When the convention changes, you change the snippet, the rule and the reference file in one pull request, and the anchor check below fails if a file the rule names has moved. Rule types and the file format are covered in [project rules in Cursor](/en/cursor-ide/quick-start/project-rules/); a team-wide rules library is in [custom rules and templates](/en/cursor-ide/automate/custom-rules-templates/).

## Fill the snippet shell with the agent

The snippet gives you a file that already has the right skeleton. The agent's job is the part that depends on your code: the real hook, the real types and the real markup.

1. Create `src/components/InvoiceList.tsx`, type `comp-query` and accept the snippet. Tab through the stops: the prop name, the hook name (replace the default `useNotifications`, which belongs to the reference component, with `useInvoices`), the empty-state text and the final return value.

2. With the cursor at `$0`, open inline edit (the shortcut is listed in [Cursor keyboard shortcuts](/en/cursor-ide/reference/keyboard-shortcuts/); the workflow is in [inline edit](/en/cursor-ide/build/inline-edit/)), or select the file and send it to the agent. Paste the prompt below.

3. Review the result against the gates, not line by line: the type check, the lint and the component test.

**Copy-paste prompt: finish a component from the comp-query shell**

```text
This file was created from the comp-query snippet. Finish it following
@src/components/NotificationList.tsx and @.cursor/rules/components.mdc.

- Use useInvoices from src/hooks/queries.ts; add the hook there if it is missing,
  following the existing hooks in that file.
- Keep the imports and the four render states exactly as the snippet laid them out;
  replace only the final return with the data view.
- Render each invoice as a list item with its customer and formatted amount.
- Create InvoiceList.test.tsx from the test-file snippet with one test per render
  state (loading, error with retry, empty, data).
- Do NOT edit other components or existing tests.

Run `npx vitest run src/components/InvoiceList.test.tsx`, `npx tsc --noEmit` and
`npm run lint`, and paste the last lines of each output.
```

For a scaffold that spans several files, such as a route, its schema, its service and its tests, a snippet per file is the wrong tool. Give the agent one anchored prompt and save it where the team can reuse it, as described in [prompt templates for Cursor's agent](/en/cursor-ide/build/prompt-templates/).

**Copy-paste prompt: multi-file scaffold for a new API route**

```text
Scaffold a POST /api/invoices route following the full pattern of the users route:
@src/routes/users.ts, @src/schemas/user.ts, @src/services/user-service.ts and
@src/routes/__tests__/users.test.ts.

Create four matching files for invoices. An invoice has customerId (string), amountCents
(positive integer) and dueDate (ISO date). Reuse the middleware, error type and response
shape exactly as the users route does; do not introduce new helpers.

Write the tests first: 201 on a valid body, 400 on each invalid field, 401 without a
session. Run them to see them fail, then implement until they pass.
Finish by running `npx vitest run src/routes`, `npx tsc --noEmit` and `npm run lint`,
and report the files created and the output of each command.
```

If the same scaffold prompt runs every week, package it as an [Agent Skill](/en/ecosystem/skills/building-custom-skills/), which Cursor describes as "a portable, version-controlled package that teaches agents how to perform domain-specific tasks" (cursor.com/docs/skills, checked on 2026-08-28). The skill then travels with the repository like the snippet file does.

## How do you verify snippet boilerplate without reading every line?

Boilerplate is where nobody looks, which is why broken boilerplate spreads. Put two checks in CI so a bad snippet or a stale rule fails the build instead of reaching the next ten files.

**Check 1: every snippet parses, has a unique prefix and expands to code that compiles.** Save this as `scripts/check-snippets.mjs`. It replaces each tab stop with its default, writes the result to `.snippet-check/`, and fails on invalid JSON, a missing `scope` or `description`, a prefix outside the `category-name` form, or a prefix used twice.

```js
// scripts/check-snippets.mjs: lint and expand project snippets. Run: node scripts/check-snippets.mjs
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

const DIR = '.vscode';
const OUT = '.snippet-check';
const EXT = { typescript: 'ts', typescriptreact: 'tsx' };
const errors = [];
const seen = new Map();

// Replace every tab stop with its default so the result is plain source code.
function expand(text) {
  let prev;
  do {
    prev = text;
    text = text
      .replace(/\$\{\d+:([^${}]*)\}/g, '$1') // ${1:Default} -> Default
      .replace(/\$\{\d+\|([^,|]*)[^}]*\|\}/g, '$1') // ${1|a,b|} -> a
      .replace(/\$\{[A-Z_]+\}/g, 'Example'); // ${TM_FILENAME_BASE} -> Example
  } while (text !== prev);
  return text.replace(/\$\d+/g, '').replace(/\$[A-Z_]+/g, 'Example').replace(/\\\$/g, '$');
}

rmSync(OUT, { recursive: true, force: true });
mkdirSync(OUT);

for (const file of readdirSync(DIR).filter((f) => f.endsWith('.code-snippets'))) {
  let snippets;
  try {
    snippets = JSON.parse(readFileSync(join(DIR, file), 'utf8'));
  } catch (e) {
    errors.push(`${file}: not strict JSON (${e.message})`);
    continue;
  }
  for (const [name, s] of Object.entries(snippets)) {
    const where = `${file} > ${name}`;
    if (!s.prefix || !s.body) {
      errors.push(`${where}: needs prefix and body`);
      continue;
    }
    if (!s.scope) errors.push(`${where}: no scope, so it is offered in every language`);
    if (!s.description) errors.push(`${where}: no description`);
    for (const p of [s.prefix].flat()) {
      if (!/^[a-z]+-[a-z0-9-]+$/.test(p)) errors.push(`${where}: prefix "${p}" is not category-name`);
      if (seen.has(p)) errors.push(`${where}: prefix "${p}" is also used by ${seen.get(p)}`);
      seen.set(p, where);
    }
    const ext = EXT[String(s.scope ?? '').split(',')[0].trim()];
    if (ext) writeFileSync(join(OUT, `${name.replace(/\W+/g, '_')}.${ext}`), expand([s.body].flat().join('\n')));
  }
}

if (errors.length) {
  console.error(errors.join('\n'));
  process.exit(1);
}
console.log(`${seen.size} snippet prefixes OK, expanded bodies in ${OUT}/`);
```

VS Code accepts comments in snippet files; this check deliberately does not, so the file stays machine-readable. It handles plain variables such as `${TM_FILENAME_BASE}` but not variable transforms; keep transforms out of shared snippets or extend `expand`. Type-check the expanded bodies with a `tsconfig.snippets.json` next to your `tsconfig.json`, and add `.snippet-check/` to `.gitignore`:

```json
{ "extends": "./tsconfig.json", "include": [".snippet-check/*.ts", ".snippet-check/*.tsx"] }
```

Keep the explicit `*.ts` and `*.tsx` globs: with a bare `".snippet-check"` entry, TypeScript skips the folder because its name starts with a dot and reports "No inputs were found" (checked with TypeScript 5.9.3).

When the type check fails on an expanded snippet, the snippet has drifted from the code: a hook was renamed, a component moved, or a default names something that never existed. Fix the snippet, not the check.

**Check 2: every file a rule names still exists.** Save this as `scripts/check-rule-anchors.sh`. It fails when a rule points at a renamed or deleted file, which is the most common way a rule silently stops working.

```sh
#!/usr/bin/env sh
# Fails when a Cursor rule names a file that no longer exists.
missing=0
for p in $(grep -ohE '(src|app|lib|tests)/[A-Za-z0-9_./-]*\.[a-z]+' .cursor/rules/*.mdc | sort -u); do
  [ -e "$p" ] || { echo "stale anchor in .cursor/rules: $p"; missing=1; }
done
exit $missing
```

Run both in the CI job that already runs your type check:

```yaml
- run: node scripts/check-snippets.mjs
- run: npx tsc --noEmit -p tsconfig.snippets.json
- run: sh scripts/check-rule-anchors.sh
```

Code the agent generates from a snippet shell passes through the same gates as any other change: the tests the prompt asked for, the type check and the lint. What the checks cannot tell you is whether the convention itself is right. That is a human decision, so give it an owner: a `CODEOWNERS` entry such as `/.vscode/*.code-snippets @your-org/frontend-leads` and `/.cursor/rules/ @your-org/frontend-leads` makes the tech lead approve every change to the shared layer. Once a month, or after a refactor, run the drift audit below.

**Copy-paste prompt: audit snippets and rules for drift**

```text
Compare three things and report where they disagree, without editing anything:
1. Every snippet in .vscode/*.code-snippets.
2. Every rule in .cursor/rules/*.mdc that mentions a snippet prefix or a reference file.
3. The five most recently modified files in src/components and src/routes.

Produce a table with: snippet or rule, what it says, what the recent code does instead,
and the file and line that shows it. Flag snippets whose prefix appears in no rule, and
rules whose reference file no longer matches the snippet's shape.
End with a proposed change list, smallest first. Do not change any file.
```

## What breaks in a Cursor snippet library, and how to recover

**A snippet never appears in the suggestion list.** The file is not in the root `.vscode` folder, the `scope` names the wrong language identifier (`typescriptreact` for `.tsx`, not `tsx`), or the JSON is invalid. Run `node scripts/check-snippets.mjs`, then run **Insert Snippet** in a file of the target language to see what Cursor actually loaded.

**Cursor Tab and the snippet fight for the Tab key.** Tab accepts Cursor Tab's inline suggestion, and it can also accept the highlighted item in the suggestion list. Which one wins when both are showing depends on your build and settings; we could not verify it for the Cursor 3.x line on 2026-09-26. Use `category-name` prefixes that do not look like code, so Cursor Tab rarely predicts on them, and insert from the suggestion list or **Insert Snippet** when in doubt. Leave VS Code's `editor.tabCompletion` at its default (off) unless the team agrees to turn it on.

**The agent ignores the snippet's shape.** The rule does not load for that file, or it names the snippet but not a reference file. Check the rule's `globs` against the file's path, and add a concrete reference file to the rule. A rule that says "follow the pattern" without naming the file gives the agent nothing to copy.

**Snippet, rule and code disagree.** Someone updated one of the three. Run the drift audit prompt, decide which one reflects the current convention, and change the other two in one pull request so the reviewer sees the whole change.

**A rule points at a file that moved.** The anchor check fails in CI. Point the rule at the file's new path, or at a better example if the old one was split. If the check is not in CI yet, run `sh scripts/check-rule-anchors.sh` locally before you merge a refactor.

**The library has grown to a hundred snippets and nobody finds anything.** Search the repository for each prefix's generated shape; if nothing written in the last quarter matches its shape, delete the snippet. Keep only shapes the team types weekly, and move the rest into rules or prompts.

**Generated boilerplate compiles but misses a state.** A type check proves the shape, not the behaviour. Require one test per render state in the prompt, as the component prompt above does, and see [protecting the test oracle](/en/workflows/test/protect-the-oracle/) for keeping the agent from editing those tests to make them pass.

## Where to go next with Cursor snippets and scaffolds

- [Prompt templates for Cursor's agent](/en/cursor-ide/build/prompt-templates/): where to store the scaffold prompts from this page as commands, skills or rules.
- [Context management in Cursor](/en/cursor-ide/quick-start/context-management/): how `@` references pull anchor files into the agent's context.
- [Custom rules and templates](/en/cursor-ide/automate/custom-rules-templates/): a team-wide rules library with ownership and review.
- [Cursor time savers](/en/cursor-ide/build/time-savers/): more habits that remove repeated keystrokes.
- [Custom commands in Claude Code](/en/claude-code/automate/custom-commands/): the equivalent for teams that run the same scaffolds in Claude Code.
