cd /news/developer-tools/12-rules-for-claude-md-example · home topics developer-tools article
[ARTICLE · art-95339] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

12 Rules for Claude.md (Example)

A developer shared a detailed CLAUDE.md configuration file for an iOS app called Trailmark, which is backed by a Node.js REST API. The file establishes strict coding rules, including type safety, naming conventions, and a test-driven development loop, to guide AI assistants in maintaining the project.

read12 min views1 publishedAug 13, 2026

| # CLAUDE.md | | | Trailmark — an iOS app for logging hikes, backed by a Node.js REST API. | | | - ios/ — SwiftUI app, iOS 17+, native components only | | | - api/ — Node 22, TypeScript, Express, Postgres 16 (Kysely) | | | - packages/contracts/ — zod schemas + generated OpenAPI. Single source of truth for both sides. Change the contract first, then the server, then the client. | | | Nested rules live in api/CLAUDE.md and ios/CLAUDE.md. Longer workflows (release, migration, security review) are skills in .claude/skills/ — do not paste them here. | | | ## Ask before you assume | | | Never guess at intent. If a task leaves anything open — which screen, which endpoint, what happens on failure, whether it needs a migration, whether this is user-facing — stop and ask. One question up front is cheaper than half a day of work in the wrong direction. | | | - Ask when the request could reasonably mean two different things. | | | - Ask before changing a public API shape, a DB schema, or anything in packages/contracts/. | | | - Do not invent product decisions, copy, or acceptance criteria. | | | - Do not widen scope past what was asked. Note the adjacent thing you spotted; don't fix it unprompted. | | | - If you had to assume something you couldn't resolve, list it explicitly at the top of your summary. | | | ## The loop | | | Every change runs through this. A task is not done until it is green. | | | bash | | | npm run check # tsc --noEmit, eslint, prettier --check | | | npm test # vitest, unit + integration | | | npm run test:api # supertest against a throwaway Postgres | | | npm run db:migrate # never edit an applied migration, always add a new one | | | | | | bash | | | cd ios && xcodegen && xcodebuild test -scheme Trailmark -destination 'platform=iOS Simulator,name=iPhone 17' | | | cd ios && swiftlint --strict | | | | | | Rules for the loop: | | | - Write the failing test first. Watch it fail for the right reason, then make it pass. | | | - Run npm run check && npm test after every meaningful edit, not once at the end. | | | - Never report success on a red loop. Never disable, skip, or .only a test to get to green. | | | - If a test is wrong, say so and explain why before changing it. | | | - Do not start long-running processes (npm run dev, expo start) to "verify" — they never exit. Use the commands above. | | | ## Type checking | | | Both languages are strict. Type errors are not warnings. | | | tsconfig.json — these stay on: | | | json | | | { | | | "compilerOptions": { | | | "strict": true, | | | "noImplicitAny": true, | | | "noUncheckedIndexedAccess": true, | | | "exactOptionalPropertyTypes": true, | | | "noImplicitOverride": true, | | | "verbatimModuleSyntax": true | | | } | | | } | | | | | | - No any. If a type is genuinely unknown, use unknown and narrow it. | | | - No as to escape an error, and no non-null !. Fix the type or narrow properly. | | | - No @ts-expect-error without a comment naming the upstream issue. | | | - All external input (request bodies, query params, API responses) is parsed with a zod schema from packages/contracts/. Types are inferred from the schema, never hand-written alongside it. | | | - Swift: no force unwraps, no try!, no as!. Use guard let, typed throws, and Result at boundaries. | | | Respect these from the start. Do not write loose code and correct it after the type check fails. | | | ## Naming | | | Consistency is for the model as much as for us. Pick the existing word, don't coin a new one. | | | Domain vocabulary — one word per concept: | | | | Concept | Use | Never | | | | | --------------- | ---------------------- | ----------------------------- | | | | | A recorded walk | hike | trip, walk, activity, session | | | | | The GPS line | track | route, path, trace | | | | | A saved place | waypoint | pin, marker, poi | | | | | Account access | Sign in / Sign out | Login, Log In, Log out | | | | Code: | | | - Functions: createHike, getHike, listHikes, updateHike, deleteHike. Not fetch, remove, save, handle. | | | - Booleans read as assertions: isSyncing, hasTrack, canEdit. | | | - Routes: kebab-case, plural nouns — GET /v1/hikes/:hikeId/waypoints. | | | - Postgres: snake_case tables and columns, plural tables — hikes, waypoints, started_at. | | | - Swift types are UpperCamelCase and the file is named after the type: HikeDetailView.swift. | | | - SwiftUI views end in View, observable state ends in Store: HikeListView, HikeStore. | | | - Test files sit beside the source: hikes.service.tshikes.service.test.ts. | | | User-facing copy: sentence case for buttons and labels ("Save hike", not "Save Hike"). Copy strings live in ios/Trailmark/Resources/Localizable.strings — no string literals in views. | | | ## Project structure | | | | | | api/ | | | src/ | | | modules/<domain>/ # route.ts, service.ts, repo.ts, *.test.ts | | | db/ # kysely client, generated types | | | middleware/ | | | lib/ # shared, dependency-free helpers | | | migrations/ # timestamped .sql, append-only | | | ios/ | | | Trailmark/ | | | Features/<Feature>/ # View, Store, and their tests | | | DesignSystem/ # Tokens.swift, reusable components | | | Networking/ # APIClient, generated contract types | | | Persistence/ # SwiftData models | | | packages/ | | | contracts/ # zod schemas, OpenAPI output | | | | | | - New backend work goes in a module folder. Routes never talk to the DB directly — route → service → repo. | | | - New screens go in Features/. Views hold no networking and no business logic; that belongs in the Store. | | | - Nothing new at the repo root without asking. | | | ## Dependencies | | | Code is cheap; maintenance isn't. Prefer a well-established package over rolling your own, and prefer the platform over a package. | | | Before installing anything, check and state: | | | - Weekly downloads over ~100k, a release in the last six months, more than one maintainer. | | | - Nothing single-maintainer or freshly published for anything touching auth, crypto, networking, or file I/O. | | | - No new dependency for something the standard library, SwiftUI, or an existing dependency already does. | | | Ask before adding a dependency. Never add one as a side effect of another task, and never pin to latest — exact versions only, lockfile committed. | | | Native SwiftUI components only on iOS. No third-party UI frameworks. | | | ## Performance | | | Every endpoint has a p95 budget of 200ms. Anything slower is a bug, not a tuning opportunity for later. | | | - Filter, sort, aggregate, and paginate in SQL. Never pull rows into Node to filter them there. | | | - Every list endpoint is paginated: limit defaults to 25, hard maximum 100. No unbounded SELECT. | | | - Select the columns you need. select * is not acceptable in a repo function. | | | - No N+1. Join or batch — one query per request path, not one per row. | | | - New query patterns come with an index in the same migration. Run EXPLAIN ANALYZE and include the plan in your summary if a query touches more than 10k rows. | | | - Do a performance pass at the end of any feature that reads data, and say what you checked. | | | On iOS: no work on the main actor beyond UI updates. Lists over 50 items use LazyVStack. Images are downsampled before display. | | | ## Error handling | | | Fail early, fail loudly, and never swallow. | | | - No empty catch. No catch { console.log(e) }. Either handle it meaningfully or let it propagate. | | | - Throw a typed AppError with a stable machine-readable code. Unexpected errors surface as 500 and page us — they do not get mapped to a friendly 200. | | | - Every error response uses this envelope: | | | ts | | | { error: { code: "hike_not_found", message: "That hike no longer exists.", details?: unknown } } | | | | | | - Messages are useful to the client: what failed, and what to do about it. "Something went wrong" is not an error message. | | | - Validation failures return 422 with the zod issue list in details. | | | - iOS maps every error code to real copy and a real recovery action. No silent failures, no generic alert as a catch-all. | | | - Log with structured context (hikeId, userId, requestId), never a bare string. | | | ## End-to-end testing | | | After any feature that spans both sides, exercise it like a person would — not just with unit tests. | | | - Boot the API and the simulator, sign in, and drive the actual flow end to end: start a hike, record a track, background the app, foreground it, sync, confirm the row in Postgres. | | | - Test the unhappy paths deliberately: airplane mode mid-sync, expired token, force-quit during a recording, duplicate submit. | | | - Report what you clicked and what you saw. If a step failed, keep the failure — don't work around it and call it passing. | | | You don't need to write Playwright suites for this. Driving the app as a human is the point. | | | ## UI testing | | | A green test suite tells you nothing about whether the screen looks right. | | | - Screenshot every screen you touch, at least once, and look at it before you say it's done. | | | - Check the small iPhone and the largest Dynamic Type setting. Check dark mode. | | | - Use realistic seed data from api/seeds/realistic.sql — real place names, long names, empty states, a 400-item list. Never Lorem ipsum, never "Test User". | | | - Look specifically for: clipped or truncated text, overlapping views, content under the safe area, missing empty state, missing state. | | | ## Architecture | | | Read this before exploring the codebase. If you find yourself searching for something that belongs here, add it (see below). | | | Request path: SwiftUI view → Store → APIClient → Express route → service → repo → Postgres. Auth is a short-lived JWT in the Keychain with a refresh token; middleware/auth.ts populates req.user and every route below /v1 requires it. | | | Sync is offline-first. The app writes to SwiftData immediately, queues a mutation, and reconciles on reconnect. Server timestamps win on conflict; the client never invents an id — it sends a client-generated idempotencyKey. | | | Where things live: | | | | You need | Look in | | | | | -------------------------------- | ------------------------------------------- | | | | | Auth, tokens, refresh | api/src/modules/auth/ | | | | | Request/response schemas | packages/contracts/src/ | | | | | DB client and generated types | api/src/db/ | | | | | Migrations | api/migrations/ | | | | | Colours, spacing, type scale | ios/Trailmark/DesignSystem/Tokens.swift | | | | | Networking, retry, auth headers | ios/Trailmark/Networking/APIClient.swift | | | | | Offline queue and reconciliation | ios/Trailmark/Persistence/SyncQueue.swift | | | | | Env vars and their defaults | api/src/config.ts | | | | | CI pipeline | .github/workflows/ci.yml | | | | ## Keeping this file current | | | This file is a failure log, not a wishlist. Every line below exists because it went wrong at least once. | | | When you make a mistake, get corrected, or discover something about this codebase that wasn't written down: | | | 1. Add one line to the failure log below, in the imperative, describing the correct behaviour. | | | 2. Keep it specific to this repo. General advice belongs nowhere. | | | 3. If the fix is a workflow rather than a rule, put it in .claude/skills/ and link it from here. | | | 4. Include the change in the same commit and mention it in your summary. | | | Keep this file under 500 lines. It is loaded into every session, and long context makes you less reliable, not more. If a section outgrows its usefulness, move it to api/CLAUDE.md, ios/CLAUDE.md, or a skill. | | | ## Failure log | | | - Do not run npm run dev to verify a change; it never exits. Use npm run check && npm test. | | | - Run migrations against the test database before npm run test:api, or every test fails on a missing column. | | | - xcodebuild needs xcodegen run first — the .xcodeproj is generated and gitignored. | | | - Do not edit packages/contracts/dist/; it is generated by npm run contracts:build. | | | - Do not add @MainActor to HikeStore methods that already run on the main actor — SwiftLint flags it and it hides real concurrency bugs. | | | - Waypoint ordering comes from sequence, not created_at. Offline waypoints arrive out of order. | | | - Never call deleteHike without checking for queued mutations against that hike first. |

── more in #developer-tools 4 stories · sorted by recency
── more on @trailmark 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/12-rules-for-claude-…] indexed:0 read:12min 2026-08-13 ·