CLAUDE.md vs System Prompt: What Actually Controls Claude Behavior
You set up a system prompt. You write a CLAUDE.md file. They say different things.
Which one wins?
This is the most common confusion I see from developers using Claude Code. The system prompt and CLAUDE.md serve different purposes, operate at different levels, and have different enforcement characteristics. Understanding the difference is the key to getting consistent, predictable behavior from Claude.
What is a system prompt?
A system prompt is the instruction block passed to Claude via the API at the start of every conversation. It's ephemeral — it exists only for that API call.
response = client.messages.create(
model="claude-sonnet-4-20250514",
system="You are a helpful coding assistant. Always use TypeScript.",
messages=[{"role": "user", "content": "Write a function to parse JSON"}]
)
The system prompt tells Claude how to behave in this conversation. It's powerful, but it has limits:
- It resets every session
- It's invisible to other tools in the workflow
- It can't enforce rules across files or projects
- It requires API-level access to modify
What is CLAUDE.md?
CLAUDE.md is a file that lives in your project directory. Claude Code reads it automatically at the start of every session. It acts as persistent, project-scoped instructions.
# CLAUDE.md
## Rules
- Use TypeScript strict mode for all files
- Never use `any` type — use `unknown` or explicit types
- All functions must have JSDoc comments
- Prefer named exports over default exports
## Architecture
- API routes go in `src/api/`
- Shared types go in `src/types/`
- Tests mirror source structure in `__tests__/`
CLAUDE.md is different from a system prompt in three key ways:
- It persists — it's a file in your repo, versioned with git
- It's project-scoped — rules apply to everyone working on the project
- It's hierarchical — you can have CLAUDE.md files at different directory levels
Which one wins when they conflict?
Here's what actually happens when CLAUDE.md and a system prompt disagree.
Short answer: CLAUDE.md instructions are treated as high-priority project context. They sit alongside the system prompt in Claude's context, but because they're specific and scoped to the project, Claude tends to follow them over generic system prompt instructions.
Let's see this in practice.
Example 1: Coding style conflict
System prompt:
Always use single quotes for strings.
CLAUDE.md:
## Style
- Use double quotes for all strings
- Enforce via ESLint: quotes: ["error", "double"]
Result: Claude follows CLAUDE.md and uses double quotes. The project-level rule with a concrete enforcement mechanism (ESLint config reference) carries more weight than a generic system instruction.
Example 2: Framework preference conflict
System prompt:
When building React components, use class components for stateful logic.
CLAUDE.md:
## Rules
- All React components must be functional components
- Use hooks for all state management
- Class components are banned — refactor any you find
Result: Claude follows CLAUDE.md. The word "banned" plus the refactoring instruction makes this an unambiguous project rule, not a preference.
Example 3: Testing approach conflict
System prompt:
Write unit tests using Jest with shallow rendering.
CLAUDE.md:
## Testing
- Use Vitest, not Jest
- Use React Testing Library with full rendering
- Test user behavior, not implementation details
- Never use shallow rendering
Result: Claude follows CLAUDE.md. The explicit tool choice (Vitest) and the direct "never use" instruction override the system prompt's suggestion.
CLAUDE.md enforcement patterns that actually work
After months of iteration, these are the patterns that produce consistent Claude behavior.
Pattern 1: Negative rules with alternatives
Don't just say what not to do — say what to do instead.
Weak:
- Don't use `var`
Strong:
- Never use `var` — use `const` by default, `let` only when reassignment is needed
Claude follows the strong version consistently because it has a clear decision path.
Pattern 2: Rules with enforcement hooks
Reference your actual tooling. This tells Claude the rule is enforced, not just preferred.
Before:
- Use consistent formatting
After:
- Format all code with Prettier (config in .prettierrc)
- Run `npm run lint` before considering any task complete
- ESLint strict mode is enabled — zero warnings allowed
When Claude sees that tooling enforces a rule, it treats the rule as non-negotiable.
Pattern 3: Architecture rules with file paths
Abstract rules get ignored. Concrete rules with paths get followed.
Before:
- Keep the codebase organized
After:
## File structure
- API routes: `src/routes/[resource].ts`
- Database models: `src/models/[Model].ts`
- Shared utilities: `src/utils/` (no subdirectories)
- Component tests: `src/components/__tests__/[Component].test.tsx`
Pattern 4: Scoped CLAUDE.md files
You can place CLAUDE.md files in subdirectories. Claude Code reads the nearest one to the file being edited.
project/
├── CLAUDE.md # Global rules
├── src/
│ ├── CLAUDE.md # Frontend-specific rules
│ └── components/
└── server/
├── CLAUDE.md # Backend-specific rules
└── routes/
This lets you enforce different conventions in different parts of the codebase without conflict.
Pattern 5: Boundary definitions
Tell Claude where to stop. This prevents scope creep in generated code.
## Boundaries
- Do NOT modify files in `src/legacy/` — migration in progress
- Do NOT add new dependencies without explicit approval
- Do NOT refactor code outside the scope of the current task
- Keep changes focused — one logical change per commit
Practical setup: combining both effectively
The best setup uses system prompts and CLAUDE.md for different things:
System prompt — session-level behavior:
You are a senior TypeScript developer. Be concise.
Ask clarifying questions before making large changes.
CLAUDE.md — project-level rules:
## Stack
- TypeScript 5.x, React 19, Next.js 15
- Prisma ORM with PostgreSQL
- Vitest + React Testing Library
## Rules
- Strict TypeScript — no `any`, no `as` assertions
- All API routes must validate input with Zod
- All components must have corresponding test files
- Use server components by default, client components only when needed
## Architecture
- App router: `src/app/`
- Shared components: `src/components/`
- Server actions: `src/actions/`
- Database schema: `prisma/schema.prisma`
The system prompt shapes how Claude talks to you. CLAUDE.md shapes what Claude builds.
Quick reference
| Feature | System Prompt | CLAUDE.md |
|---|---|---|
| Scope | Single conversation | Entire project |
| Persistence | Ephemeral | Git-versioned |
| Who controls it | API caller | Any team member |
| Best for | Behavior/tone | Code rules/architecture |
| Conflict resolution | Lower priority | Higher priority |
| Supports hierarchy | No | Yes (nested directories) |
Start with tested rules
Writing effective CLAUDE.md rules from scratch takes iteration. If you want a head start, I've published a CLAUDE.md Rules Pack with 50+ production-tested rules covering TypeScript, React, Next.js, Prisma, and testing conventions — organized by the enforcement patterns described above.
If you're also using Cursor alongside Claude Code, the Cursor Rules Pack v2 follows the same structure for .cursorrules files. Both packs are designed to work together so your AI tools stay consistent across your workflow.
The takeaway
System prompts and CLAUDE.md are not competing — they're complementary. Use system prompts for conversation-level behavior. Use CLAUDE.md for project-level enforcement. When they conflict, CLAUDE.md wins because it's specific, persistent, and scoped.
The developers getting the best results from Claude aren't writing longer prompts. They're writing better CLAUDE.md files.
Top comments (0)