# Prompt & Rules Quality for Cursor, Windsurf, and Claude Code: The Complete Developer Guide

AI coding agents forget everything the moment your session ends. Every new chat, every fresh terminal, every reset context window starts from zero — no memory of your linting rules, your folder conventions, or the fact that you told it three times yesterday not to use default exports.

Rules files and hooks are how you fix that. They're not a bolt-on "prompt quality" product — they're native, built-in mechanisms in Cursor, Windsurf, and Claude Code that inject persistent context before the model ever sees your prompt, and (in Claude Code's case) can deterministically gate what the agent is allowed to do.

This guide covers the real, current mechanism in each tool, with working code, plus how to keep them in sync so you're not maintaining three divergent config files by hand.

* * *

## Why this matters

LLMs are stateless between completions. Without a rules mechanism, "use functional components," "no default exports," "TypeScript strict mode," and "never touch `.env`" all have to be retyped, every session, and the model still might not follow them consistently.

Rules files fix the _first_ problem: they inject standing context automatically. Hooks (Claude Code only, as of this writing) fix the _second_ problem: they give you deterministic enforcement that doesn't depend on the model remembering anything at all.

Keep that distinction in mind through the rest of this guide — **rules shape what the agent knows; hooks control what the agent is allowed to do.**

* * *

## Cursor: Project Rules (`.mdc`)

The old single-file `.cursorrules` is deprecated. The current system is **Project Rules** — multiple `.mdc` (Markdown + Config) files living in `.cursor/rules/`, each with its own activation logic.

### Rule types

| Type | Location | Scope | Shared via |
| --- | --- | --- | --- |
| Project Rules | .cursor/rules/*.mdc | Current repo | Git |
| User Rules | Cursor Settings → Rules | All your projects | Not shared |
| Team Rules | Cursor dashboard | All team members | Dashboard (paid plans) |
| AGENTS.md | Project root/subdirs | Current repo | Git |

### The `.mdc` format

Each file is YAML frontmatter + Markdown body:

```markdown
---
description: "TypeScript conventions for this project"
globs: "src/**/*.ts,src/**/*.tsx"
alwaysApply: false
---

# TypeScript Standards

- Use strict mode (`tsconfig` → `strict: true`)
- Prefer interfaces over type aliases for object shapes
- Use const objects instead of enums
- No default exports except for Next.js page components
- All function parameters must have explicit types
- Use early returns for error conditions
```

### Frontmatter fields and the four activation modes

| Field | Type | Effect |
| --- | --- | --- |
| description | string | Summary the agent uses to decide relevance in "Agent Requested" mode |
| globs | string \| string[] | File patterns that auto-attach this rule when a matching file is open |
| alwaysApply | boolean | If true, injected into every chat regardless of context |

Combining these gives you four practical modes:

```markdown
<!-- 1. Always — injected into every single conversation -->
---
description: "Core project context"
alwaysApply: true
---

<!-- 2. Auto-Attached — fires only when matching files are in context -->
---
description: "Django ORM and DRF patterns"
globs: "**/backend/**/*.py"
alwaysApply: false
---

<!-- 3. Agent Requested — model reads the description and decides -->
---
description: "How to write and run integration tests in this repo"
alwaysApply: false
---

<!-- 4. Manual — only injected when explicitly @-mentioned in chat -->
---
description: "One-off migration playbook, rarely needed"
alwaysApply: false
---
```

### A realistic rule set

```javascript
.cursor/rules/
├── project-context.mdc      # alwaysApply: true
├── typescript.mdc           # globs: src/**/*.{ts,tsx}
├── testing.mdc              # globs: **/*.test.ts
├── api-error-format.mdc     # description-based, agent decides
└── db-migration-local.mdc   # manual, gitignored, personal only
```

`.cursor/rules/testing.mdc`:

```markdown
---
description: "Testing conventions using Vitest"
globs: "**/*.test.ts,**/*.spec.ts"
alwaysApply: false
---

# Testing Standards

- Use Vitest, not Jest
- One `describe` block per exported function
- Mock external HTTP calls with `msw`, never with manual `fetch` stubs
- Every new API route needs at least one happy-path and one error-path test
```

### Practical rules

-   Keep each `.mdc` file under ~500 lines — a bloated file burns context budget on every request.
    
-   Name files in kebab-case by concern (`error-responses.mdc`, not `rules2.mdc`).
    
-   Disable a rule without deleting it: set `alwaysApply: false` and strip the `globs`/`description`.
    
-   For personal, non-shared overrides, use a `*-local.mdc` naming convention and add `.cursor/rules/*-local.mdc` to `.gitignore`.
    
-   `AGENTS.md` is the simpler fallback for small projects that don't need glob-level scoping — and it doubles as a cross-tool format (see "Keeping rules in sync across tools" below).
    

* * *

## Windsurf: Rules & Memories (Cascade)

Windsurf's agent, Cascade, splits persistent context into two separate systems that people often conflate:

-   **Memories** — auto-generated by Cascade during a session, or created on request ("remember this"). Workspace-scoped, free (no credit cost), not shared across projects.
    
-   **Rules** — explicitly authored by you, always active, applied globally or per-workspace.
    

### Rule storage locations

| Location | Scope |
| --- | --- |
| ~/.codeium/windsurf/global_rules.md (or via Settings) | Every workspace |
| .windsurf/rules/*.md in the project root | Current workspace |
| .windsurf/rules/ in a subdirectory | That subdirectory only |
| .windsurfrules (legacy single file) | Current workspace, still supported |

### Activation modes

| Mode | Behavior |
| --- | --- |
| Always On | Injected into every Cascade interaction |
| Manual | Only applied when @mentioned in the Cascade input |
| Model Decision | Cascade reads a natural-language description and decides relevance itself |
| Glob | Applied when files matching a pattern (e.g. src/**/*.ts) are in context |

