System Prompts & Rules Files
Learning Outcomes
- Explain the difference between system prompts and user prompts
- Create effective CLAUDE.md and .cursorrules files for your projects
- Write persistent rules that improve AI output across every interaction
- Version and evolve rules files as your project matures
- Share rules across teams for consistent AI-assisted development
Lesson Plan
| Segment | Duration | Topic |
|---|---|---|
| Intro | 4 min | What are system prompts? |
| Explain | 8 min | System vs user prompts — the hierarchy |
| Demo | 10 min | Writing a CLAUDE.md file |
| Demo | 8 min | Creating .cursorrules |
| Explain | 7 min | Principles for effective persistent rules |
| Demo | 7 min | Versioning and evolving rules |
| Demo | 4 min | Team sharing patterns |
| Wrap-up | 2 min | Key takeaways |
Before You Begin
Pre-work:
- Complete Lesson 5 on Constraints & Guardrails
- Have a coding project (any language, any size)
- Familiarise yourself with your preferred AI tool's configuration format
Shopping List:
- An AI coding tool (Claude Code, Cursor, Codex, or similar)
- A project repository with at least a few files
- A text editor for creating configuration files
- Basic understanding of YAML/Markdown formatting
A system prompt is a set of instructions that persists across every interaction with your AI tool. Unlike a user prompt (which you type fresh each time), a system prompt operates in the background — shaping behaviour, enforcing conventions, and providing context automatically.
The hierarchy of prompts:
┌─────────────────────────────────┐
│ Tool's Built-in System Prompt │ ← Set by the AI tool vendor
├─────────────────────────────────┤
│ Your Project Rules File │ ← You control this
├─────────────────────────────────┤
│ Your User Prompt │ ← What you type each time
└─────────────────────────────────┘
Each level can influence, refine, or override the level above it — though the built-in system prompt typically has the strongest authority.
Why system prompts matter:
Without a system prompt, you repeat yourself constantly:
> Use TypeScript. Follow our naming conventions. Don't use any
> deprecated APIs. Return errors as Result types not exceptions...
With a system prompt, these instructions apply automatically to every interaction.
Where system prompts live by tool:
| Tool | File | Location |
|---|---|---|
| Claude Code | CLAUDE.md |
Project root |
| Cursor | .cursorrules |
Project root |
| GitHub Copilot | .github/copilot-instructions.md |
.github/ directory |
| Codex | AGENTS.md |
Project root |
| Windsurf | .windsurfrules |
Project root |
Let's build a rules file from scratch. We'll use generic principles that translate across tools.
Start with project identity:
# Project: TaskFlow API
A REST API for task management built with Node.js and Express.
Uses PostgreSQL for persistence and Redis for caching.
## Tech Stack
- Runtime: Node.js 20
- Framework: Express 4.x
- Database: PostgreSQL 15 with Knex query builder
- Cache: Redis 7
- Testing: Jest + Supertest
- Language: TypeScript 5.x (strict mode)
Add coding conventions:
## Coding Conventions
- Use named exports, not default exports
- Prefer async/await over .then() chains
- All functions must have explicit return types
- Use Result<T, E> pattern for operations that can fail
- Error messages must be user-friendly (no stack traces in responses)
- Database queries go in repository files, not controllers
Add architectural rules:
## Architecture
- Follow the controller → service → repository pattern
- Controllers handle HTTP concerns only (parsing, status codes)
- Services contain business logic
- Repositories handle data access
- Never import a repository directly into a controller
Add testing expectations:
## Testing
- Every new endpoint needs integration tests
- Use factory functions for test data (see tests/factories/)
- Mock external services, never the database
- Test file naming: [module].test.ts
Try it now: Create a rules file for your own project. Start with just three sections: project identity, tech stack, and your top 5 coding conventions.
Not all rules are created equal. Here's what makes the difference between rules that get followed and rules that get ignored.
Principle 1: Be specific, not aspirational
Bad:
- Write clean code
- Follow best practices
- Keep things simple
Good:
- Functions must be under 30 lines
- No more than 3 parameters per function (use an options object for more)
- Every public function needs a JSDoc comment with @param and @returns
Principle 2: Show, don't just tell
Bad:
- Use proper error handling
Good:
- Wrap external API calls in try/catch and return a Result type:
```typescript
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
try {
const response = await api.get(`/users/${id}`);
return Ok(response.data);
} catch (error) {
return Err(new ApiError('Failed to fetch user', error));
}
}
**Principle 3: Explain the "why" for non-obvious rules**
Bad:
```markdown
- Never use `any` type
Good:
- Never use `any` type (breaks type safety across the codebase;
use `unknown` and narrow with type guards instead)
Principle 4: Include anti-patterns explicitly
## Do NOT
- Don't use ORM lazy loading (causes N+1 queries in production)
- Don't throw errors for expected cases (user not found is not an error)
- Don't add comments that restate the code (// increment counter)
- Don't use barrel files (index.ts re-exports) — they break tree shaking
Principle 5: Reference existing code as examples
## Examples
- For a well-structured controller, see src/controllers/auth.controller.ts
- For test patterns, see tests/integration/users.test.ts
- For database migrations, see migrations/002_add_teams.ts
Your rules file is a living document. It should evolve as your project grows and as you learn what works.
Track your rules in version control:
Your rules file belongs in git, right alongside your code. This means:
- Changes to rules are reviewed in PRs just like code changes
- You can see when and why a rule was added (git blame)
- Different branches can experiment with different rules
- New team members get the rules automatically when they clone
When to add a new rule:
Add a rule when you notice yourself correcting the AI for the same thing more than twice:
Session 1: "No, don't use default exports here"
Session 2: "I said no default exports"
Session 3: → Add to rules file: "Use named exports, not default exports"
When to remove or relax a rule:
Remove rules that are:
- No longer relevant (deprecated library references)
- Too restrictive (blocking legitimate patterns)
- Already enforced by linting (no need for AI to double-check what ESLint catches)
Structuring rules for growth:
# CLAUDE.md
## Core Rules (always apply)
[Your most important, stable conventions]
## Current Sprint Context
[Temporary rules for the current work — remove after the sprint]
- We're migrating from Express to Fastify — all new routes use Fastify
- The payments module is being rewritten — don't modify legacy payment code
## Module-Specific Rules
### API Layer
[Rules for src/api/]
### Database Layer
[Rules for src/db/]
Using dated comments:
## Rules
- Use Zod for all input validation (added 2024-03, replaced Joi)
- All new API endpoints must include OpenAPI annotations (added 2024-06)
Rules files become even more powerful when shared across a team. Everyone's AI assistant produces consistent output.
Pattern 1: Shared project rules (committed to repo)
The simplest approach — one rules file in the repository root:
project/
├── CLAUDE.md ← Shared rules for Claude Code users
├── .cursorrules ← Shared rules for Cursor users
├── .github/
│ └── copilot-instructions.md ← Shared rules for Copilot users
└── src/
All team members get the same rules. Changes go through PR review.
Pattern 2: Layered rules (shared + personal)
Many tools support multiple rule files that layer on top of each other:
~/.claude/CLAUDE.md ← Your personal global preferences
project/CLAUDE.md ← Team project rules
project/src/api/CLAUDE.md ← Module-specific rules
Personal preferences go in your global config (not committed):
# ~/.claude/CLAUDE.md (personal, not in git)
- I prefer verbose variable names over abbreviations
- Always explain your reasoning before writing code
- I'm learning Rust — add helpful comments in Rust files
Pattern 3: Template starters for new projects
Create a template rules file your team copies into new projects:
# [Project Name]
## Overview
[One paragraph describing the project]
## Tech Stack
[List the core technologies]
## Conventions
[Fill in your team's standards]
## Testing
[Your testing expectations]
## Security
- Never log sensitive data (tokens, passwords, PII)
- Always validate input at the boundary
- Use parameterised queries (no string concatenation in SQL)
Pattern 4: Rules for specific workflows
Different rules for different types of work:
## When writing new features
- Start with types/interfaces before implementation
- Create tests alongside the implementation
- Add migration if schema changes are needed
## When fixing bugs
- Write a failing test first that reproduces the bug
- Fix the bug
- Verify the test passes
- Check for similar bugs nearby
## When refactoring
- Never change behaviour (tests must still pass with no modifications)
- Make atomic commits (one refactoring step per commit)
- If a refactor requires test changes, it's not a pure refactor
Getting team buy-in:
- Start with a minimal rules file (core conventions only)
- Let the team see it working for a sprint
- Collect suggestions in a shared document
- Review and add rules together
- Remove rules that cause friction
Questions & Answers
Key Takeaways
- System prompts persist — they shape every interaction without you repeating yourself
- Be specific — vague rules get ignored; concrete rules with examples get followed
- Show the "why" — non-obvious rules need context or they'll be overridden
- Version your rules — commit them to git, review changes in PRs
- Evolve continuously — add rules when patterns emerge, remove rules that cause friction
- Share with your team — consistent AI output comes from consistent rules
Next Steps: In Lesson 7 — Chain-of-Thought Prompting, you'll learn how to guide AI through complex reasoning by explicitly requesting step-by-step thinking.