开发技巧

Cursor 的项目规则放在 .cursor/rules/*.mdc

Cursor 的项目规则放在 .cursor/rules/.mdc。每条规则的生效时机由 frontmatter 里的 alwaysApply、globs 和 description 三个字段组合决定,写错了,规则就会“静默失效”。 这篇给一个 Flask 待办服务写一套规则(四种生效方式各一条),再写一个检查脚本 ruleslint.py。它按官方文档的规

BINBHO · 图文教程

Cursor 的项目规则放在 .cursor/rules/*.mdc。每条规则的生效时机由 frontmatter 里的 alwaysApply、globs 和 description 三个字段组合决定,写错了,规则就会“静默失效”。 这篇给一个 Flask 待办服务写一套规则(四种生效方式各一条),再写一个检查脚本 rules_lint.py。它按官方文档的规则类型表,列出每条规则的类型和触发条件,并检查这些问题:扩展名不是 .mdc、缺少 frontmatter、@文件 引用的文件不存在、globs 匹配不到任何文件、规则超过 500 行。

先看清成品(box 上真实运行)

  • 4 条规则被正确归类为 Always Apply、Apply to Specific Files、Apply Intelligently、Apply Manually 四种。
  • 查出 1 个 ERROR(.md 文件会被忽略)、1 个 WARN(引用了不存在的 app/routes.py)和 1 个 INFO。
  • 因为有 ERROR,脚本以退出码 1 结束,可以直接放进 CI。
  • pytest 3 项通过。

合规提醒:Cursor 会把代码上下文发送给模型服务商处理。涉及公司代码时,先确认团队是否允许使用,并按需要开启 Privacy Mode。规则、hooks 和 MCP 配置会提交进仓库,里面不要写任何密钥,密钥用环境变量或 ${env:NAME} 引用。用 AI 生成的代码,版权和许可证合规由你负责,合并前要人工审查。本文不列 Cursor 的价格,额度和可用模型以官网为准。

运行说明:规则在 Cursor 里的实际生效情况,需要在 Cursor 编辑器中打开对话才能观察。box 上只有 Cursor CLI,没有登录账号,所以没有观察 Agent 实际引用了哪些规则。本文的检查完全依据官方文档描述的规则。

做完之后你会得到什么?

成果 说明
.cursor/rules/*.mdc 四种类型的示例规则,外加一个故意放错扩展名的 old-notes.md
rules_lint.py 规则检查脚本
test_lint.py 测试规则类型表、示例仓库、缺少 frontmatter 的情况
app/__init__.py 一个最小的 Flask 应用,让 globs 有文件可以匹配

需要准备哪些环境?

Python 3.9 以上,pip install pytest。在 Cursor 里使用这些规则,只需要把 .cursor/rules 目录提交进仓库。

规则怎么生效(来自官方文档)

alwaysApply description globs 类型 何时生效
true — — Always Apply 每次对话都带上,globs 和 description 被忽略
false — 有 Apply to Specific Files 上下文里有匹配的文件时自动带上
false 有 无 Apply Intelligently Agent 读描述后决定是否引用
false 无 无 Apply Manually 只有在对话里 @规则名 时才生效

其他要点:

  • 项目规则必须用 .mdc 扩展名,.cursor/rules 下的普通 .md 文件会被忽略。
  • globs 可以写多个模式,用逗号分隔。
  • 官方建议每条规则不超过 500 行。
  • 项目根目录(以及子目录)的 AGENTS.md 也会被 Cursor 读取,适合写简单的说明。

文件结构

54-cursor-A-src/
├── .cursor/rules/
│   ├── project-basics.mdc        # Always Apply
│   ├── release-checklist.mdc     # Apply Intelligently
│   ├── sql-snippets.mdc          # Apply Manually
│   ├── old-notes.md              # 故意放错扩展名
│   └── backend/api-style.mdc     # Apply to Specific Files
├── app/__init__.py
├── rules_lint.py
└── test_lint.py

第 1 步:规则示例

# .cursor/rules/project-basics.mdc
---
alwaysApply: true
---
- 这是一个 Flask + SQLite 的待办事项服务,Python 3.11。
- 改代码后必须运行 `python -m pytest -q`,测试不过不要宣布完成。
- 不要新增依赖;确需新增时先说明理由。
# .cursor/rules/backend/api-style.mdc
---
description: 编写或修改 HTTP 接口时的返回格式和分页约定
globs: app/**/*.py, tests/**/*.py
alwaysApply: false
---
- 接口统一返回 {"code": 0, "data": ..., "message": ""},出错时 code 非 0。
- 分页参数用 page、page_size,page_size 上限 100。
- 新接口必须同时写测试。

