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.
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。
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 artifact | Claude Code destination | What changes |
|---|---|---|
Root .cursorrules | CLAUDE.md or .claude/rules/legacy-cursor.md | Legacy Cursor rules become explicit project memory |
Always-applied .cursor/rules/*.mdc | .claude/rules/*.md without paths | The rule loads for every Claude Code session |
Glob-scoped .cursor/rules/*.mdc | .claude/rules/*.md with paths | Cursor globs become Claude Code path patterns |
| Agent-requested Cursor rule | Candidate .claude/skills/<name>/SKILL.md | Start manual, then enable model invocation after reviewing the trigger |
| Manual Cursor rule | User-invoked Claude Code skill | Invoke it with /skill-name, not Cursor's @ruleName syntax |
.cursor/commands/*.md | .claude/skills/<name>/SKILL.md | The slash workflow gains skill metadata |
.cursor/mcp.json | Root .mcp.json | Review commands, secrets, and scope before copying |
Root AGENTS.md | AGENTS.md plus a CLAUDE.md import | Shared 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.mdKeep .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.mjsReview each pair, then write the files.
node scripts/migrate-cursor-config.mjs --writeCopy MCP configuration only as a separate decision.
node scripts/migrate-cursor-config.mjs --write --include-mcpIf .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:
- Should Claude know this on every task, only for matching files, or only when a workflow is requested?
- Does the rule describe a current command and directory?
- 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:
- Read every
command,args, URL, and package name. - Replace literal credentials with environment-variable references.
- Confirm every contributor should receive this server.
- 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.
| Check | Passing result |
|---|---|
| Shared repository rules | Claude follows the commands and boundaries from AGENTS.md |
| Path rule | A 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 skill | Claude finds a narrowly described review skill only when relevant |
| MCP | Approved tools connect, while unconfigured servers remain unavailable |
| Completion | Claude 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
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。
Agent Cost Dashboard
Build a Claude Agent SDK cost dashboard in Next.js 16. Record per-run estimates, token usage, cache activity, and model mix without confusing estimates with billing.
エージェント型コマース:AI エージェントが支払えるアプリの作り方
2026年のエージェント型コマースをわかりやすく解説するガイド。x402、ACP、Machine Payments Protocol が何をするのか、そして AI エージェントが購入できる有料 API を週末で出荷するための手順を紹介します。