### Example: `global_rules.md`

```markdown
# Global Rules

- Server Components by default in Next.js; Client Components only when
  the file needs interactivity, state, or browser-only APIs
- Never suggest `any` in TypeScript — use `unknown` and narrow it
- Prefer named exports over default exports
```

### Example: workspace rule, glob-scoped

```markdown
---
trigger: glob
globs: "*.py"
---

# Python Backend Rules

- Follow PEP 8; format with `ruff format`
- All public functions require type hints and a docstring
- Use `pydantic` models for request/response validation, not raw dicts
```

### XML-tag style (Windsurf's alternate, model-friendly format)

Windsurf documentation also supports wrapping rules in XML-style tags, which some teams find easier for the model to parse and cite back:

```markdown
<coding_guidelines>
- My project's programming language is Python
- Use early returns when possible
- Always add docstrings when creating new functions and classes
</coding_guidelines>
```

### Practical rules

-   Each rule file has a hard character cap (in the low five figures — Windsurf truncates content beyond it), so split large standards into multiple focused files rather than one giant `global_rules.md`.
    
-   Add `.windsurfrules` (the legacy single-file format) to `.gitignore` if it holds personal preferences rather than team conventions.
    
-   Don't put project-specific facts ("we migrated the auth service last sprint") in Rules — that's what auto-generated Memories are for. Rules should hold durable _policy_, not session trivia.
    
-   Review Cascade's auto-generated memories periodically via the Memories panel; stale ones actively mislead the agent.
    

* * *

## Claude Code: `CLAUDE.md` + Hooks

Claude Code splits the same problem into two genuinely different tools, and this is the part most guides gloss over: `CLAUDE.md` **is context, hooks are control.** `CLAUDE.md` can be ignored by a distracted model. A hook cannot.

### `CLAUDE.md` hierarchy

Claude Code walks up the directory tree and merges multiple `CLAUDE.md` files, broadest to most specific:

| Level | Location | Scope |
| --- | --- | --- |
| Managed policy | /Library/Application Support/ClaudeCode/CLAUDE.md (macOS) / /etc/claude-code/CLAUDE.md (Linux) | Org-wide, enterprise-managed |
| User | ~/.claude/CLAUDE.md | Every project, every session |
| Project | CLAUDE.md at repo root | This repo, committed to git |
| Subdirectory | &lt;dir&gt;/CLAUDE.md | Loaded on demand when Claude works in that folder |

