Based on Claude Code Mastery Guides V3-V5 by TheDecipherist https://github.com/TheDecipherist/claude-code-mastery
| Command | What it does |
|---|---|
pnpm dev |
Start dev server with hot reload |
pnpm dev:website |
Dev server on port 3000 |
pnpm dev:api |
Dev server on port 3001 |
pnpm dev:dashboard |
Dev server on port 3002 |
pnpm build |
Type-check + compile TypeScript |
pnpm start |
Run compiled production build |
pnpm typecheck |
TypeScript type-check only (no emit) |
| Testing | |
pnpm test |
Run ALL tests (unit + E2E) |
pnpm test:unit |
Run unit/integration tests (Vitest) |
pnpm test:unit:watch |
Unit tests in watch mode |
pnpm test:coverage |
Unit tests with coverage report |
pnpm test:e2e |
Run E2E tests (kills test ports first, spawns servers on 4000/4010) |
pnpm test:e2e:ui |
E2E with Playwright UI mode |
pnpm test:e2e:headed |
E2E with visible browser |
pnpm test:e2e:chromium |
E2E on Chromium only (fast) |
pnpm test:e2e:report |
Open last E2E test report |
pnpm test:kill-ports |
Kill anything on test ports (4000, 4010, 4020) |
| Database | |
pnpm db:query <name> |
Run a dev/test database query |
pnpm db:query:list |
List all registered database queries |
| Content | |
pnpm content:build |
Build all published markdown → HTML |
pnpm content:build:id <id> |
Build a single article by ID |
pnpm content:list |
List all articles and their status |
| Docker | |
pnpm docker:optimize |
Audit Dockerfile against 12 best practices (use /optimize-docker in Claude) |
| Setup | |
/install-global |
Install/merge global Claude config into ~/.claude/ (one-time, never overwrites) |
/setup |
Interactive .env configuration — GitHub, database, Docker, analytics, RuleCatch |
/setup --reset |
Re-configure everything from scratch |
| RuleCatch | |
pnpm ai:monitor |
Live view of AI activity — tokens, cost, violations, tool usage (separate terminal) |
/what-is-my-ai-doing |
Same as above — launches AI-Pooler monitor |
| Git | |
/worktree <name> |
Create isolated branch + worktree for a task (never touch main) |
| Code Quality | |
/refactor <file> |
Audit + refactor a file against all CLAUDE.md rules (split, type, extract, clean) |
| API | |
/create-api <resource> |
Scaffold a full API endpoint — route, handler, types, tests — wired into the server |
| Documentation | |
/diagram <type> |
Generate diagrams from actual code: architecture, api, database, infrastructure, all |
| Utility | |
pnpm clean |
Remove dist/, coverage/, test-results/, playwright-report/ |
- NEVER commit passwords, API keys, tokens, or secrets to git/npm/docker
- NEVER commit
.envfiles — ALWAYS verify.envis in.gitignore - Before ANY commit: verify no secrets are included
- NEVER output secrets in suggestions, logs, or responses
- ALWAYS use TypeScript for new files (strict mode)
- NEVER use
anyunless absolutely necessary and documented why - When editing JavaScript files, convert to TypeScript first
- Types are specs — they tell you what functions accept and return
CORRECT: /api/v1/users
WRONG: /api/users
Every API endpoint MUST use /api/v1/ prefix. No exceptions.
ABSOLUTE RULE: ALL database access goes through src/core/db/index.ts. No exceptions.
- NEVER create
new MongoClient()anywhere else in the codebase - NEVER import
mongodbdirectly in any file exceptsrc/core/db/index.ts - NEVER use
mongooseor any ODM — native MongoDB driver only - ALWAYS import from
src/core/db/for all database operations - All query inputs are automatically sanitized against NoSQL injection (enabled by default)
- To disable sanitization: set
DB_SANITIZE_INPUTS=falsein.envorsanitize = falseinclaude-mastery-project.conf - Programmatic toggle:
configureSanitization(false)— only if you handle sanitization yourself
// CORRECT — import from the centralized wrapper
import { queryOne, queryMany, insertOne, updateOne, bulkOps, closePool } from '@/core/db/index.js';
// WRONG — NEVER do this
import { MongoClient } from 'mongodb'; // FORBIDDEN outside src/core/db/
const client = new MongoClient(uri); // FORBIDDEN — creates rogue connection// Single document lookup
const user = await queryOne<User>('users', { email });
// Multiple documents with pipeline
const recentOrders = await queryMany<Order>('orders', [
{ $match: { userId, status: 'active' } },
{ $sort: { createdAt: -1 } },
{ $limit: 20 },
]);
// Lookup/join — $limit is enforced BEFORE $lookup automatically
const userWithOrders = await queryWithLookup<UserWithOrders>('users', {
match: { _id: userId },
lookup: { from: 'orders', localField: '_id', foreignField: 'userId', as: 'orders' },
unwind: 'orders',
});
// Count
const total = await count('users', { role: 'admin' });// Insert
await insertOne('users', { email, name, createdAt: new Date() });
await insertMany('events', batchOfEvents);
// Update — use $inc for counters, $set for fields (NEVER read-modify-write)
await updateOne<User>('users', { _id: userId }, { $set: { name: 'New Name' } });
await updateOne<Stats>('stats', { date }, { $inc: { pageViews: 1, visitors: 1 } }, true); // upsert
// Complex batch operations (auto-retries E11000 concurrent upsert races)
await bulkOps('sessions', [
{ updateOne: { filter: { sessionId }, update: { $inc: { events: 1 } }, upsert: true } },
{ updateOne: { filter: { sessionId }, update: { $set: { lastSeen: new Date() } } } },
]);
// Delete
await deleteOne('tokens', { token: expiredToken });import { connect } from '@/core/db/index.js';
// High-traffic API service
await connect(undefined, { pool: 'high', label: 'API' }); // 20 max connections
// Standard service
await connect(undefined, { pool: 'standard', label: 'Web' }); // 10 max connections
// Low-traffic background job
await connect(undefined, { pool: 'low', label: 'Worker' }); // 5 max connectionsANY crash or termination signal MUST close MongoDB pools before exiting.
NEVER call process.exit() without closing pools first.
import { gracefulShutdown } from '@/core/db/index.js';
// Termination signals — clean exit
process.on('SIGTERM', () => gracefulShutdown(0));
process.on('SIGINT', () => gracefulShutdown(0));
// Crashes — close pools, then exit with error code
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
gracefulShutdown(1);
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
gracefulShutdown(1);
});gracefulShutdown() is idempotent — safe to call from multiple signals.
Register indexes alongside your queries, then call ensureIndexes() once at startup:
import { registerIndex, ensureIndexes } from '@/core/db/index.js';
// Register at module load time
registerIndex({ collection: 'users', fields: { email: 1 }, unique: true });
registerIndex({ collection: 'users', fields: { apiKey: 1 }, unique: true, sparse: true });
registerIndex({ collection: 'sessions', fields: { userId: 1, startedAt: -1 } });
registerIndex({ collection: 'tokens', fields: { expiresAt: 1 }, expireAfterSeconds: 0 }); // TTL
// Call once at app startup
await ensureIndexes(); // creates all registered indexes
await ensureIndexes({ dryRun: true }); // just logs what would be createdMongoDB skips indexes that already exist, so ensureIndexes() is safe to call every startup.
ABSOLUTE RULE: ALL ad-hoc / test / dev database queries go through the db-query system. No exceptions.
When a developer asks to "look something up in the database", "check a collection", "find a user", or any exploratory query:
- Create a query file in
scripts/queries/<descriptive-name>.ts - Register it in
scripts/db-query.tsquery registry - NEVER create standalone scripts, one-off files, or inline queries in
src/
// scripts/queries/find-expired-sessions.ts
import { queryMany } from '../../src/core/db/index.js';
export default {
name: 'find-expired-sessions',
description: 'Find sessions that expired in the last 24 hours',
async run(args: string[]): Promise<void> {
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
const sessions = await queryMany('sessions', [
{ $match: { expiresAt: { $lt: cutoff } } },
{ $sort: { expiresAt: -1 } },
{ $limit: 50 },
]);
console.log(`Found ${sessions.length} expired sessions:`);
console.log(JSON.stringify(sessions, null, 2));
},
};Then register in scripts/db-query.ts:
const queryRegistry = {
'find-expired-sessions': () => import('./queries/find-expired-sessions.js'),
};Run: npx tsx scripts/db-query.ts find-expired-sessions
Why this matters:
- One place for all test queries — no random scripts scattered across the project
- Clear separation —
scripts/queries/= dev/test,src/= production only - Every query uses the wrapper — enforces the same connection pool and patterns
- Easy cleanup — when done exploring, delete the query file and its registry entry
- Discoverable —
npx tsx scripts/db-query.ts --listshows all available queries
FORBIDDEN patterns:
// NEVER do this — creates rogue query files outside the system
// scripts/check-users.ts ← WRONG
// src/utils/debug-query.ts ← WRONG
// src/handlers/temp-lookup.ts ← WRONG
// ALWAYS do this — use the db-query system
// scripts/queries/check-users.ts + register in db-query.ts ← CORRECTWhy this matters (overall Rule 3):
- One pool — prevents connection exhaustion (the #1 Claude Code database failure)
- One place to change — swap databases without touching business logic
- One place to mock — testing becomes trivial
- Aggregation only — consistent, flexible, supports joins
- BulkWrite only — atomic, better performance than individual operations
- $limit before $lookup — prevents joining entire collections (massive perf hit)
- One place for test queries — no scripts scattered across the project
- ALWAYS define explicit success criteria for E2E tests
- "Page loads" is NOT a success criterion
- Every test MUST verify: URL, visible elements, data displayed
- NEVER write tests without assertions
- Use
/create-e2e <feature>to create E2E tests with proper structure
// CORRECT — explicit success criteria (MINIMUM 3 assertions per test)
await expect(page).toHaveURL('/dashboard'); // 1. URL
await expect(page.locator('h1')).toContainText('Welcome'); // 2. Element visible
await expect(page.locator('[data-testid="user"]')).toContainText('test@example.com'); // 3. Data correct
// WRONG — passes even if broken
await page.goto('/dashboard');
// no assertion!A test is NOT finished until it has:
- At least one URL assertion (
toHaveURL) - At least one element visibility assertion (
toBeVisible) - At least one content/data assertion (
toContainText,toHaveValue) - Error case coverage (what happens when it fails?)
E2E test execution — ALWAYS kills test ports first:
pnpm test:e2e # kills ports 4000/4010/4020 → spawns servers → runs Playwright
pnpm test:e2e:headed # same but with visible browser
pnpm test:e2e:ui # same but with Playwright UI modeE2E tests run on TEST ports (4000, 4010, 4020) — never dev ports.
playwright.config.ts spawns servers automatically via webServer.
- ALWAYS use environment variables for secrets
- NEVER put API keys, passwords, or tokens directly in code
- NEVER hardcode connection strings — use DATABASE_URL from .env
- NEVER auto-deploy, even if the fix seems simple
- NEVER assume approval — wait for explicit "yes, deploy"
- ALWAYS ask before deploying to production
- No file > 300 lines (split if larger)
- No function > 50 lines (extract helper functions)
- All tests must pass before committing
- TypeScript must compile with no errors (
tsc --noEmit)
- When multiple
awaitcalls are independent (none depends on another's result), ALWAYS usePromise.all - NEVER await independent operations sequentially — it wastes time
- Before writing sequential awaits, evaluate: does the second call need the first call's result?
// CORRECT — independent operations run in parallel
const [users, products, orders] = await Promise.all([
getUsers(),
getProducts(),
getOrders(),
]);
// WRONG — sequential when they don't depend on each other
const users = await getUsers();
const products = await getProducts(); // waits for users unnecessarily
const orders = await getOrders(); // waits for products unnecessarily// CORRECT — sequential when there IS a dependency
const user = await getUserById(id);
const orders = await getOrdersByUserId(user.id); // needs user.idAuto-branch is ON by default. Every command that modifies code will automatically create a feature branch when it detects you're on main. No manual step required.
- Commands auto-create branches like
refactor/<name>,test/<name>,feat/<name>,chore/<name> - You never work on main by accident — the commands handle it
- Use
/worktree <branch-name>when you want a separate directory (parallel sessions) - If Claude screws up on a feature branch, delete it — main is untouched
# What happens automatically:
# 1. You run /refactor src/handlers/users.ts while on main
# 2. Command creates branch: git checkout -b refactor/users
# 3. Refactor happens on the new branch
# 4. Main is never touched
# For parallel sessions (separate directories):
/worktree add-auth # creates branch + separate working directory
# To disable auto-branching:
# Set auto_branch = false in claude-mastery-project.confBefore merging any branch back to main:
- Review the full diff:
git diff main...HEAD - Ask the user: "Do you want RuleCatch to check for violations on this branch?"
- Only merge after the user confirms
Why this matters:
- Main should always be deployable
- Feature branches are disposable — delete and start over if needed
git diff main...HEADshows exactly what changed, making review easy- Auto-branching means zero friction — you don't have to remember
- Worktrees let you run multiple Claude sessions in parallel without conflicts
- RuleCatch catches violations Claude missed — last line of defense before merge
Disabled by default. When enabled (docker_test_before_push = true in claude-mastery-project.conf), ANY docker push is BLOCKED until the image passes local verification:
- Build the image
- Run the container locally
- Wait 5 seconds for startup
- Verify container is still running (didn't crash/exit)
- Hit the health endpoint (must return 200)
- Check logs for fatal errors
- Clean up test container
- Only then allow
docker push
If any step fails: STOP, show what failed, and do NOT push.
# Enable in claude-mastery-project.conf:
docker_test_before_push = true
# Disable (default):
docker_test_before_push = falseThis gate applies globally — every command or workflow that pushes to Docker Hub must respect it.
Before jumping to conclusions:
- Missing UI element? → Check feature gates BEFORE assuming bug
- Empty data? → Check if services are running BEFORE assuming broken
- 404 error? → Check service separation BEFORE adding endpoint
- Auth failing? → Check which auth system BEFORE debugging
- Test failing? → Read the error message fully BEFORE changing code
If you're on Windows, you should be running VS Code in WSL 2 mode. Most people don't know this exists and it dramatically changes everything:
- HMR is 5-10x faster — file changes don't cross the Windows/Linux boundary
- Playwright tests run significantly faster — native Linux browser processes
- File watching actually works —
tsx watch,next dev,nodemonare all reliable - Node.js filesystem operations avoid the slow NTFS translation layer
- Claude Code runs faster — native Linux tools (
grep,find,git)
CRITICAL: Your project must be on the WSL filesystem (~/projects/), NOT on /mnt/c/. Having WSL but keeping your project on the Windows filesystem gives you the worst of both worlds.
# Check if you're set up correctly:
pwd
# GOOD: /home/you/projects/my-app
# BAD: /mnt/c/Users/you/projects/my-app ← still hitting Windows filesystem
# VS Code: click green "><" icon bottom-left → "Connect to WSL"Run /setup to auto-detect your environment and get specific instructions.
| Service | Dev Port | Test Port | URL |
|---|---|---|---|
| Website | 3000 | 4000 | http://localhost:{port} |
| API | 3001 | 4010 | http://localhost:{port} |
| Dashboard | 3002 | 4020 | http://localhost:{port} |
When starting any service, ALWAYS use its assigned port:
# CORRECT
npx next dev -p 3002
# WRONG — never let it default
npx next devBefore starting services, ALWAYS kill existing processes on those ports:
lsof -ti:3000,3001,3002 | xargs kill -9 2>/dev/nullproject/
├── CLAUDE.md # You are here
├── CLAUDE.local.md # Personal overrides (gitignored)
├── .claude/
│ ├── commands/ # Slash commands (/review, /refactor, /worktree, /new-project, etc.)
│ ├── skills/ # Triggered expertise & scaffolding templates
│ ├── agents/ # Custom subagents
│ └── hooks/ # Enforcement scripts (block-secrets, verify-no-secrets, rulecatch-check)
├── project-docs/
│ ├── ARCHITECTURE.md # System overview & data flow
│ ├── INFRASTRUCTURE.md # Deployment & environment details
│ └── DECISIONS.md # Why we chose X over Y
├── docs/ # GitHub Pages site
├── src/
│ ├── core/
│ │ └── db/ # Centralized database wrapper
│ ├── handlers/ # Business logic
│ ├── adapters/ # External service wrappers
│ └── types/ # Shared TypeScript types
├── tests/
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── scripts/
│ ├── db-query.ts # Test Query Master — index of all dev/test queries
│ ├── queries/ # Individual query files (dev/test only, NOT production)
│ ├── build-content.ts # Markdown → HTML article builder
│ └── content.config.json # Article registry (source, output, SEO metadata)
├── content/ # Markdown source files for articles/posts
├── .env.example # Template with placeholders (committed)
├── .env # Actual secrets (NEVER committed)
├── .gitignore
├── .dockerignore
├── package.json # All scripts: dev, test, db:query, content:build, ai:monitor
├── claude-mastery-project.conf # Profile presets for /new-project (clean, default, api, etc.)
├── playwright.config.ts # E2E test config (test ports 4000/4010/4020, webServer)
├── vitest.config.ts # Unit/integration test config
└── tsconfig.json
| Document | Purpose | When to Read |
|---|---|---|
project-docs/ARCHITECTURE.md |
System overview & data flow | Before architectural changes |
project-docs/INFRASTRUCTURE.md |
Deployment details | Before environment changes |
project-docs/DECISIONS.md |
Architectural decisions | Before proposing alternatives |
ALWAYS read relevant docs before making cross-service changes.
// CORRECT — explicit, typed
import { getUserById } from './handlers/users.js';
import type { User } from './types/index.js';
// WRONG — barrel imports that pull everything
import * as everything from './index.js';// CORRECT — handle errors explicitly
try {
const user = await getUserById(id);
if (!user) throw new NotFoundError('User not found');
return user;
} catch (err) {
logger.error('Failed to get user', { id, error: err });
throw err;
}
// WRONG — swallow errors silently
try {
return await getUserById(id);
} catch {
return null; // silent failure
}Renaming packages, modules, or key variables mid-project causes cascading failures that are extremely hard to catch. If you must rename:
- Create a checklist of ALL files and references first
- Use IDE semantic rename (not search-and-replace)
- Full project search for old name after renaming
- Check: .md files, .txt files, .env files, comments, strings, paths
- Start a FRESH Claude session after renaming
For any non-trivial task, start in plan mode. Don't let Claude write code until you've agreed on the plan. Bad plan = bad code. Always.
- Use plan mode for: new features, refactors, architectural changes, multi-file edits
- Skip plan mode for: typo fixes, single-line changes, obvious bugs
- One Claude writes the plan. You review it as the engineer. THEN code.
Every step in a plan MUST have a consistent, unique name. This is how the user references steps when requesting changes. Claude forgets to update plans — named steps make it unambiguous.
CORRECT — named steps the user can reference:
Step 1 (Project Setup): Initialize repo with TypeScript
Step 2 (Database Layer): Create MongoDB wrapper
Step 3 (Auth System): Implement JWT authentication
Step 4 (API Routes): Create user endpoints
Step 5 (Testing): Write E2E tests for auth flow
WRONG — generic steps nobody can reference:
Step 1: Set things up
Step 2: Build the backend
Step 3: Add tests
When the user asks to change something in the plan:
- FIND the exact named step being changed
- REPLACE that step's content entirely with the new approach
- Review ALL other steps for contradictions with the change
- Rewrite the full updated plan so the user can see the complete picture
CORRECT:
User: "Change Step 3 (Auth System) to use session cookies instead of JWT"
Claude: Replaces Step 3 content, checks Steps 4-5 for JWT references,
outputs the FULL updated plan with Step 3 rewritten
WRONG:
User: "Actually use session cookies instead"
Claude: Appends "Also, use session cookies" at the bottom
← Step 3 still says JWT. Now the plan contradicts itself.
Claude will forget to do this. If you notice the plan has contradictions, tell Claude: "Rewrite the full plan — Step 3 and Step 7 contradict each other."
- If fundamentally changing direction:
/clear→ state requirements fresh
When updating any feature, keep these locations in sync:
README.md(repository root)project-docs/(relevant documentation)- Inline code comments
- Test descriptions
If you update one, update ALL.
Every time Claude makes a mistake, add a rule to prevent it from happening again.
This is the single most powerful pattern for improving Claude's behavior over time:
- Claude makes a mistake (wrong pattern, bad assumption, missed edge case)
- You fix the mistake
- You tell Claude: "Update CLAUDE.md so you don't make that mistake again"
- Claude adds a rule to this file
- Mistake rates actually drop over time
This file is checked into git. The whole team benefits from every lesson learned.
Don't just fix bugs — fix the rules that allowed the bug. Every mistake is a missing rule.
If RuleCatch is installed: also add the rule as a custom RuleCatch rule so it's monitored automatically across all future sessions. CLAUDE.md rules are suggestions — RuleCatch enforces them.
- Quality over speed — if unsure, ask before executing
- Plan first, code second — use plan mode for non-trivial tasks
- One task, one chat —
/clearbetween unrelated tasks - One task, one branch — use
/worktreeto isolate work from main - Use
/contextto check token usage when working on large tasks - When testing: queue observations, fix in batch (not one at a time)
- Research shows 2% misalignment early in a conversation can cause 40% failure rate by end — start fresh when changing direction