Cursor’s rules system quietly became one of the most-configured parts of the editor in 2026. Every serious team running Cursor now keeps a stack of .mdc files that tell the Agent how to write code, which libraries to avoid, and which folders need extra scrutiny. The problem is that nobody ships good tooling for managing them. You write frontmatter by hand, you guess at glob patterns, and you find out a rule is broken only after the Agent ignores it mid-task.
This tutorial walks through building an actual VS Code extension that fixes that gap: a Cursor Rules manager that creates properly-formatted .mdc files, validates their frontmatter on save, and gives you a webview panel to browse every rule in a workspace at a glance. It runs inside both VS Code and Cursor, since Cursor is built on the same extension host and installs standard .vsix packages. By the end you will have a working extension, a packaged .vsix file, a pre-commit hook that keeps broken rules out of your main branch, and a clear picture of how Cursor’s rule system evolved through September 2026.
None of this requires touching Cursor’s proprietary Agent API or any AI model at all. Everything below is plain TypeScript against the public VS Code Extension API, which is exactly why the same .vsix installs cleanly in both editors. If you have built a VS Code extension before, expect roughly 90 minutes end to end; if this is your first one, budget closer to two and a half hours, most of it spent getting comfortable with the Extension Development Host workflow rather than writing code.
Why Cursor Rules Need Dedicated Tooling in 2026
Cursor rules stopped being a novelty a long time ago. They are now the primary mechanism teams use to keep an AI agent aligned with a codebase’s real conventions, and Cursor has kept expanding what a rule can do. On September 23, 2026, Cursor shipped Rollouts and Security Review, a pair of bots that watch deployments and scan every pull request for exploitable bugs. Security Review specifically applies team rules to pull requests on Teams and Enterprise plans, covering injection flaws, auth bypasses, committed secrets, SSRF, unsafe deserialization, and vulnerable dependency bumps. That single change turned rules from a style guide into an enforcement layer, and it raised the cost of a sloppy or malformed rule file considerably.
Two weeks earlier, on September 10, 2026, Cursor launched Projects in beta, a feature built for larger bodies of work such as a full migration or a new app, which maintains context over months and delegates tasks to subagents. Projects lean even harder on well-scoped rules, because a subagent working autonomously for days needs the same guardrails a human engineer would get from a senior teammate. Add the September 2, 2026 rollout of self-hosted machines and the August 27, 2026 option to start a Cursor session without a repository at all, and you get an editor where rules increasingly travel with the workflow rather than sitting untouched in a dotfile.
Meanwhile VS Code itself moved forward on September 23, 2026 with version 1.139, followed by a 1.139.1 patch on September 25 addressing follow-up issues, according to the official VS Code releases page. That is the baseline this tutorial targets, and it matters because the extension APIs for commands, webviews, and file-system watching all ship through that same release train, whether you run them in stock VS Code or inside Cursor’s fork.
Cursor Rules 101: Project Rules, User Rules, Team Rules, and AGENTS.md
Before writing a line of extension code, it helps to know exactly what you are building tooling for. Cursor’s own documentation describes rules as system-level instructions for the Agent, distinct from a one-off prompt because they persist, they are scoped, and they can bundle “prompts, scripts, and more together, making it easy to manage and share workflows across your team,” according to Cursor’s official rules documentation. There are four categories, and each one behaves differently enough that a management extension needs to treat them separately.
Project Rules live inside the repository. “Project rules live in .cursor/rules as .mdc files and are version-controlled,” per Cursor’s docs, which means they travel with a git clone and apply to everyone on the branch. User Rules sit outside any repo and apply globally to one person’s Cursor installation. Team Rules are pushed from an organization’s dashboard and now double as the policy source for Security Review. AGENTS.md is the odd one out: a plain markdown file at the project root that Cursor treats as an alternative or companion to .cursor/rules. Cursor’s help documentation puts it plainly: “Create an AGENTS.md file in your project root.”
| Rule type | Location | Format | Scope | Version-controlled? |
|---|---|---|---|---|
| Project Rules | .cursor/rules/ |
.mdc |
Single repository | Yes |
| User Rules | Cursor app settings | Plain text | Everywhere the user works | No |
| Team Rules | Org dashboard | Plain text / templates | All team projects, plus Security Review and Bugbot | Managed centrally |
| AGENTS.md | Project root | Markdown | Single repository (also read by other agent CLIs) | Yes |
One detail catches almost everyone the first time: the old .cursorrules single-file format has been superseded, and Cursor’s docs are explicit that “project rules must use the .mdc extension.” Skip that and the Agent silently ignores the file. Cursor also exposes two ways to author a rule without touching a text editor directly: typing /create-rule inside Agent, which drops a scaffolded .mdc file with frontmatter already filled in, or going through Customize → Rules → Add Rule in the UI. Our extension automates the same job from the command palette, with validation the built-in flow does not offer.
Prerequisites: Tools and Versions You’ll Need
This build uses TypeScript, the standard VS Code Extension API, and no external AI or MCP dependencies, so the toolchain is short. Install everything below before Step 1.
| Tool | Version used in this tutorial | Why you need it |
|---|---|---|
| Visual Studio Code or Cursor | VS Code 1.139.1 or newer / any recent Cursor build | Extension host and debugging (F5) target |
| Node.js | Node.js 24 (Krypton), current Active LTS | Runs the build tooling and the extension’s JavaScript |
| npm | Bundled with Node.js 24 | Installs dependencies and runs scripts |
| Yeoman + generator-code | Latest published version | Scaffolds the extension project structure |
| TypeScript | Latest published version | Compiles the extension source |
| @vscode/vsce | Latest published version | Packages the extension into a .vsix file |
| @vscode/test-cli and @vscode/test-electron | Latest published versions | Runs integration tests inside a real extension host |
| Git | Any recent version | Version-controls the extension and the sample repo’s .cursor/rules |
You do not need a Cursor Pro or Teams subscription to build or test this extension. It only reads and writes files inside .cursor/rules and AGENTS.md, it never calls the Cursor Agent API, and it works the same whether the person running it has Cursor open or plain VS Code. That is also what makes it installable from a .vsix in either editor without modification.
A quick note on why this tutorial avoids a YAML parsing library like js-yaml despite it being the obvious first instinct: pulling in a full YAML parser for three known keys adds a dependency, a supply-chain surface, and a bundle-size cost that is hard to justify for a schema this small. If your own rules grow more complex, with nested objects or nonstandard YAML features, swapping the hand-rolled parser in validate.ts for js-yaml is a one-file change and does not affect anything else covered in the steps ahead.
Step 1: Scaffold the Extension with Yeoman
Start from the official generator instead of hand-rolling a package.json. It saves you from chasing down the exact engines.vscode version and activation event syntax that changes slightly release to release.
npm install -g yo generator-code
yo code
# When prompted, choose:
# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? cursor-rules-manager
# ? What's the identifier of your extension? cursor-rules-manager
# ? Bundle the source code with webpack? Yes
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm
cd cursor-rules-manager
code .
Opening the folder with code . works whether your default editor is VS Code or Cursor, since both read the same workspace structure. Press F5 once the scaffold finishes to confirm the generator produced a working “Hello World” extension before you touch anything else. You should see a second Extension Development Host window launch with your unfinished extension already active.
Step 2: Define the .mdc Frontmatter Schema
Every .mdc file starts with YAML frontmatter that tells Cursor how and when to apply the rule: a description, an optional list of glob patterns that scope it to certain paths, and a flag for whether it should always be included in context. Getting this frontmatter exactly right is the entire reason to build validation tooling, because a typo here fails silently rather than throwing an error in the editor.
---
description: Enforce repository testing conventions for backend services
globs:
- "src/services/**/*.ts"
- "src/api/**/*.ts"
alwaysApply: false
---
# Backend Service Testing Rules
- Every exported function in `src/services/` must have a corresponding
test file in `src/services/__tests__/`.
- Do not mock the database client in integration tests; use the
test container defined in `docker-compose.test.yml`.
- Prefer `it.each` for parameterized test cases over copy-pasted blocks.
Create a src/schema.ts file in your extension project and encode that structure as a TypeScript interface. This becomes the single source of truth the rest of the extension validates against.
// src/schema.ts
export interface RuleFrontmatter {
description: string;
globs?: string[];
alwaysApply?: boolean;
}
export const REQUIRED_FIELDS: (keyof RuleFrontmatter)[] = ["description"];
export function isValidExtension(filePath: string): boolean {
return filePath.endsWith(".mdc");
}
Step 3-5: Build the “Create Rule” Command
Step 3: Register the command in package.json
Open package.json and add a contributes.commands entry alongside a keybinding and a command palette category so the command is discoverable without memorizing an ID.
{
"contributes": {
"commands": [
{
"command": "cursorRulesManager.createRule",
"title": "Cursor Rules: Create New Rule",
"category": "Cursor Rules"
},
{
"command": "cursorRulesManager.validateRules",
"title": "Cursor Rules: Validate All Rules",
"category": "Cursor Rules"
},
{
"command": "cursorRulesManager.browseRules",
"title": "Cursor Rules: Browse Rules",
"category": "Cursor Rules"
}
],
"keybindings": [
{
"command": "cursorRulesManager.createRule",
"key": "ctrl+alt+r",
"mac": "cmd+alt+r"
}
]
}
Step 4: Prompt for rule details and write the file
In src/extension.ts, register the command handler. It asks two quick questions with the built-in Quick Input API, generates a kebab-case filename, and writes the .mdc file into .cursor/rules, creating the directory if it does not exist yet.
// src/extension.ts
import * as vscode from "vscode";
import * as path from "path";
export function activate(context: vscode.ExtensionContext) {
const createRule = vscode.commands.registerCommand(
"cursorRulesManager.createRule",
async () => {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showErrorMessage("Open a workspace folder first.");
return;
}
const name = await vscode.window.showInputBox({
prompt: "Rule name (e.g. backend-testing-conventions)",
validateInput: (v) =>
/^[a-z0-9-]+$/.test(v) ? null : "Use lowercase letters, numbers, and hyphens only",
});
if (!name) return;
const description = await vscode.window.showInputBox({
prompt: "One-line description of what this rule enforces",
});
if (!description) return;
const globsInput = await vscode.window.showInputBox({
prompt: "Glob patterns this rule applies to (comma-separated, optional)",
});
const globsYaml = globsInput
? `globs:n${globsInput
.split(",")
.map((g) => ` - "${g.trim()}"`)
.join("n")}n`
: "";
const content = `---ndescription: ${description}n${globsYaml}alwaysApply: falsen---nn# ${name}nnn`;
const rulesDir = vscode.Uri.joinPath(workspaceFolder.uri, ".cursor", "rules");
await vscode.workspace.fs.createDirectory(rulesDir);
const fileUri = vscode.Uri.joinPath(rulesDir, `${name}.mdc`);
await vscode.workspace.fs.writeFile(fileUri, Buffer.from(content, "utf8"));
const doc = await vscode.workspace.openTextDocument(fileUri);
await vscode.window.showTextDocument(doc);
}
);
context.subscriptions.push(createRule);
}
Step 5: Reuse the AGENTS.md path for repos that prefer it
Some teams standardize on AGENTS.md instead of a folder of .mdc files, especially when the same repository is also driven by other coding agents. Add a second command that appends to (or creates) that file rather than writing a new .mdc, since AGENTS.md is meant to hold everything in one place rather than being split by concern.
const appendToAgentsFile = vscode.commands.registerCommand(
"cursorRulesManager.appendAgentsRule",
async () => {
const folder = vscode.workspace.workspaceFolders?.[0];
if (!folder) return;
const text = await vscode.window.showInputBox({
prompt: "Instruction to add to AGENTS.md",
});
if (!text) return;
const agentsUri = vscode.Uri.joinPath(folder.uri, "AGENTS.md");
let existing = "";
try {
const bytes = await vscode.workspace.fs.readFile(agentsUri);
existing = Buffer.from(bytes).toString("utf8");
} catch {
existing = "# Agent Instructionsnn";
}
const updated = `${existing.trimEnd()}n- ${text}n`;
await vscode.workspace.fs.writeFile(agentsUri, Buffer.from(updated, "utf8"));
vscode.window.showInformationMessage("AGENTS.md updated.");
}
);
context.subscriptions.push(appendToAgentsFile);
Both write paths are valid. Cursor’s CLI documentation confirms the two formats are meant to coexist rather than compete: “The CLI also reads AGENTS.md and CLAUDE.md at the project root (if present) and applies them as rules alongside .cursor/rules,” per Cursor’s CLI docs. That means the extension does not have to force a team to pick one format. It just needs to keep whichever format they already use internally consistent.
Step 6-8: Add Frontmatter Validation on Save
Step 6: Parse frontmatter without a heavy dependency
You do not need a full YAML library for three simple keys. A small parser keeps the extension’s bundle size down and avoids a supply-chain dependency for something this narrow in scope.
// src/validate.ts
import { REQUIRED_FIELDS, RuleFrontmatter } from "./schema";
export interface ValidationResult {
valid: boolean;
errors: string[];
}
export function parseFrontmatter(content: string): RuleFrontmatter | null {
const match = content.match(/^---n([sS]*?)n---/);
if (!match) return null;
const lines = match[1].split("n");
const result: Partial = {};
for (const line of lines) {
const kv = line.match(/^(w+):s*(.*)$/);
if (!kv) continue;
const [, key, value] = kv;
if (key === "alwaysApply") {
result.alwaysApply = value.trim() === "true";
} else if (key === "description") {
result.description = value.trim();
}
}
return result as RuleFrontmatter;
}
export function validateRule(content: string, filePath: string): ValidationResult {
const errors: string[] = [];
if (!filePath.endsWith(".mdc")) {
errors.push("Project rules must use the .mdc extension, not .md or .cursorrules.");
}
const frontmatter = parseFrontmatter(content);
if (!frontmatter) {
errors.push("Missing YAML frontmatter block (--- ... ---).");
return { valid: false, errors };
}
for (const field of REQUIRED_FIELDS) {
if (!frontmatter[field]) {
errors.push(`Missing required field: ${field}`);
}
}
return { valid: errors.length === 0, errors };
}
Step 7: Wire validation into onDidSaveTextDocument
Register a save listener that only fires for files inside .cursor/rules, then surface problems through the Diagnostics API so they show up in the Problems panel instead of a disruptive popup.
const diagnostics = vscode.languages.createDiagnosticCollection("cursor-rules");
context.subscriptions.push(diagnostics);
vscode.workspace.onDidSaveTextDocument((doc) => {
if (!doc.uri.fsPath.includes(`${path.sep}.cursor${path.sep}rules${path.sep}`)) {
return;
}
const result = validateRule(doc.getText(), doc.uri.fsPath);
if (result.valid) {
diagnostics.delete(doc.uri);
return;
}
const issues = result.errors.map(
(msg) =>
new vscode.Diagnostic(
new vscode.Range(0, 0, 0, 1),
msg,
vscode.DiagnosticSeverity.Warning
)
);
diagnostics.set(doc.uri, issues);
});
Step 8: Add a workspace-wide validate command
Per-file validation catches mistakes as you write them, but a command that checks every rule at once is what actually gets used before a pull request, especially now that Security Review reads team rules against every PR. Use vscode.workspace.findFiles to glob the whole rules folder.
const validateAll = vscode.commands.registerCommand(
"cursorRulesManager.validateRules",
async () => {
const files = await vscode.workspace.findFiles(".cursor/rules/**/*.{mdc,md}");
let problems = 0;
for (const file of files) {
const bytes = await vscode.workspace.fs.readFile(file);
const result = validateRule(Buffer.from(bytes).toString("utf8"), file.fsPath);
if (!result.valid) {
problems += result.errors.length;
vscode.window.showWarningMessage(
`${path.basename(file.fsPath)}: ${result.errors.join("; ")}`
);
}
}
vscode.window.showInformationMessage(
problems === 0
? `All ${files.length} rules passed validation.`
: `Found ${problems} issue(s) across ${files.length} rules.`
);
}
);
context.subscriptions.push(validateAll);
Step 9-10: Build a Webview Panel to Browse All Rules
Step 9: Create the panel and render a summary table
A folder full of .mdc files is hard to scan in the file explorer, since every filename looks the same and none of them show their glob scope. A webview panel that reads every rule and lays out its description, globs, and alwaysApply flag in one table solves that without leaving the editor. Follow the official VS Code webview guide for the security-sensitive parts (content security policy, nonce generation) rather than skipping them for convenience.
// src/rulesPanel.ts
import * as vscode from "vscode";
import { parseFrontmatter } from "./validate";
export async function showRulesPanel(context: vscode.ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
"cursorRulesBrowser",
"Cursor Rules",
vscode.ViewColumn.One,
{ enableScripts: false }
);
const files = await vscode.workspace.findFiles(".cursor/rules/**/*.mdc");
const rows = await Promise.all(
files.map(async (file) => {
const bytes = await vscode.workspace.fs.readFile(file);
const text = Buffer.from(bytes).toString("utf8");
const fm = parseFrontmatter(text);
const name = file.fsPath.split("/").pop();
return `${name} ${fm?.description ?? ""} ${
fm?.alwaysApply ? "Always" : "Scoped"
} `;
})
);
panel.webview.html = `
${rows.length} Project Rules Found
File Description Application
${rows.join("")}
`;
}
Step 10: Register the browse command and clean up disposables
import { showRulesPanel } from "./rulesPanel";
const browseRules = vscode.commands.registerCommand(
"cursorRulesManager.browseRules",
() => showRulesPanel(context)
);
context.subscriptions.push(browseRules);
Notice enableScripts: false in the panel options. This extension never needs to run JavaScript inside the webview, it only renders a static table, so leaving scripting disabled removes an entire category of webview security review from your plate. Enable it only if you later add interactivity, and if you do, follow the nonce-based content security policy pattern from the official guide rather than inlining a broad unsafe-inline policy.
Step 11-13: Test, Package, and Install in Cursor and VS Code
Step 11: Write an integration test with @vscode/test-cli
Unit-testing validateRule is straightforward since it is a pure function, but the command registration and file-writing behavior need a real extension host. Add a test that scaffolds a temporary workspace, runs the create command, and asserts the file lands where expected.
// src/test/validate.test.ts
import * as assert from "assert";
import { validateRule } from "../validate";
suite("Rule validation", () => {
test("rejects a file missing frontmatter", () => {
const result = validateRule("# Just a heading, no frontmatter", "rule.mdc");
assert.strictEqual(result.valid, false);
assert.ok(result.errors.some((e) => e.includes("frontmatter")));
});
test("rejects the wrong file extension", () => {
const content = "---ndescription: testn---n";
const result = validateRule(content, "rule.cursorrules");
assert.ok(result.errors.some((e) => e.includes(".mdc")));
});
test("accepts a well-formed rule", () => {
const content = "---ndescription: Enforce test coveragenalwaysApply: falsen---nnBody text.";
const result = validateRule(content, "rule.mdc");
assert.strictEqual(result.valid, true);
});
});
Run it with npm test, which the Yeoman scaffold already wires to @vscode/test-cli. It launches a disposable instance of VS Code, loads your extension, and runs the suite inside that real process rather than mocking the API surface, which catches activation-order bugs that pure unit tests miss entirely.
Step 12: Package the extension into a .vsix
npm install -g @vscode/vsce
npm run compile
vsce package
# Produces: cursor-rules-manager-0.0.1.vsix
Step 13: Install and verify in both editors
Install the packaged file the same way in either editor, since Cursor’s extension host accepts standard .vsix packages built against the public VS Code Extension API.
# VS Code
code --install-extension cursor-rules-manager-0.0.1.vsix
# Cursor
cursor --install-extension cursor-rules-manager-0.0.1.vsix
Open the command palette (Ctrl+Shift+P or Cmd+Shift+P) in a project and type “Cursor Rules” to confirm all three commands appear under that category. Run Create New Rule, fill in a description, then run Browse Rules to see it listed in the webview panel. If both steps work, the extension is functioning end to end.
Output Example: What a Working Session Looks Like
Running Cursor Rules: Create New Rule from the palette and answering the three prompts (name: api-error-handling, description: “Standardize error responses across API routes”, globs: src/api/**/*.ts) produces this file at .cursor/rules/api-error-handling.mdc:
---
description: Standardize error responses across API routes
globs:
- "src/api/**/*.ts"
alwaysApply: false
---
# api-error-handling
Running Cursor Rules: Validate All Rules right after creating an intentionally broken file (one saved with a .md extension and no frontmatter) reports back in the notification area: “Found 2 issue(s) across 3 rules,” followed by a per-file warning such as “broken-rule.md: Project rules must use the .mdc extension, not .md or .cursorrules.” A second warning for the same file adds “Missing YAML frontmatter block (— … —).” Fix both problems, save, and re-running the command returns “All 3 rules passed validation.”
The Complete Working Project
Here is the full file tree once every step above is applied. Nothing in this list depends on a paid Cursor plan, an API key, or a network call, which keeps the extension usable for teams that cannot run third-party network code inside their editor.
cursor-rules-manager/
├── package.json # commands, keybindings, activation events
├── src/
│ ├── extension.ts # activate(), command registration
│ ├── schema.ts # RuleFrontmatter interface
│ ├── validate.ts # parseFrontmatter, validateRule
│ ├── rulesPanel.ts # webview browser
│ └── test/
│ └── validate.test.ts # @vscode/test-cli suite
├── tsconfig.json
└── cursor-rules-manager-0.0.1.vsix # output of `vsce package`
From here, the natural next step is publishing it somewhere teammates can install from, whether that is a private VS Code Marketplace organization feed or an internal Open VSX registry mirror, but packaging and distribution mechanics for either path are a separate, larger topic on their own.
Common Pitfalls When Building a Rules Management Extension
1. Writing to the wrong root when multiple workspace folders exist. The sample code above grabs workspaceFolders?.[0] for simplicity, but a real multi-root workspace needs vscode.window.showWorkspaceFolderPick() so a rule does not silently land in the wrong repository.
2. Forgetting that .cursorrules is legacy. Older repositories still carry a single .cursorrules file from before the .mdc format existed. A validation command that only checks .cursor/rules/**/*.mdc will silently miss a stale .cursorrules file sitting at the project root, giving a false sense that everything is current.
3. Overly broad glob patterns that make alwaysApply redundant. A rule scoped to **/* behaves like alwaysApply: true but costs more tokens per request because Cursor still evaluates the glob match. Be deliberate about which rules truly need to run everywhere.
4. Enabling webview scripts before you need them. It is tempting to add enableScripts: true “just in case,” but every enabled script surface is something a security reviewer, or Security Review itself if the extension repo lives inside a Cursor Teams org, now has to account for.
5. Skipping activation event scoping. The Yeoman default activates on onStartupFinished, which is fine for a small extension, but as commands grow, prefer onCommand:cursorRulesManager.createRule-style granular activation so VS Code and Cursor do not spin up your extension host code for workspaces that never touch rules.
6. Treating AGENTS.md and .cursor/rules as mutually exclusive in your own logic. Since Cursor’s CLI reads both formats together, an extension that only manages one of them will confuse a team that has quietly adopted the other, especially on a repo shared with a different coding agent.
Troubleshooting: 8 Issues and Fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Command palette does not show “Cursor Rules” commands | Extension not activated, or a typo in the command ID between package.json and extension.ts | Check the Extension Development Host’s Debug Console for activation errors; confirm the IDs match exactly |
| Rule file created but Cursor Agent never applies it | File saved with a .md extension instead of .mdc |
Rename the file; Cursor’s docs are explicit that project rules must use .mdc |
| Validation command reports 0 files found | Glob pattern in findFiles does not match the actual folder depth | Confirm the rules truly live at .cursor/rules/*.mdc and not a nested subfolder the glob excludes |
| Webview panel opens blank | An unhandled exception was thrown while reading files, before panel.webview.html was set |
Wrap the file-reading loop in try/catch and render a fallback message on failure |
| F5 debugging opens a window but the extension is inactive | Build step did not run, so the compiled JS in out/ or dist/ is stale |
Run npm run watch in a separate terminal before pressing F5 |
| vsce package fails with a missing repository field | Recent vsce versions warn or fail without a repository field in package.json |
Add a repository URL, even a placeholder one for local testing, or pass --allow-missing-repository |
| Extension works in VS Code but a command is missing in Cursor | Cursor’s compatibility layer does not support every proposed (pre-release) API | Avoid proposed APIs; stick to the stable, published VS Code Extension API surface |
| Diagnostics never clear after fixing a rule | The onDidSaveTextDocument handler only sets diagnostics, it never calls diagnostics.delete() on success |
Confirm the success branch explicitly clears the collection for that file URI |
Advanced Tips: Team Rules, Security Review, and Multi-Root Workspaces
Once the base extension works, a few extensions to the design pay off for larger teams. First, if your organization uses Cursor Teams or Enterprise, remember that Security Review reads team rules against every pull request looking specifically for injection flaws, auth bypasses, committed secrets, SSRF, unsafe deserialization, and vulnerable dependency changes. That means a rule describing a security convention, such as “never construct SQL with string concatenation,” has a direct, measurable payoff beyond just steering the Agent’s style. Consider adding a “severity” custom field to your frontmatter schema (even though Cursor itself does not require one) so your validate command can flag security-relevant rules for extra review before merge.
Second, build in support for the home-directory rules path. Cursor’s changelog confirms that rules in ~/.cursor/rules are included in context alongside project rules, so a “list all active rules” command that only checks the workspace is technically incomplete. Reading the home directory requires os.homedir() rather than a workspace-relative path, and it is worth gating behind a setting since not every user wants their personal rules surfaced in a shared demo.
Third, plan for Cursor Projects. Because Projects delegate work to subagents running over long stretches without a human in the loop, a rule that is ambiguous or contradicts another rule becomes far more expensive to catch, since nobody is watching every turn. Add a lint check to your validate command that flags two rules whose glob patterns overlap and whose descriptions contain conflicting verbs like “always” and “never” applied to the same file pattern. It will not catch every conflict, but it catches the obvious ones before a long-running agent burns hours on contradictory instructions.
Finally, if you are distributing this extension to a team that already relies on a similar public tool, know what exists. The Cursor Project Rules extension on the VS Code Marketplace already solves the specific problem of syncing rules across multiple git repositories, noting that “the rules will be added to the .cursor/rules directory in your workspace.” Building your own version makes sense when you need custom validation logic tied to your organization’s specific Security Review policies, but check the marketplace first if all you need is basic multi-repo syncing.
One more thing worth tracking as your extension matures: the broader agent-instruction ecosystem is converging on shared conventions rather than fragmenting further. The AGENTS.md specification site documents the format as a cross-tool standard rather than a Cursor-only feature, which is exactly why Cursor’s CLI reads it alongside its own native rule format instead of replacing it.
Bonus: Enforce Rule Validation in CI with a Pre-Commit Hook
The extension catches broken rules while you are actively working in the editor, but it cannot stop a teammate who edits a .mdc file in a terminal, or a script that generates rules during a migration, from committing something malformed. Since the validation logic in validate.ts is a plain TypeScript function with no dependency on the vscode module, you can lift it straight into a standalone Node script and run it as a Git pre-commit hook with Husky.
npm install --save-dev husky
npx husky init
Extract the validation logic into a repo-level script that does not import vscode at all, so it can run outside the extension host:
// scripts/validate-rules.mjs
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
const rulesDir = ".cursor/rules";
let hasErrors = false;
for (const file of readdirSync(rulesDir)) {
const fullPath = join(rulesDir, file);
const content = readFileSync(fullPath, "utf8");
if (!file.endsWith(".mdc")) {
console.error(`${file}: must use the .mdc extension`);
hasErrors = true;
continue;
}
const match = content.match(/^---n([sS]*?)n---/);
if (!match || !/description:s*S+/.test(match[1])) {
console.error(`${file}: missing or empty description in frontmatter`);
hasErrors = true;
}
}
if (hasErrors) {
console.error("nFix the rule files above before committing.");
process.exit(1);
}
console.log("All Cursor rules passed validation.");
Then wire it into the generated hook file:
echo "node scripts/validate-rules.mjs" >> .husky/pre-commit
Now a commit that touches .cursor/rules fails fast, locally, before it ever reaches a pull request, which matters more than it used to now that Security Review evaluates team rules against every PR on Teams and Enterprise plans. Catching a malformed file at commit time is far cheaper than catching it after Security Review or a confused Agent run downstream. Teams that use a monorepo with several .cursor/rules folders across packages can loop the script over each package directory instead of hardcoding a single path, which is a small change worth making before rolling this out past a single-repo pilot.
Frequently Asked Questions
Does a VS Code extension built this way also work inside Cursor without changes?
Yes. Cursor is built on the same open-source core as VS Code and installs standard .vsix packages through its own extension host, as long as the extension sticks to the stable, published Extension API rather than proposed or Microsoft-internal APIs.
What is the difference between .cursorrules and .cursor/rules?.cursorrules was the original single-file format and is now legacy. The current format is a folder, .cursor/rules, containing individual .mdc files, each with its own frontmatter and optional glob scoping.
Can I skip .cursor/rules entirely and just use AGENTS.md?
Yes, and Cursor’s CLI documentation confirms both formats are read together, applying AGENTS.md and CLAUDE.md alongside .cursor/rules, so you can pick whichever fits your team, or use both for different purposes.
Do I need a Cursor subscription to test this extension?
No. The extension only reads and writes local files. It never calls a Cursor API or requires authentication, so you can develop and test it entirely in free VS Code and only need Cursor installed if you want to confirm cross-editor compatibility.
Why does my rule get ignored by the Cursor Agent even though the file looks correct?
The most common cause is a wrong file extension (.md instead of .mdc), followed by a malformed YAML frontmatter block, followed by a glob pattern that does not actually match the file the Agent is editing. Run the validate command from this tutorial to catch the first two automatically.
Should team rules and project rules ever contain the same instruction?
Avoid duplicating instructions across the two. Team Rules are managed centrally from the dashboard and now feed Security Review, so keeping organization-wide policy there and repository-specific detail in Project Rules avoids the two drifting out of sync over time.
Can this extension be extended to auto-generate rules from an existing codebase?
Yes, that is a natural next feature. You would scan the codebase for existing conventions (linter configs, test patterns, folder structure) and pre-fill the description and globs fields, though verifying the generated rule actually reflects the codebase still needs a human review step.
Does upgrading to VS Code 1.139 break any of the APIs used here?
No. Every API used in this tutorial (commands, webview panels, diagnostics, workspace file system) is part of the stable API surface and has been for multiple releases. The 1.139 update and its 1.139.1 patch did not deprecate any of them.