Every AI coding session starts from scratch. The model has no memory of your last session, no knowledge of your team's conventions, no awareness of the architectural decisions you have spent months making. Without persistent context, you repeat yourself constantly: explaining your stack, re-stating your coding standards, re-describing your architectural boundaries every single time.
Project context files solve this. CLAUDE.md, .cursorrules, system prompts, and custom instruction files give AI tools persistent awareness of your codebase's identity — the information that should always be present without consuming your dynamic context budget. This topic covers how to design, write, and maintain these files for maximum effect.
The Role of Persistent Context Files in Token Optimization
Before diving into specific formats, it is important to understand what persistent context files are optimizing:
They replace repetitive, expensive preambles. Without a CLAUDE.md, developers commonly paste several paragraphs of project context at the start of every AI session. Over 50 sessions per day across a team of 10, that is potentially 500 repetitions of the same 500–1000 tokens — a significant ongoing cost.
They encode standing decisions so you do not re-litigate them. "Use async/await not Promises," "our validation library is Zod not Joi," "services are injected via NestJS DI." These decisions are made once, encoded in the context file, and applied permanently without consuming dynamic context budget.
They compress tribal knowledge. Every codebase has implicit knowledge that lives only in senior engineers' heads. Context files externalize this knowledge for both AI tools and new team members.
They prevent common AI errors before they happen. An AI that knows "never use raw SQL outside repositories" from CLAUDE.md will not suggest raw SQL even when processing files that appear to permit it.
The token optimization win is clear: one well-written 1,500-token CLAUDE.md replaces 500+ tokens of ad-hoc context per session, paying itself off after the third session.
Tip: Calculate your "context file ROI" — estimate how many tokens of preamble you currently write per session, multiply by your daily session count, then compare to the one-time cost of writing a CLAUDE.md. Most teams find their investment pays off within 48 hours of use.
Writing Effective CLAUDE.md Files
CLAUDE.md is read by Claude Code at the start of every session in a directory that contains it. It is the single most impactful context file you can write for Claude-based workflows.
Anatomy of an effective CLAUDE.md:
## Stack
- Runtime: Node.js 20 LTS, TypeScript 5.3 (strict mode)
- Framework: NestJS 10
- Database: PostgreSQL 15 via TypeORM 0.3
- Cache: Redis 7 via ioredis
- Testing: Jest 29 + Supertest for integration tests
- Validation: class-validator + class-transformer (on DTOs only)
- Logging: Pino with structured JSON output
## Architecture
This is a 3-tier NestJS application. Feature modules live in `src/modules/`.
Each module MUST have: `*.module.ts`, `*.controller.ts`, `*.service.ts`,
`*.repository.ts` (if it touches the DB), `dto/` directory, `entities/` directory.
Architectural rules (do not violate these):
- Controllers handle HTTP concerns only — no business logic
- Services contain all business logic — no direct DB access
- Repositories handle all DB access — TypeORM queries only here
- Never import across module boundaries except through module exports
- Config accessed only via NestJS ConfigService — never process.env directly
## Coding Standards
- Use async/await everywhere — never raw Promise chains or callbacks
- All public methods must have return type annotations
- Error handling: throw domain exceptions (src/common/exceptions/) not raw Error
- Never use `any` type — use `unknown` and narrow it explicitly
- Prefer `const` over `let` — never `var`
- String literals for IDs: always use UUID v4 via `crypto.randomUUID()`
## Testing Requirements
- Unit tests live next to source files: `*.spec.ts`
- Integration tests in `test/` directory: `*.e2e-spec.ts`
- Mock external services using Jest mocks — never hit real APIs in tests
- Test coverage minimum: 80% lines for services
- Describe blocks follow: describe('ServiceName', () => { describe('methodName', ...) })
## Common Patterns
### Creating a new module
1. Run: `nest g module modules/feature-name`
2. Run: `nest g controller modules/feature-name --no-spec`
3. Create service, repository, dto/ and entities/ manually
4. Register the module in AppModule
### Error handling pattern
```typescript
// Always use domain exceptions, never raw throw new Error()
import { ResourceNotFoundException } from '@/common/exceptions';
if (!user) {
throw new ResourceNotFoundException('User', id);
}
Database transactions
// Always use the transaction helper for multi-step writes
await this.db.transaction(async (manager) => {
await manager.save(Order, order);
await manager.save(OrderItem, items);
});
What NOT to Do
- Do not use Sequelize or Mongoose — we use TypeORM only
- Do not use express directly — everything goes through NestJS
- Do not write migrations by hand — use
npm run migration:generate - Do not bypass the repository pattern with entityManager.query()
- Do not add new npm dependencies without discussion — check existing packages first
Current Work in Progress
- Payment refund flow is being rebuilt (src/modules/payments/refunds/) — treat as unstable
- Migration to NestJS 11 is planned — do not create new dependencies on NestJS 10-specific APIs
Key Files for Orientation
src/app.module.ts— root module with all feature module registrationssrc/config/— all configuration schemas and validationsrc/common/— shared utilities, guards, interceptors, exceptionssrc/database/— TypeORM data source configuration and base entity
This CLAUDE.md costs roughly 600–700 tokens and delivers:
- Stack precision (prevents wrong library suggestions)
- Architectural guardrails (prevents pattern violations)
- Coding standards (prevents style inconsistencies)
- Testing requirements (produces consistent test structure)
- Active anti-patterns (prevents the most common mistakes)
**Tip:** Include a "What NOT to Do" section explicitly. AI models respond well to negative constraints, and this section prevents the most costly mistakes — where the AI generates technically correct code that violates a project-specific rule the model could not have inferred from the source alone.
---
## Writing .cursorrules for Cursor IDE
Cursor reads `.cursorrules` from your project root and applies it as a persistent system-level instruction for all AI interactions within that project. The format is free-form text, but effective rules files follow a specific structure.
**A production-grade .cursorrules example:**
You are an expert React developer working on a large TypeScript/React e-commerce platform.
IDENTITY
- Stack: React 18, TypeScript 5, Vite 5, Tailwind CSS 3, TanStack Query v5, Zustand 4
- Testing: Vitest + React Testing Library
- API client: auto-generated from OpenAPI spec in
src/api/generated/ - State management: TanStack Query for server state, Zustand for UI state
COMPONENT RULES
- All components are function components with TypeScript props interfaces
- Props interfaces are defined in the same file, above the component:
interface ComponentNameProps { ... } - Complex components go in
src/components/features/with their own directory - Simple UI primitives go in
src/components/ui/ - Every component file exports: the component (default or named), its props interface, and nothing else
- Never use class components
- Prefer named exports for components in
src/components/
STATE RULES
- Data fetching: ALWAYS use TanStack Query hooks, never useEffect + fetch
- Server mutations: ALWAYS use useMutation from TanStack Query
- UI state (modals, toggles): Zustand stores in
src/stores/ - Never put server state in Zustand — that is what TanStack Query is for
- Form state: React Hook Form only — no controlled component forms with useState
STYLING RULES
- Tailwind CSS only — no CSS modules, no inline styles, no styled-components
- Design tokens live in
tailwind.config.ts— use them, do not hardcode colors/sizes - Responsive design uses Tailwind's sm:/md:/lg: prefixes
- Dark mode uses the
dark:prefix — every component must support it
API RULES
- API calls go through generated hooks in
src/api/generated/— never write fetch/axios calls manually - If a generated hook does not exist, add the endpoint to
openapi.yamland regenerate - Never construct API URLs manually — always use the generated base URL from config
WHEN WRITING TESTS
- Test behavior, not implementation — test what the component renders, not which hooks it calls
- Mock API calls using msw (Mock Service Worker) — mocks are in
src/mocks/handlers/ - Always test: renders without crashing, happy path, primary error state
- Avoid testing every edge case in component tests — that belongs in unit tests of utility functions
ANTI-PATTERNS (never do these)
useEffectfor data fetching (use TanStack Query)- Direct
localStorageaccess (use the customuseStoragehook insrc/hooks/) - Prop drilling more than 2 levels deep (use Zustand or context)
anytype anywhere (use proper TypeScript)- Inline event handlers with complex logic (extract to named functions)
**Cursorrules for different personas:**
QA engineers working in Cursor can have their own `.cursorrules` optimized for their workflow:
You are helping a QA engineer write and review test plans, test cases, and automation scripts.
TESTING PHILOSOPHY
This team uses BDD-style tests with Gherkin scenarios.
Test automation uses Playwright for E2E and pytest for API testing.
WHEN WRITING PLAYWRIGHT TESTS
- Use Page Object Model — page objects live in
tests/pages/ - All selectors use data-testid attributes — NEVER use CSS classes or XPath
- Tests must be independent — never share state between tests
- Use Playwright's built-in fixtures — no beforeAll with side effects
WHEN WRITING TEST CASES (manual)
Format: Given/When/Then with clear preconditions and acceptance criteria.
Reference story IDs (e.g., PROJ-123) in test case titles.
Always include: happy path, validation errors, boundary values, concurrent user scenarios.
WHAT TO AVOID
- Brittle selectors that will break on UI changes
- Tests that depend on execution order
- Hard-coded test data — use factories or fixtures
**Tip:** Write persona-specific `.cursorrules` and keep them in version control at `.cursor/rules/` with names like `rules-engineer.md`, `rules-qa.md`. Team members copy the relevant file to `.cursorrules` when working in that role. This ensures everyone's AI assistance is tuned to their actual workflow, not just the most common use case.
---
## System Prompts and Custom Instructions in Other Tools
Each AI tool has its own mechanism for persistent context:
**GitHub Copilot — Custom Instructions (VS Code):**
Create `.github/copilot-instructions.md` in your repository root:
```markdown
## Project Overview
This is a Node.js microservices backend. Each service is in its own directory under `services/`.
## Coding Standards
- TypeScript with strict: true
- ESLint with Airbnb config (enforced in CI)
- Prettier for formatting (config in .prettierrc)
- All functions async — no synchronous blocking operations
## Dependencies
- ORM: Prisma (not TypeORM)
- HTTP: Fastify (not Express)
- Validation: Zod schemas co-located with route handlers
- Tests: Vitest, not Jest
## When suggesting code:
- Always include error handling
- Use Prisma's typed client — never raw SQL
- Use Zod schema validation at every API boundary
- Follow the existing patterns in nearby files — do not introduce new patterns
aider — .aider.conf.yml:
Aider supports a configuration file that sets persistent behavior:
model: claude-sonnet-4-5
map-tokens: 1024
auto-commits: false
read:
- CLAUDE.md # always include project context
- docs/architecture.md # include architectural overview
gitignore: true
ignore:
- "*.lock"
- "*.min.*"
- "dist/"
- "coverage/"
- "*.snap"
VS Code + Claude extension — workspace settings:
// .vscode/settings.json
{
"claude.systemPrompt": "You are helping develop a TypeScript NestJS API. Follow the conventions in CLAUDE.md. Always use async/await. Never use `any` type. Prefer the patterns you observe in existing files over introducing new patterns.",
"claude.contextFiles": [
"CLAUDE.md",
"docs/architecture.md"
]
}
Tip: Cross-reference your persistent context files with each other. If your CLAUDE.md mentions "see docs/architecture.md for module patterns," make sure that file exists and is accurate. AI tools that follow links and references will build deeper contextual understanding; broken references create confusion and are worse than silence.
Structuring Context Files for Different Team Personas
A context file written purely for engineers will be less useful for QA engineers and product managers using AI assistance in the same codebase. Consider maintaining layered context files:
Repo root CLAUDE.md — universal context:
Contains what everyone needs: project name, stack summary, repository structure, key commands to run tests and builds, glossary of domain terms.
docs/context-engineer.md — engineering-specific context:
Coding standards, architectural patterns, testing requirements, CI/CD details.
docs/context-qa.md — QA-specific context:
Test framework details, test data management, environment setup, known flaky areas, regression suite organization.
docs/context-pm.md — PM-specific context:
Domain model glossary, feature flag naming conventions, how to read the changelog, key metrics definitions, how features map to modules.
A minimal PM-focused context file looks like:
## Domain Glossary
- Order: a customer's purchase request with 1+ line items
- Fulfillment: the process of picking, packing, and shipping an order
- SKU: Stock Keeping Unit — our internal product variant identifier
- AOV: Average Order Value (key metric in analytics queries)
## Feature-to-Module Mapping
- Checkout flow → src/modules/checkout/
- Order management → src/modules/orders/
- Customer accounts → src/modules/customers/
- Inventory → src/modules/inventory/
- Promotions/discounts → src/modules/promotions/
## How to Interpret Code References
When an engineer mentions "the repository layer," they mean files ending in `*.repository.ts`.
When they mention "a migration," they mean database schema changes in `database/migrations/`.
When they say "a DTO," they mean a data validation class in a `dto/` directory.
## Metrics Definitions (for analytics questions)
- Conversion rate: completed_orders / sessions (sessions table in analytics DB)
- Cart abandonment: carts not converted within 24h
- LTV: total order value per customer over 12 months
Tip: Ask each team member persona to write one paragraph answering "What do I wish the AI knew about this project before I start working?" — their answers become the raw material for their persona-specific context file. This technique surfaces the implicit knowledge that context files are meant to preserve.
Maintaining Context Files: The Lifecycle
Context files that fall out of date become liabilities. A CLAUDE.md that says "use TypeORM" when the team migrated to Prisma last month will cause the AI to consistently suggest incorrect patterns.
Establishing a maintenance cadence:
<!-- Last reviewed: 2025-05-10 | Reviewer: Dat Hoang -->
<!-- Review schedule: first Monday of each month -->
<!-- Trigger immediate review if: stack version changes, architectural pattern changes,
new conventions adopted, module structure changes -->
Change-triggered updates:
Add a context file update step to your PR template:
<!-- .github/pull_request_template.md -->
## Checklist
- [ ] Tests added/updated
- [ ] Documentation updated
- [ ] **If this PR changes project conventions or adds new patterns: CLAUDE.md updated**
- [ ] **If this PR adds new architectural patterns: docs/architecture.md updated**
Automated staleness detection:
from pathlib import Path
import subprocess
from datetime import datetime, timedelta
CONTEXT_FILES = ['CLAUDE.md', 'docs/context-engineer.md', '.cursorrules']
STALENESS_THRESHOLD_DAYS = 30
for context_file in CONTEXT_FILES:
result = subprocess.run(
['git', 'log', '-1', '--format=%ci', context_file],
capture_output=True, text=True
)
if not result.stdout.strip():
print(f"WARNING: {context_file} has never been committed")
continue
last_modified = datetime.fromisoformat(result.stdout.strip().split(' ')[0])
age = datetime.now() - last_modified
if age > timedelta(days=STALENESS_THRESHOLD_DAYS):
print(f"WARNING: {context_file} is {age.days} days old — review for staleness")
else:
print(f"OK: {context_file} updated {age.days} days ago")
Tip: At the start of each sprint or iteration, spend 10 minutes reviewing your CLAUDE.md with the question: "Has anything we did last sprint made this file inaccurate?" This is a 10-minute investment per sprint that prevents hundreds of incorrect AI suggestions in the next sprint. Treat context file accuracy as a team health metric, not an individual responsibility.