@app/routes.py
# .cursor/rules/release-checklist.mdc
---
description: 准备发布版本、写更新日志、打 tag 时使用的检查清单
alwaysApply: false
---
1. 更新 CHANGELOG.md,按 Added / Fixed / Changed 分组。
2. 版本号遵循语义化版本。
3. 确认迁移脚本可以回滚。
# .cursor/rules/sql-snippets.mdc
---
alwaysApply: false
---
常用查询:按状态统计待办数量
SELECT status, COUNT(*) FROM todos GROUP BY status;

第 2 步:检查脚本

"""Cursor 项目规则检查器:按官方文档的规则类型表,判断 .cursor/rules 下每条规则会在什么时候生效,
并找出常见问题。规则类型(官方文档):
  alwaysApply: true                    → Always Apply(每次对话都带上,globs/description 被忽略)
  false + globs                        → Apply to Specific Files(上下文里有匹配文件时带上)
  false + description(无 globs)      → Apply Intelligently(Agent 根据描述决定是否引用)
  false + 两者都没有                   → Apply Manually(只有在对话里 @ 规则名时才会用)
另外:只有 .mdc 会被识别,.mdc 以外的文件会被忽略。"""
import pathlib, re, sys, fnmatch

MAX_LINES = 500     # 官方文档:Keep rules under 500 lines


def parse(text):
    m = re.match(r"^---\n(.*?)\n---\n?(.*)$", text, re.S)
    if not m:
        return None, text
    meta = {}
    for line in m.group(1).splitlines():
        if ":" in line:
            k, v = line.split(":", 1)
            meta[k.strip()] = v.strip().strip('"')
    return meta, m.group(2)


def classify(meta):
    always = meta.get("alwaysApply", "false").lower() == "true"
    if always:
        return "Always Apply"
    if meta.get("globs"):
        return "Apply to Specific Files"
    if meta.get("description"):
        return "Apply Intelligently"
    return "Apply Manually"


def glob_match(globs, path):
    pats = [g.strip() for g in globs.split(",") if g.strip()]
    return any(fnmatch.fnmatch(path, p) or fnmatch.fnmatch(path, p.replace("**/", "")) for p in pats)


def lint(root="."):
    root = pathlib.Path(root); rd = root / ".cursor" / "rules"
    rows, issues = [], []
    for f in sorted(rd.rglob("*")):
        if f.is_dir():
            continue
        rel = f.relative_to(root)
        if f.suffix != ".mdc":
            issues.append(("ERROR", rel, "不是 .mdc 文件,规则系统会忽略它"))
            continue
        text = f.read_text(encoding="utf-8")
        meta, body = parse(text)
        if meta is None:
            issues.append(("ERROR", rel, "缺少 --- 包起来的 frontmatter"))
            continue
        kind = classify(meta)
        rows.append((rel, kind, meta.get("globs", ""), meta.get("description", "")))
        if kind == "Always Apply" and (meta.get("globs") or meta.get("description")):
            issues.append(("WARN", rel, "alwaysApply: true 时 globs/description 会被忽略"))
        if kind == "Apply Manually":
            issues.append(("INFO", rel, "既没有 globs 也没有 description:只有 @ 提到时才生效,确认这是你想要的"))
        if kind == "Apply Intelligently" and len(meta["description"]) < 10:
            issues.append(("WARN", rel, "description 太短,Agent 很难判断何时该用"))
        if text.count("\n") > MAX_LINES:
            issues.append(("WARN", rel, f"超过 {MAX_LINES} 行,建议拆分"))
        for ref in re.findall(r"^@(\S+)", body, re.M):
            if not (root / ref).exists():
                issues.append(("WARN", rel, f"引用的文件 @{ref} 不存在"))
        if meta.get("globs"):
            hits = [p for p in root.rglob("*") if p.is_file() and ".cursor" not in p.parts
                    and glob_match(meta["globs"], str(p.relative_to(root)))]
            if not hits:
                issues.append(("WARN", rel, f"globs `{meta['globs']}` 目前没有匹配任何文件"))
    if (root / "AGENTS.md").exists():
        issues.append(("INFO", pathlib.Path("AGENTS.md"), "同时存在 AGENTS.md,它也会被 Cursor 读取,注意别和规则冲突"))
    return rows, issues