More specific files layer on top of (don't fully replace) broader ones — a `frontend/CLAUDE.md` and a `backend/CLAUDE.md` can carry completely different conventions in a monorepo without bloating the root file.

### Example `CLAUDE.md`

```markdown
# Project: Acme Billing Service

## Stack
- Node 20, TypeScript, Fastify, Postgres via Prisma
- Tests: Vitest. Run with `npm test`, not `npm run test:watch` in CI contexts.

## Conventions
- All monetary values are integer cents, never floats
- Every new endpoint needs a corresponding OpenAPI entry in `openapi.yaml`
- Do not add new npm dependencies without calling it out explicitly in your response

## Commands
- `npm run dev` — start local server on :3000
- `npm run db:migrate` — apply pending Prisma migrations
- `npm run lint:fix` — run before considering any task done

@docs/architecture.md
@~/.claude/personal-style.md
```

### The `@import` syntax

`CLAUDE.md` files can pull in other files with `@path/to/file`:

-   Supports relative paths, absolute paths, and home-directory paths (`@~/.claude/...`)
    
-   Imports can themselves import other files, up to 5 levels deep
    
-   `@` references are **not** evaluated inside code spans or fenced code blocks — so documenting the syntax (as above) doesn't accidentally trigger a real import
    

Use this to split a sprawling root file into composable pieces (`@docs/architecture.md`, `@docs/testing-strategy.md`) instead of one 2,000-line wall of text.

### Hooks: deterministic quality gates

`CLAUDE.md` is a request, not a guarantee — the model can still skip a step under time pressure inside a long agentic run. **Hooks** are shell commands Claude Code fires automatically at fixed points in its execution loop, regardless of what the model "remembers."

**Where hooks live:**

```python
~/.claude/settings.json          # global, all projects
.claude/settings.json            # project-level, committable
.claude/settings.local.json      # project-level, gitignored
<managed policy path>            # org-wide, admin-controlled
```

**Key lifecycle events:**

| Event | Fires | Can block the action? |
| --- | --- | --- |
| PreToolUse | Before a tool call executes | ✅ Yes (exit code 2) |
| PostToolUse | After a tool call succeeds | ❌ No — but can trigger follow-up (format, test, notify) |
| UserPromptSubmit | When you submit a prompt, before Claude processes it | ✅ Yes |
| Stop | When Claude finishes responding | — |
| SessionStart | New session begins | — |
| PreCompact | Before context gets compacted | — |
| Notification | Claude sends a notification | — |

**Basic shape**, in `.claude/settings.json`:

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}
```

Every hook receives structured JSON on stdin describing the event (`tool_name`, `tool_input`, etc.), and controls what happens next via its exit code:

-   `exit 0` → continue normally
    
-   `exit 2` (on `PreToolUse`) → block the tool call; stderr is shown to the model as the reason
    
-   Non-zero on `PostToolUse` → surfaced as feedback, but the action already happened
    

#### Example: block edits to `.env` and force-pushes

```bash
#!/bin/bash
# .claude/hooks/block-dangerous.sh
INPUT=$(cat)
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input | tojson')

if echo "$TOOL_INPUT" | jq -r '.file_path // empty' | grep -q '\.env'; then
  echo "Blocked: .env files must be edited manually, not by the agent" >&2
  exit 2
fi

if echo "$TOOL_INPUT" | jq -r '.command // empty' | grep -q 'push.*--force'; then
  echo "Blocked: force push requires human review" >&2
  exit 2
fi

exit 0
```

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Edit|Write",
        "hooks": [
          { "type": "command", "command": "bash .claude/hooks/block-dangerous.sh" }
        ]
      }
    ]
  }
}
```

#### Example: auto-format and auto-test after every edit

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
        ]
      }
    ]
  }
}
```

This is the real difference from a rules file: writing "always run Prettier after editing" in `CLAUDE.md` is a _hope_. A `PostToolUse` hook is a _guarantee_ — it runs every time, independent of the model's attention.

Set an explicit `timeout` (in seconds) on any hook that shells out to something slow, like a full test suite — default timeouts are short and have changed across releases, so don't rely on the default for long-running commands.

* * *

## Side-by-side comparison

|   | Cursor | Windsurf | Claude Code |
| --- | --- | --- | --- |
| Context file(s) | .cursor/rules/*.mdc | .windsurf/rules/*.md, global_rules.md | CLAUDE.md (hierarchical) |
| Legacy single-file format | .cursorrules (deprecated) | .windsurfrules (still supported) | — |
| Granular scoping | Per-file, via globs | Per-file, via glob trigger | Per-directory, via nested files |
| Model decides relevance? | Yes (description, no alwaysApply) | Yes ("Model Decision" mode) | No — loaded by directory/hierarchy |
| Auto-generated memory | No | Yes (Cascade Memories) | No |
| Team-shared rules | Yes (paid dashboard) | Via committed .windsurf/rules/ | Via committed CLAUDE.md |
| Can block an action outright | No | No | Yes — PreToolUse hooks |
| Auto-run commands after edits | No | No | Yes — PostToolUse hooks |
| Cross-file import | No native syntax | No native syntax | @path imports, depth 5 |

The takeaway: Cursor and Windsurf give you rich, granular _context injection_. Claude Code gives you that too (via `CLAUDE.md`), plus a second, independent layer of _enforcement_ that doesn't exist in the other two as of this writing.

* * *

## One rule set, three formats — a worked example

Say you want one policy — "REST handlers must validate input with Zod, and every mutation needs a test" — enforced consistently. Here's the same rule expressed natively in each tool.

**Cursor** — `.cursor/rules/api-validation.mdc`:

```markdown
---
description: "Input validation and test coverage for REST handlers"
globs: "src/routes/**/*.ts"
alwaysApply: false
---
# API Validation

