Build This Now
Build This Now
What Is Claude CodeInstallationNative InstallerFirst Project
speedy_devvkoen_salo
Blog/Handbook/Workflow/Cursor Migration Guide

Cursor Migration Guide

Migrate a Cursor project to Claude Code without losing rules, commands, MCP servers, or shared agent instructions. Includes a safe conversion script and a verification checklist.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

See what we build for companies →
speedy_devvWritten by speedy_devvPublished Jul 27, 202614 min readHandbook hubWorkflow index

A safe Cursor to Claude Code migration maps each instruction to the Claude Code feature with the same job. Always-on repository facts belong in CLAUDE.md, file-specific rules belong in .claude/rules/, repeatable commands belong in .claude/skills/, and shared MCP servers belong in .mcp.json after a security review.

There is no honest one-to-one conversion for every Cursor rule. Cursor attachment modes map to a mix of Claude memory, path rules, and skills. The script deliberately leaves ambiguous rules for a human to classify.

The short mapping

Cursor project artifactClaude Code destinationWhat changes
Root .cursorrulesCLAUDE.md or .claude/rules/legacy-cursor.mdLegacy Cursor rules become explicit project memory
Always-applied .cursor/rules/*.mdc.claude/rules/*.md without pathsThe rule loads for every Claude Code session
Glob-scoped .cursor/rules/*.mdc.claude/rules/*.md with pathsCursor globs become Claude Code path patterns
Agent-requested Cursor ruleCandidate .claude/skills/<name>/SKILL.mdStart manual, then enable model invocation after reviewing the trigger
Manual Cursor ruleUser-invoked Claude Code skillInvoke it with /skill-name, not Cursor's @ruleName syntax
.cursor/commands/*.md.claude/skills/<name>/SKILL.mdThe slash workflow gains skill metadata
.cursor/mcp.jsonRoot .mcp.jsonReview commands, secrets, and scope before copying
Root AGENTS.mdAGENTS.md plus a CLAUDE.md importShared instructions stay in one file

Cursor documents Always, Auto Attached, Agent Requested, and Manual rules in its official rules guide, which also marks .cursorrules as legacy. Anthropic recommends CLAUDE.md for session-wide facts, path-scoped rules for files, and skills for procedures.

Final file tree

The migration keeps Cursor files during the trial. That makes rollback boring, which is exactly what you want.

your-project/
├── .claude/
│   ├── rules/
│   │   ├── api-validation.md
│   │   └── legacy-cursor.md
│   └── skills/
│       ├── create-pr/
│       │   └── SKILL.md
│       └── security-review/
│           └── SKILL.md
├── .cursor/
│   ├── commands/
│   ├── rules/
│   └── mcp.json
├── scripts/
│   └── migrate-cursor-config.mjs
├── .mcp.json
├── AGENTS.md
└── CLAUDE.md

Keep .cursor/ until Claude passes the checks below.

Put shared instructions in AGENTS.md

Cursor CLI reads root AGENTS.md and CLAUDE.md, according to the Cursor CLI documentation. Claude Code reads CLAUDE.md, so keep cross-tool facts in AGENTS.md and import them.

# Project instructions

## Repository map

- `apps/web` is the Next.js 16 application.
- `packages/database` owns schema and migrations.
- `packages/ui` owns shared React components.

## Commands

- Install with `pnpm install --frozen-lockfile`.
- Run the nearest package test while editing.
- Run `pnpm lint && pnpm typecheck && pnpm test` before completion.

## Boundaries

- Do not edit generated files.
- Never run production migrations from an agent session.
- Shared code belongs in `packages`, not another app's private directory.

Then make Claude Code load the shared file. Claude Code imports resolve relative to the file containing the import.

@AGENTS.md

# Claude Code

- Read the nearest nested `CLAUDE.md` before editing a package.
- Use package-filtered checks during implementation.
- Report any verification command that could not run.

This avoids three drifting copies. Test the bridge in both tools before removing old rules.

Convert rules and commands safely

The script below uses only Node.js standard-library modules. It performs a dry run by default, refuses to overwrite existing Claude files, and writes only when you pass --write.

It handles the common Cursor MDC fields: description, globs, and alwaysApply. A root rule with globs becomes a path-scoped Claude rule. An always-applied root rule becomes an unscoped Claude rule. A nested rule without globs retains its directory scope. Nested rules that also declare globs are skipped because the two tools may resolve that combination differently. Rules without a file scope become manually invoked skills, even when they have a description. That conservative default matters because the MDC fields do not prove that a hand-edited rule is safe to invoke automatically.

Save this as scripts/migrate-cursor-config.mjs.

import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";

const root = process.cwd();
const writeChanges = process.argv.includes("--write");
const includeMcp = process.argv.includes("--include-mcp");
const planned = [];
const warnings = [];
const ignoredDirectories = new Set([
  ".git",
  ".claude",
  ".next",
  ".turbo",
  "coverage",
  "dist",
  "node_modules",
]);

function splitFrontmatter(source) {
  if (!source.startsWith("---\n")) {
    return { data: {}, body: source.trim(), parseWarnings: [] };
  }

  const end = source.indexOf("\n---\n", 4);
  if (end === -1) {
    return {
      data: {},
      body: source.trim(),
      parseWarnings: ["frontmatter has no closing delimiter"],
    };
  }

  const header = source.slice(4, end);
  const body = source.slice(end + 5).trim();
  const data = {};
  const parseWarnings = [];
  let currentKey = null;

  for (const rawLine of header.split("\n")) {
    const line = rawLine.trim();
    if (!line || line.startsWith("#")) continue;

    const listItem = rawLine.match(/^\s+-\s+(.+)$/);
    if (listItem && currentKey) {
      if (!Array.isArray(data[currentKey])) data[currentKey] = [];
      data[currentKey].push(listItem[1].trim().replace(/^["']|["']$/g, ""));
      continue;
    }

    const separator = line.indexOf(":");
    if (separator === -1) continue;

    const key = line.slice(0, separator).trim();
    let value = line.slice(separator + 1).trim();
    currentKey = key;

    if (value === "|" || value === ">") {
      parseWarnings.push(`multiline ${key} must be reviewed manually`);
      continue;
    }

    value = value.replace(/^["']|["']$/g, "");

    if (value === "true") value = true;
    if (value === "false") value = false;
    data[key] = value;
  }

  return { data, body, parseWarnings };
}

function parseGlobs(value) {
  if (Array.isArray(value)) return value.filter(Boolean);
  if (typeof value !== "string" || value.trim() === "") return [];

  const unwrapped = value.replace(/^\[/, "").replace(/\]$/, "");
  return unwrapped
    .split(",")
    .map((item) => item.trim().replace(/^["']|["']$/g, ""))
    .filter(Boolean);
}

function yamlString(value) {
  return JSON.stringify(value);
}

function safeName(value) {
  const name = value
    .toLowerCase()
    .replace(/[^a-z0-9-]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 64);

  if (!name) throw new Error(`Cannot create a safe name from ${value}`);
  return name;
}

async function exists(file) {
  try {
    await stat(file);
    return true;
  } catch {
    return false;
  }
}

async function planFile(relativeTarget, content, source) {
  const target = path.join(root, relativeTarget);

  if (
    (await exists(target)) ||
    planned.some((item) => item.relativeTarget === relativeTarget)
  ) {
    warnings.push(`SKIP ${relativeTarget}: target already exists`);
    return;
  }

  planned.push({ target, relativeTarget, content, source });
}

async function listFiles(directory, extension) {
  if (!(await exists(directory))) return [];

  const entries = await readdir(directory, { withFileTypes: true });
  return entries
    .filter((entry) => entry.isFile() && entry.name.endsWith(extension))
    .map((entry) => path.join(directory, entry.name))
    .sort();
}

async function findCursorRules(directory = root) {
  const entries = await readdir(directory, { withFileTypes: true });
  const files = [];

  for (const entry of entries) {
    if (!entry.isDirectory() || ignoredDirectories.has(entry.name)) continue;

    const child = path.join(directory, entry.name);
    if (entry.name === ".cursor") {
      files.push(...(await listFiles(path.join(child, "rules"), ".mdc")));
      continue;
    }

    files.push(...(await findCursorRules(child)));
  }

  return files.sort();
}

async function convertRule(file) {
  const source = await readFile(file, "utf8");
  const { data, body, parseWarnings } = splitFrontmatter(source);
  const relativeSource = path.relative(root, file).split(path.sep).join("/");
  const marker = "/.cursor/rules/";
  const markedSource = `/${relativeSource}`;
  const markerIndex = markedSource.lastIndexOf(marker);
  const scope = markedSource.slice(1, markerIndex);
  const rawName = [scope, path.basename(file, ".mdc")]
    .filter(Boolean)
    .join("-");
  const name = safeName(rawName);
  const globs = parseGlobs(data.globs);

  if (parseWarnings.length > 0) {
    warnings.push(
      `SKIP ${relativeSource}: ${parseWarnings.join(", ")}`,
    );
    return;
  }

  if (/^\s*@\S+/m.test(body)) {
    warnings.push(
      `SKIP ${relativeSource}: Cursor @file references need a manual path review`,
    );
    return;
  }

  if (scope && globs.length > 0) {
    warnings.push(
      `SKIP ${relativeSource}: combine its nested directory scope and globs manually`,
    );
    return;
  }

  if (data.alwaysApply === true && !scope) {
    await planFile(
      `.claude/rules/${name}.md`,
      `${body}\n`,
      relativeSource,
    );
    return;
  }

  if (globs.length > 0 || scope) {
    const scopedGlobs = globs.length > 0 ? globs : [`${scope}/**`];
    const paths = scopedGlobs
      .map((glob) => `  - ${yamlString(glob)}`)
      .join("\n");
    await planFile(
      `.claude/rules/${name}.md`,
      `---\npaths:\n${paths}\n---\n\n${body}\n`,
      relativeSource,
    );
    return;
  }

  const description =
    typeof data.description === "string" && data.description
      ? data.description
      : `Run the migrated ${name} workflow when the user invokes it.`;
  await planFile(
    `.claude/skills/${name}/SKILL.md`,
    `---\ndescription: ${yamlString(description)}\ndisable-model-invocation: true\n---\n\n${body}\n`,
    relativeSource,
  );
}

async function convertCommand(file) {
  const source = (await readFile(file, "utf8")).trim();
  const name = safeName(path.basename(file, ".md"));

  if (/^\s*@\S+/m.test(source)) {
    warnings.push(
      `SKIP ${path.relative(root, file)}: Cursor @file references need a manual path review`,
    );
    return;
  }

  await planFile(
    `.claude/skills/${name}/SKILL.md`,
    [
      "---",
      `description: ${yamlString(`Run the migrated Cursor ${name} workflow.`)}`,
      "disable-model-invocation: true",
      "---",
      "",
      source,
      "",
    ].join("\n"),
    path.relative(root, file),
  );
}

async function convertLegacyRules() {
  const file = path.join(root, ".cursorrules");
  if (!(await exists(file))) return;

  const source = (await readFile(file, "utf8")).trim();
  await planFile(
    ".claude/rules/legacy-cursor.md",
    `${source}\n`,
    ".cursorrules",
  );
}

async function convertMcp() {
  if (!includeMcp) return;

  const sourceFile = path.join(root, ".cursor", "mcp.json");
  if (!(await exists(sourceFile))) return;

  const source = await readFile(sourceFile, "utf8");
  const parsed = JSON.parse(source);

  if (
    typeof parsed !== "object" ||
    parsed === null ||
    typeof parsed.mcpServers !== "object" ||
    parsed.mcpServers === null
  ) {
    throw new Error(".cursor/mcp.json has no mcpServers object");
  }

  const secretPattern = /(token|secret|password|api[_-]?key)/i;
  if (secretPattern.test(source)) {
    warnings.push(
      "MCP config contains a secret-like name. Confirm values use environment variables.",
    );
  }

  await planFile(
    ".mcp.json",
    `${JSON.stringify(parsed, null, 2)}\n`,
    ".cursor/mcp.json",
  );
}

const cursorRules = await findCursorRules();
const cursorCommands = await listFiles(
  path.join(root, ".cursor", "commands"),
  ".md",
);

await convertLegacyRules();
for (const file of cursorRules) await convertRule(file);
for (const file of cursorCommands) await convertCommand(file);
await convertMcp();

for (const item of planned) {
  console.log(
    `${writeChanges ? "WRITE" : "PLAN "} ${item.relativeTarget} <- ${item.source}`,
  );

  if (writeChanges) {
    await mkdir(path.dirname(item.target), { recursive: true });
    await writeFile(item.target, item.content, { flag: "wx" });
  }
}

for (const warning of warnings) console.warn(warning);

if (!writeChanges) {
  console.log("\nDry run only. Re-run with --write after reviewing the plan.");
}
if (!includeMcp) {
  console.log(
    "MCP was not copied. Add --include-mcp only after reviewing server commands and secrets.",
  );
}

The parser handles inline and multi-line glob arrays, but it is not a general YAML parser. It skips multiline metadata and Cursor @file references instead of moving a reference to a directory where it may resolve differently. It also normalizes generated skill names to Claude Code's lowercase, hyphenated naming rules.

Run the migration

Start from a clean diff and run the dry mode.

node scripts/migrate-cursor-config.mjs

Review each pair, then write the files.

node scripts/migrate-cursor-config.mjs --write

Copy MCP configuration only as a separate decision.

node scripts/migrate-cursor-config.mjs --write --include-mcp

If .mcp.json already exists, the script skips it. Merge the mcpServers objects by hand so an existing Claude server is not discarded.

Review the generated rules

Automated classification gets you to a reviewable draft. It cannot know whether a repository rule is still useful or whether a described Cursor rule was Agent Requested rather than Manual.

Open every generated file and ask:

  1. Should Claude know this on every task, only for matching files, or only when a workflow is requested?
  2. Does the rule describe a current command and directory?
  3. Does a tool already enforce it, or does it contain Cursor-only language?

Move procedures out of always-loaded rules and into skills. Keep CLAUDE.md short. Anthropic's memory guide recommends targeting fewer than 200 lines per CLAUDE.md, since long files consume context and reduce adherence. For a safe Agent Requested rule, remove disable-model-invocation: true only after its description is specific enough to trigger at the right time.

Cursor rule globs and Claude rule paths look similar, but test them. Claude Code loads a path-scoped rule when it works with a matching file. Use /context to confirm which instruction files are active.

Review skills instead of assuming command parity

Cursor commands are Markdown files under .cursor/commands. Claude Code still supports .claude/commands, but Anthropic has merged custom commands into skills. The script makes every migrated skill manual. Remove disable-model-invocation: true only when the description is precise and automatic invocation is safe. Releases, production migrations, and destructive cleanup should stay manual.

Audit MCP servers before copying

Cursor stores project MCP configuration in .cursor/mcp.json. Claude Code stores team-shared project servers in root .mcp.json, as documented in the Claude Code MCP guide.

Similar JSON does not make an MCP server trustworthy. Before copying it:

  1. Read every command, args, URL, and package name.
  2. Replace literal credentials with environment-variable references.
  3. Confirm every contributor should receive this server.
  4. Run claude mcp list, then inspect and authenticate it through /mcp.

Test one server at a time. Claude Code asks for approval before it uses a project-scoped server from .mcp.json. A server may also depend on Cursor-specific environment variables or a local executable path that Claude cannot access.

Verify behavior with a fixed task

Run one contained task with a known answer in both tools on separate branches. Judge the resulting behavior and diff, not whether Claude merely sees the files.

Use this verification grid.

CheckPassing result
Shared repository rulesClaude follows the commands and boundaries from AGENTS.md
Path ruleA web-only rule loads for a web file and stays out of an unrelated package task
Manual workflow/create-pr appears, but Claude does not invoke it by itself
Automatic skillClaude finds a narrowly described review skill only when relevant
MCPApproved tools connect, while unconfigured servers remain unavailable
CompletionClaude runs the requested checks and reports anything it skipped

Check whether the agent stayed in scope, used the canonical command, obeyed the boundary, and passed the tests.

For larger repositories, pair this migration with the context management guide and the Claude monorepo setup. If your team is choosing between editors rather than migrating configuration, the Claude Code, Cursor, and Copilot comparison covers the workflow differences.

Cursor migration FAQ

Can Claude Code use Cursor rules directly?

Claude Code does not treat .cursor/rules as native configuration. Preserve each rule's intent by moving always-on facts to CLAUDE.md, scoped guidance to .claude/rules, and procedures to skills.

Should I delete the .cursor directory after migration?

No. Keep both configurations for one or two representative tasks. Delete Cursor files only after Claude Code follows the same boundaries and passes the same checks.

Will the same MCP configuration work in both tools?

Not always. Review commands, transports, environment variables, and server scope. Test each copied server with claude mcp list and /mcp.

The migration is complete when Claude Code gets the right context for a task, not when every Cursor file has a Claude-shaped copy. Leave uncertain rules alone, test the important workflows, and simplify after the behavior matches.

If you want a ready-made multi-agent engineering harness instead of assembling the workflow yourself, the Code Kit is a $29 one-time purchase. It does not include model access, and Claude Code still requires a paid Anthropic plan.

Posted by @speedy_devv

Continue in Workflow

  • Agentic Commerce: How to Build an App AI Agents Can Pay For
    A plain-English guide to agentic commerce in 2026: what x402, ACP, and the Machine Payments Protocol do, plus a weekend walkthrough for shipping a paid API that AI agents can buy from.
  • Claude Code Best Practices
    Five habits separate engineers who ship with Claude Code: PRDs, modular CLAUDE.md rules, custom slash commands, /clear resets, and a system-evolution mindset.
  • Claude Code Auto Mode
    A second Sonnet model reviews every Claude Code tool call before it fires. What auto mode blocks, what it allows, and the allow rules it drops in your settings.
  • Channels, Routines, Teleport, Dispatch
    The four Claude Code features Anthropic shipped in March and April 2026 that turn the CLI into an event-driven coordination layer across phone, web, and desktop.
  • Claude Code 1M Context in Practice: When Bigger Isn't Better
    The 1M-token context window is GA at flat pricing, but bigger isn't always better. A decision framework, token-cost math, and when to use /compact, subagents, and dynamic workflows instead.
  • How to Build an Admin Dashboard With Claude Code
    Ship an internal admin panel with Claude Code: role-gated routes, a searchable users and orders table with pagination, impersonation-safe RLS, and metrics tiles pulled through a type-safe API.

More from Handbook

  • Agent Fundamentals
    Five ways to build specialist agents in Claude Code: Task sub-agents, .claude/agents YAML, custom slash commands, CLAUDE.md personas, and perspective prompts.
  • Agent Harness Engineering
    The harness is every layer around your AI agent except the model itself. Learn the five control levers, the constraint paradox, and why harness design determines agent performance more than the model does.
  • Agent Patterns
    Orchestrator, fan-out, validation chain, specialist routing, progressive refinement, and watchdog. Six orchestration shapes to wire Claude Code sub-agents with.
  • Agent Teams Best Practices
    Battle-tested patterns for Claude Code Agent Teams. Context-rich spawn prompts, right-sized tasks, file ownership, delegate mode, and v2.1.33-v2.1.45 fixes.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

See what we build for companies →

On this page

The short mapping
Final file tree
Put shared instructions in AGENTS.md
Convert rules and commands safely
Run the migration
Review the generated rules
Review skills instead of assuming command parity
Audit MCP servers before copying
Verify behavior with a fixed task
Cursor migration FAQ
Can Claude Code use Cursor rules directly?
Should I delete the .cursor directory after migration?
Will the same MCP configuration work in both tools?

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

See what we build for companies →