# 12 Rules for Claude.md (Example)

> Source: <https://gist.github.com/warrenday/07abe3bbf147720345846f195834fc0b>
> Published: 2026-08-13 11:07:28+00:00

| # 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.ts` → `hikes.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 loading 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. |