- Every route handler must validate `req.body` with a Zod schema before use
- Every handler that mutates state needs a corresponding test in `*.test.ts`
```

**Windsurf** — `.windsurf/rules/api-validation.md`:

```markdown
---
trigger: glob
globs: "src/routes/**/*.ts"
---
# API Validation

- Every route handler must validate `req.body` with a Zod schema before use
- Every handler that mutates state needs a corresponding test in `*.test.ts`
```

**Claude Code** — a section inside `src/routes/CLAUDE.md`:

```markdown
## API Validation

- Every route handler must validate `req.body` with a Zod schema before use
- Every handler that mutates state needs a corresponding test in `*.test.ts`
```

...and optionally backed by a **hook** that actually checks it, rather than trusting the model read the section:

```bash
#!/bin/bash
# .claude/hooks/check-zod-validation.sh — PostToolUse on route file edits
FILE=$(cat | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" == src/routes/* ]] && ! grep -q "z\.object\|zodSchema" "$FILE"; then
  echo "Warning: $FILE has no visible Zod validation call" >&2
fi
exit 0
```

Notice the content is identical — only the wrapper (frontmatter keys, file location) differs. That's exactly the problem the next section solves.

* * *

## Keeping rules in sync across tools

Maintaining near-duplicate content in `.cursor/rules/`, `.windsurf/rules/`, and `CLAUDE.md` by hand drifts fast. A small but real open-source ecosystem exists specifically to solve this — write once, generate the rest.

### Ruler (`@intellectronica/ruler`)

The most established of these. One `.ruler/` directory is the source of truth; `ruler apply` distributes it to every supported agent's native format (Claude Code, Cursor, Windsurf, Copilot, Codex CLI, Cline, Aider, and others).

```bash
npm install -g @intellectronica/ruler
cd your-project
ruler init
```

`ruler init` scaffolds:

```python
.ruler/
├── AGENTS.md      # your canonical rules, in plain Markdown
├── ruler.toml     # which agents to target, output paths
└── mcp.json       # optional shared MCP server config
```

Split large rule sets into focused files — they're concatenated alphabetically:

```
.ruler/
├── AGENTS.md
├── coding_style.md
├── api_conventions.md
└── security_guidelines.md
```

Apply to every configured agent, or target specific ones:

```bash
ruler apply                          # all configured agents
ruler apply --agents cursor,claude   # only these two
ruler revert                         # undo, restores from .bak files
ruler revert --dry-run               # preview the undo first
```

`ruler.toml` controls which agents get generated and where — check `ruler --help` for the exact keys in the version you install, since the CLI evolves, but the shape is roughly:

```toml
default_agents = ["claude", "cursor", "windsurf", "copilot"]

[agents.cursor]
enabled = true

[agents.windsurf]
enabled = true
```

Each generated file gets a source marker for traceability, e.g.:

```xml
<!-- Source: .ruler/api_conventions.md -->
```

...so if Cursor's rule looks wrong, you know exactly which canonical file to fix.

### Lighter alternatives

If you don't want a full tool, a pre-commit hook that regenerates target files from one canonical `AGENTS.md` accomplishes the same thing with a few lines:

```bash
# .husky/pre-commit
npx agentsync sync   # example: regenerates CLAUDE.md, .cursorrules, etc. from AGENTS.md
git add -A
```

The specific tool matters less than the principle: **pick one canonical file, generate the rest, and never hand-edit a generated file.**

* * *

## Hooks as a real quality _gate_

Since Claude Code is the only one of the three with an enforcement layer today, it's worth showing a fuller pattern: a "quality gate" that runs lint, type-check, and tests after every batch of edits, and reports failures back to the model so it can self-correct — without you doing anything.

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/quality-gate.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}
```

```bash
#!/bin/bash
# .claude/hooks/quality-gate.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

[[ "$FILE" != *.ts && "$FILE" != *.tsx ]] && exit 0

npx eslint "$FILE" --quiet 2>&1 | tee /tmp/lint-out.txt
if [ -s /tmp/lint-out.txt ]; then
  echo "ESLint found issues in $FILE — fix before continuing:" >&2
  cat /tmp/lint-out.txt >&2
  exit 1   # PostToolUse can't block, but this surfaces feedback to the model
fi

npx tsc --noEmit -p . 2>&1 | tee /tmp/tsc-out.txt
if [ -s /tmp/tsc-out.txt ]; then
  echo "Type errors present:" >&2
  cat /tmp/tsc-out.txt >&2
  exit 1
fi

exit 0
```

Because this is a `PostToolUse` hook it can't undo the edit — but a non-zero exit surfaces the lint/type output directly to the model as feedback, so in an agentic loop it typically self-corrects on the next turn instead of you catching it in review three files later.

For anything that should be **prevented outright** rather than corrected after the fact (touching production config, running `rm -rf`, editing CI secrets), use `PreToolUse` with `exit 2` instead — that's the only exit code across either event that actually stops the action.

* * *

## Common mistakes

-   **Treating** `CLAUDE.md` **as an enforcement mechanism.** It's context. If something must never happen, it needs a `PreToolUse` hook, not a bullet point.
    
-   **One giant rules file.** All three tools reward focused, single-topic files over a 2,000-line monolith — it wastes context budget and the model deprioritizes buried instructions.
    
-   **Forgetting** `alwaysApply`**/**`trigger` **semantics.** A Cursor rule with no `globs` and `alwaysApply: false` and a vague description may simply never fire. Test that your rule actually attaches.
    
-   **Committing personal preferences.** "I prefer terse commit messages" belongs in Cursor's User Rules or a gitignored local file — not in a team's shared `.cursor/rules/`.
    
-   **No** `timeout` **on slow hooks.** A hook that shells out to a full test suite without an explicit timeout can stall the agent loop.
    
-   **Hand-editing generated files.** If you adopt Ruler or a similar sync tool, edit the canonical source only — hand edits get silently overwritten on the next `apply`.
    

* * *

## Cheat sheet

```bash
Cursor
  .cursor/rules/*.mdc        → frontmatter: description, globs, alwaysApply
  .cursor/rules/*-local.mdc  → gitignore for personal overrides

Windsurf
  global_rules.md            → all workspaces
  .windsurf/rules/*.md       → this workspace (or subdirectory)
  .windsurfrules              → legacy single-file, still works

Claude Code
  ~/.claude/CLAUDE.md         → user, all projects
  ./CLAUDE.md                 → project root
  ./**/CLAUDE.md              → subdirectory, loaded on demand
  @path/to/file.md            → import (max depth 5)
  ~/.claude/settings.json     → global hooks
  .claude/settings.json       → project hooks (commit this)
  .claude/settings.local.json → project hooks (gitignore this)

Sync tool
  npm install -g @intellectronica/ruler
  ruler init && ruler apply
```

* * *

## FAQ

**Is** `.cursorrules` **still supported?** Yes, but it's deprecated — Cursor still reads it, but new projects should use `.cursor/rules/*.mdc` for glob scoping and per-rule activation modes.

**Does Windsurf have anything like Claude Code's hooks?** Not as of this writing. Windsurf's Rules and Memories shape context; they don't gate or block tool calls the way a Claude Code `PreToolUse` hook can.

**Can a Cursor or Windsurf rule stop the agent from running a command?** No — both are context-injection systems. Neither has a deterministic blocking mechanism equivalent to Claude Code's `PreToolUse` + `exit 2`.

**What's the difference between Windsurf Memories and Rules?** Memories are auto-generated (or on-request) facts scoped to a workspace, free to create, meant for session-specific context. Rules are explicitly authored, always-active policy. Don't put durable team conventions in Memories — they're not guaranteed to persist the way a committed rules file is.

**Do I need a separate tool to keep these in sync?** Not strictly — you can hand-maintain three files for a small project. Past a handful of rules, drift becomes real; a sync tool like Ruler (or a simple pre-commit script) pays for itself quickly.

**Where do MCP server configs fit into this?** Outside the scope of this guide, but the same tools generally handle it: Cursor uses `.cursor/mcp.json`, Claude Code uses `.mcp.json`, and Ruler can distribute a shared `mcp.json` alongside your rules.

---

*Originally published on [ZyVOP](https://zyvop.com/prompt-rules-quality-for-cursor-windsurf-and-claude-code-the-complete-developer-guide-rk7u5)*

💡 For more articles like this, [subscribe to the ZyVOP newsletter](https://zyvop.com/newsletter)!