if __name__ == "__main__":
    rows, issues = lint(sys.argv[1] if len(sys.argv) > 1 else ".")
    print(f"{'规则':<45}{'类型':<26}触发条件")
    for rel, kind, g, d in rows:
        print(f"{str(rel):<45}{kind:<26}{g or d or ('每次对话' if kind == 'Always Apply' else '@' + rel.stem)}")
    print()
    for lvl, rel, msg in issues:
        print(f"[{lvl}] {rel}: {msg}")
    sys.exit(1 if any(l == "ERROR" for l, _, _ in issues) else 0)

第 3 步:测试

import pathlib
from rules_lint import classify, lint, parse


def test_classify_table():
    assert classify({"alwaysApply": "true", "globs": "x"}) == "Always Apply"
    assert classify({"alwaysApply": "false", "globs": "a/*.py"}) == "Apply to Specific Files"
    assert classify({"description": "用于发布"}) == "Apply Intelligently"
    assert classify({}) == "Apply Manually"


def test_repo_rules():
    rows, issues = lint(pathlib.Path(__file__).parent)
    kinds = {r[0].name: r[1] for r in rows}
    assert kinds == {"api-style.mdc": "Apply to Specific Files", "project-basics.mdc": "Always Apply",
                     "release-checklist.mdc": "Apply Intelligently", "sql-snippets.mdc": "Apply Manually"}
    msgs = [(l, r.name, m) for l, r, m in issues]
    assert any(l == "ERROR" and n == "old-notes.md" for l, n, _ in msgs)
    assert any("@app/routes.py" in m for _, _, m in msgs)


def test_missing_frontmatter(tmp_path):
    d = tmp_path / ".cursor/rules"; d.mkdir(parents=True); (d / "x.mdc").write_text("没有头")
    assert lint(tmp_path)[1][0][0] == "ERROR"

第 4 步:运行

$ python rules_lint.py
规则                                           类型                        触发条件
.cursor/rules/backend/api-style.mdc          Apply to Specific Files   app/**/*.py, tests/**/*.py
.cursor/rules/project-basics.mdc             Always Apply              每次对话
.cursor/rules/release-checklist.mdc          Apply Intelligently       准备发布版本、写更新日志、打 tag 时使用的检查清单
.cursor/rules/sql-snippets.mdc               Apply Manually            @sql-snippets

[WARN] .cursor/rules/backend/api-style.mdc: 引用的文件 @app/routes.py 不存在
[ERROR] .cursor/rules/old-notes.md: 不是 .mdc 文件,规则系统会忽略它
[INFO] .cursor/rules/sql-snippets.mdc: 既没有 globs 也没有 description:只有 @ 提到时才生效,确认这是你想要的
$ echo $?
1

$ python -m pytest -q
3 passed in 0.01s

检查点:把 old-notes.md 改名为 .mdc 并补上 frontmatter,把 @app/routes.py 改成实际存在的文件,脚本就会以退出码 0 结束。

调试示例

app/**/*.py 没有匹配到 app/__init__.py。 检查 globs 是否匹配得到文件时,我用的是 Python 的 fnmatch。实测 fnmatch('app/__init__.py', 'app/**/*.py') 返回 False,因为 **/ 至少要对应一层目录;而 fnmatch('app/__init__.py', 'app/*.py') 返回 True。按常见的 glob 语义(Cursor 文档里 src/** 表示 src/ 下任意深度的文件),**/ 也应该能匹配零层目录。所以 glob_match() 会额外试一次去掉 **/ 的写法,避免把能生效的规则误报为“没有匹配任何文件”。

常见问题

现象 原因 处理
规则从来没有生效 扩展名是 .md,或者没写 frontmatter 改成 .mdc 并补上 --- 包起来的头部
写了 globs 却每次都生效 同时设了 alwaysApply: true 改成 false
Agent 不引用 Apply Intelligently 规则 description 太模糊 写清楚“什么场景下使用”
规则互相矛盾 AGENTS.md、用户规则、项目规则写法不一致 统一放到项目规则里管理

模板规则

  • 每条规则只讲一件事,控制在 500 行以内。
  • 规则类型由三个字段决定,用检查脚本在 CI 里把关。
  • 规则里引用的文件必须真实存在。

项目资源

用途 地址
Cursor 文档索引(llms.txt) https://cursor.com/llms.txt
Cursor Rules 文档 https://cursor.com/docs/rules

规则写对了,Agent 每次都能按团队约定工作,不用在对话里反复叮嘱。