Claude Contributor Prompt

A copy-pasteable prompt that non-technical contributors paste into Claude Code sessions targeted at bookish-broccoli. It front-loads the most-violated project rules and forces a stricter self-review before PR handoff. Sits on top of the auto-loaded CLAUDE.md in the repo root.

Canonical file: docs/claude-contributor-prompt.md on main (verified present 2026-06-12; last substantive update PR#1573, 2026-04-28 — seeds → mocks rename plus new rule blocks). This page mirrors that revision.

Why this exists

Claude Code reads CLAUDE.md automatically in every session, but for non-technical authors driving Claude through a feature/fix the auto-loaded rules aren’t always enough. This prompt:

  • Tells Claude to be more conservative, verify harder, flag pattern concerns before the PR is opened
  • Restates the highest-impact rules at the top of the conversation so they win attention
  • Adds a structured self-check that the contributor can read and confirm before marking “done”

Source of truth precedence: if this prompt and CLAUDE.md ever disagree, CLAUDE.md wins, and the prompt file should be updated to match.

Known drift (as of 2026-06-12): the repo doc has not yet absorbed several newer CLAUDE.md rules — Money/Big.js decimal arithmetic, the HubSpot association infrastructure (no <role>Id FK columns), the full Human FK Rule (references(() => users.id) now forbidden everywhere, auth-token tables only grandfathered), and the test co-location convention. CLAUDE.md wins for all of these.

How to use

In a Claude Code session inside bookish-broccoli, either:

  • Paste the entire Prompt block (below) into the conversation, OR
  • Tell Claude: Read docs/claude-contributor-prompt.md and follow it.

Where the rules are actually enforced

The prompt is documentation; the enforcement lives in:

  • CLAUDE.md (repo root) — project conventions
  • .claude/commands/audit.md/audit slash command, run before marking PR Ready (PR#1492 added this)
  • eslint.config.js (backend + frontend) — lint gates that block deprecated patterns
  • scripts/git-hooks/pre-commit and pre-push — automated checks
  • scripts/quick-lint.sh — Claude Code PostToolUse hook (see Git Workflow)

Rule summary (synthesized)

The full prompt below is the canonical text. This summary is for quick reference / cross-linking.

Stance

  • Prefer the smallest, most boring, most in-pattern solution
  • Grep for 3 existing examples before writing new code
  • Stop and propose alternatives when the request would introduce a new pattern, contradict an existing one, or bypass a helper
  • Discipline: brainstorm → design → TDD (failing test first) → verify → self-review → push → draft PR
  • Ask before coding when uncertain

Backend MUSTs

  • Routes lean: parse → destructure services → call → respond. Business logic in services. See Service Layer Pattern, Routes Overview.
  • Infrastructure via c.var.container; services via c.var.services (destructure only, never alias)
  • Typed errors from lib/errors.ts (NotFoundError, AuthorizationError, …). Never raw strings. See Error Handling Pattern.
  • RBAC: always verify*Access / require*Access. The verify*Ownership variants are deprecated and bypass RBAC; ESLint blocks them. See RBAC Authorization.

Frontend MUSTs

  • Server state → React Query hooks in frontend/src/lib/queries/. Never inline useQuery/useMutation in components/pages (lint blocks). See React Query Pattern, Query Modules.
  • URL-shareable UI state → hooks in frontend/src/hooks/url-state/ (useTabState, useFilterState, useModalState, useMultiSortState). Never raw useSearchParams (lint blocks). See URL State Management.
  • Global client state → Zustand. Never useContext for feature-global state. See State Management.
  • Forms → react-hook-form + zodResolver. See Validation Pattern.
  • API contracts → types in shared/, imported by both sides. Never inline DTO interfaces on the frontend. See Shared Layer.

Data MUSTs

  • Schema lean by default: the answer to “should I add this column/table/index/FK?” is no unless argued to yes — needed now (YAGNI), no existing home, minimal cardinality, cleanup story. Integrity defaults: explicit onDelete on every FK, index every FK column, NOT NULL unless absence is meaningful, unique constraints for business invariants.
  • Bilingual fields → LocalizedText {de, en} via sanitizedLocalizedText() / optionalSanitizedLocalizedText(). Never flat nameDe/nameEn columns.
  • Schema changes → edit backend/src/db/schema.ts only, then make db-generate NAME=<desc>. Never hand-write SQL in atlas/migrations/. atlas.sum is append-only — rename your migration to a current timestamp after merging main, re-run atlas migrate hash; never re-hash from scratch. See Database Migrations.
  • FK target rule: contacts is the central person entity; person-naming columns FK contacts, with only active auth artifacts (sessions, token tables) on users.id. (CLAUDE.md has since tightened this further — see drift note above.) See Contacts, Users.
  • User-facing text validation → strictTextField(N), sanitizedLocalizedText(N). Never raw z.string() for user-visible content.
  • Mock vs Config data: mock/demo → backend/src/db/mocks/ (AUTO_MOCK=true, local/pr-preview/development only); config the app needs → DB migration. “Seeding”/seed* identifiers are retired (deterministicUuid, UUID_NAMESPACES). See Mock and Config Data.
  • Files — CAS + streams: every file via the files table and fileId FK; fileService.upload(stream, …) / download(fileId); the buffer-shaped storage.ts helpers are deprecated.
  • Background jobs — BullMQ at backend/src/lib/jobs/ (queue + processor + scheduler); never setInterval() / setTimeout() recursion.
  • Drizzle helpers over raw sql: typed builder by default; raw sql`…` only when Drizzle can’t express it (comment why); never sql.raw(userInput) or string-concatenated input.

Code quality MUSTs

  • All identifiers in English. German only in i18n JSON, locale data, proper nouns in comments. Pre-commit hook blocks German identifiers (PR#1491 hardened this).
  • User-facing strings — i18n only: every visible string through t('ns.key') (frontend) / getFixedT(locale, '<ns>') (backend boundary), keys added to BOTH locales/de/ and locales/en/ in the same commit (parity hook enforces).
  • Files >600 lines → decompose into 4 modules: fooService.ts + useFooState.ts + FooSection.tsx + Foo.tsx. See Component Decomposition.
  • New components/ui/*.tsx → must have a sibling .stories.tsx. See Component Library.
  • New features and bug fixes → tests with // AC-N: labels mapping to acceptance criteria.

MUST NOT (anti-patterns)

  • NEVER commit to main (use a worktree branch). See Git Workflow.
  • NEVER --no-verify on commit or push. Fix the root cause.
  • NEVER edit backend/atlas/migrations/ files or atlas.sum manually; never re-hash atlas.sum from scratch.
  • NEVER mock.module('@/db') or mock.module('@/lib/auth') — they leak process-wide and have broken 600+ tests. Use constructor DI.
  • NEVER hold server data in useState; never inline frontend DTO interfaces.
  • NEVER add a new userId FK on a person-naming column.
  • NEVER setInterval() / module-scope loops for recurring work — BullMQ only.
  • NEVER sql.raw(...) or user input concatenated into sql templates.
  • NEVER hardcode user-facing text (German or English) in components, routes, or services.
  • NEVER suppress an ESLint/TypeScript error without explaining why in the PR description.

Workflow (mandatory)

  1. Worktree branch with prefix feat- / fix- / chore- / docs- / refactor- / test- / hotfix-
  2. cd worktrees/<branch-name>/renewa-one && make dev-init
  3. Discipline: brainstorm → design → TDD → verify
  4. Quality gates in Docker: make typecheck && make lint && make test
  5. Bilingual changelog fragments in renewa-one/frontend/changelog/{de,en}/<branch-name>.md (exempt prefixes: chore-, docs-, test-, refactor-)
  6. Pre-push conflict check: git fetch origin main && git merge origin/main; rename your migration if a newer one landed
  7. git push -u origin <branch-name> then gh pr create --draft. Title ≤ 70 chars.
  8. gh pr checks <N> once (no --watch). Fix on same branch.
  9. Mark Ready for Review only when all checks green.

Self-check checklist

  • Brainstormed (for creative work); failing test first (for bug fix / feature)
  • make typecheck && make lint && make test all green
  • grep -rn "TODO\|FIXME" <changed files> returns zero, or each has an issue number
  • Changelog fragments present (unless exempt branch); screenshots in .supporting/verification/<branch>/ if UI changed
  • PR description names the pattern(s) applied and confirms no anti-patterns introduced
  • No --no-verify, no useState for server data, no raw useSearchParams, no inline queries in components, no verify*Ownership, no mock.module('@/db'), no new userId FK on a person-naming column, no re-hashed atlas.sum

The full prompt (canonical text — copy-paste this)

# Instructions for contributing to Renewa One
 
You are helping me build on Renewa One. The repo has a `CLAUDE.md` file that
auto-loads — follow it exactly. These additional instructions raise the bar
for PRs authored via Claude by a non-technical contributor: be more
conservative, verify harder, and flag pattern concerns BEFORE I submit.
 
## Your stance
- Prefer the smallest, most boring solution that matches an existing pattern
  in the repo. Before writing new code, grep for 3 existing examples and
  copy their shape.
- If my request would introduce a new pattern, contradict a project pattern,
  or bypass an existing helper, STOP and propose the in-pattern alternative
  before coding.
- Follow the discipline: brainstorm → design → TDD (failing test first) →
  verify with evidence → self-review → push → draft PR. No shortcuts without
  me explicitly saying "skip discipline".
- When uncertain, ASK me before writing code. Say:
  "I'd approach this with pattern X because Y; the alternative is Z — OK?"
 
## MUST follow (project patterns)
 
**Backend architecture**
- Routes stay lean: parse → `const { foo } = c.var.services;` (destructure
  only, never alias) → call → respond. Business logic lives in services.
- Infrastructure access via `c.var.container`; service access via
  `c.var.services`.
- Typed errors from `lib/errors.ts` (`NotFoundError`, `AuthorizationError`,
  …). Never throw raw strings.
- RBAC: always `verify*Access` / `require*Access`. The `verify*Ownership`
  variants are deprecated and bypass RBAC — ESLint blocks them.
 
**Frontend architecture**
- Server state → React Query hooks defined in `frontend/src/lib/queries/`.
  NEVER inline `useQuery`/`useMutation` in `components/` or `pages/` (lint
  blocks this).
- URL-shareable UI state → hooks in `frontend/src/hooks/url-state/`
  (`useTabState`, `useFilterState`, `useModalState`, `useMultiSortState`).
  NEVER raw `useSearchParams` (lint blocks this).
- Global client state → Zustand. NEVER `useContext` for feature-global state.
- Forms → `react-hook-form` + `zodResolver`.
- API contracts → types defined in `shared/`, imported by both backend and
  frontend. NEVER redeclare DTO shapes inline on the frontend.
 
**Data**
- **Schema changes — lean by default.** Default answer to "should I add
  this column / table / index / FK?" is *no*; argue your way to *yes*.
  For every new schema artifact:
  - **Necessary now?** Required for the feature shipping in this PR
    (not "we might want this later"). YAGNI applies to the schema.
  - **Could it live somewhere existing?** Extend a parent table or an
    existing JSONB shape before creating a new table or sibling
    column.
  - **Cardinality minimised?** 1:1 → just add columns to the parent.
    1:n → FK on the child, no join table. m:n → ONLY if both sides
    genuinely have multiple counterparts AND the relation has zero
    attributes. The moment a join row needs `assignedAt` / `role` /
    `status` / `expiresAt`, it's a first-class entity, not a join
    table.
  - **Cleanup story?** When the feature is removed, the schema
    artifacts go too. PRs that remove functionality must remove the
    now-orphan columns / tables in the same PR.
  - **Integrity defaults**: every FK has an explicit `onDelete`
    (`'cascade'` / `'set null'` / `'restrict'` — never omit), every FK
    column has an accompanying index, columns are `NOT NULL` unless
    absence is semantically meaningful, unique constraints capture
    business invariants.
- Bilingual fields → `LocalizedText {de, en}` via `sanitizedLocalizedText()`
  / `optionalSanitizedLocalizedText()`. NEVER flat `nameDe`/`nameEn` columns.
- Schema changes → edit `backend/src/db/schema.ts` ONLY. Then run
  `make db-generate NAME=<short_description>`. NEVER hand-write SQL in
  `atlas/migrations/` and NEVER touch `atlas.sum`. **`atlas.sum` is
  append-only** — if a newer migration landed on main while you were
  branched, rename your migration to a current timestamp (so it sorts
  after main's last) and re-run `atlas migrate hash`. Never re-hash from
  scratch — that corrupts the Merkle chain and blocks deploys.
- **FK target rule: contact is the central person entity, user is a
  transient auth artifact.** Any column naming a person FKs to
  `contacts` — both identity (`departmentId`, `reportsToContactId`) and
  audit-trail (`assigneeId`, `ownerId`, `createdBy`, `updatedBy`,
  `reviewerId`, `assignedBy`). The only legitimate `users.id` FK is on
  an active authentication artifact (`sessions`, `magic_link_tokens`,
  `password_reset_tokens`, `email_verification_tokens`). Column names
  stay role-based — no `*ContactId` suffix; the rule is universal so
  the suffix is noise.
- User-facing text validation → `strictTextField(N)`,
  `sanitizedLocalizedText(N)`. NEVER raw `z.string()` for anything a
  user will see.
- **Mock vs Config data:**
  - Mock/demo data (sample users, demo buildings, fixture rows the app
    doesn't need to function) → `backend/src/db/mocks/`. Loads at app
    startup when `AUTO_MOCK=true` in `local`/`pr-preview`/`development`
    only. (`AUTO_SEED` is honoured as a deprecated alias.)
  - Config data the app NEEDS to run (enum lookup tables, default
    departments, system permissions, default workflows, …) → **DB
    migration** (`make db-generate NAME=config_<thing>` or
    `atlas migrate new config_<thing>` for hand-written data SQL).
    Migrations are the only mechanism that is both idempotent AND runs
    on every environment, including staging and production.
  - Decision rule: "would the app fail to function in production
    without this row?" Yes → migration. No → `mocks/`.
  - The terms "seeding"/"seeds" are retired in new code. The
    deterministic-UUID helpers were also renamed: `seedUuid`
    `deterministicUuid`, `SEED_NAMESPACES``UUID_NAMESPACES`. No new
    identifier should re-introduce the `seed*` prefix.
- **File handling — CAS + streams:**
  - Every file goes through CAS. The `files` table is the single
    source of truth.
  - Any new column referring to a file is a `fileId: uuid('file_id')
    .references(() => files.id, …)` FK. NEVER a path, URL, blob,
    base64 string, or inline bytes column.
  - Read a file → `fileService.download(fileId)` → consume the
    returned stream. Write a file → `fileService.upload(stream, …)`
    → returns `fileId`.
  - The buffer-shaped helpers in `backend/src/lib/storage.ts`
    (`uploadFile(key, body: Buffer, …)`, `downloadFile(key): Buffer`)
    are deprecated legacy. NEVER import them in new code.
  - Loading a whole file into memory before persisting/returning it
    is a bug, not a style preference.
- **Recurrent / background jobs — use BullMQ:**
  - Any "run this every N minutes" / "process X in the background"
    work goes through `backend/src/lib/jobs/`: a queue (in
    `factory.ts`), a worker / processor (in `processors/`), and a
    scheduler entry (in `schedulers.ts`) that registers the
    repeatable job at app startup.
  - NEVER use `setInterval()`, `setTimeout()` recursion, or a
    module-scope loop to schedule recurring work. That shape runs on
    every replica simultaneously, drops work on crash, and has no
    retry / observability. It is an anti-pattern.
  - Copy from existing processors:
    `processors/notifications.ts`, `processors/reminder-scheduler.ts`,
    `processors/rejection-bundle.ts`, `processors/entra-sync.ts`.
- **Database queries — Drizzle helpers, not raw `sql`:**
  - Default to Drizzle's typed query builder: `eq`, `and`, `or`,
    `inArray`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`, `isNull`,
    `count`, `coalesce`, `desc`, etc. They bind values through the
    Postgres driver and are SQL-injection-safe by construction.
  - Reach for raw `` sql`…` `` template literals ONLY when Drizzle
    cannot express what you need (functional indexes, atomic counter
    increments, PG operators with no Drizzle equivalent). Add a
    one-line comment justifying why.
  - NEVER use `sql.raw(userInput)`, NEVER concatenate user input into
    a `sql` template, NEVER interpolate unvalidated request values via
    `${}`. Every new `` sql`…` `` site is a SQL-injection touchpoint.
 
**Code quality**
- All identifiers in English (German only in i18n JSON, locale data, proper
  nouns in comments). Pre-commit hook blocks German identifiers.
- **User-facing strings — i18n only.** Every string a user reads goes
  through `useTranslation()` / `t('ns.key')` and lives in
  `renewa-one/frontend/src/locales/{de,en}/<namespace>.json`. NEVER
  hardcode user-visible text — neither German nor English — in
  `.tsx`/`.ts` files. Applies to JSX text, button labels, `aria-label`
  / `title` / `alt` / `placeholder`, toast messages, Zod / validation
  errors, table headers, menu items, dialog titles, empty-state copy,
  breadcrumbs. Backend-side: error messages returned to the client and
  email/SMS/notification copy must be keyed and localized at the
  response boundary (`getFixedT(locale, '<ns>')`), not hardcoded German.
  When you add a new key, add it to BOTH `locales/de/<ns>.json` AND
  `locales/en/<ns>.json` in the same commit — the i18n parity hook
  enforces sync.
- Files >600 lines → decompose into 4 modules:
  `fooService.ts` (pure logic) + `useFooState.ts` (hooks) + `FooSection.tsx`
  (presentational) + `Foo.tsx` (orchestrator).
- New `components/ui/*.tsx` → must have a sibling `.stories.tsx`.
- New features and bug fixes → must include tests with `// AC-N:` labels
  mapped to the design doc's acceptance criteria.
 
## MUST NOT do (anti-patterns)
 
- NEVER commit to `main`. Always create a worktree:
  `git worktree add ./worktrees/<branch-name> -b <branch-name>`.
- NEVER use `--no-verify` on commit or push. If a hook fails, fix the root
  cause. Tell me if you're stuck.
- NEVER edit files in `backend/atlas/migrations/` or `atlas.sum` — they are
  generated. NEVER re-hash `atlas.sum` from scratch on a merge — append only.
- NEVER use `mock.module('@/db')` or `mock.module('@/lib/auth')` — they
  leak process-wide and broke 600+ tests last time. Use constructor DI.
- NEVER hold server data in `useState`. Use React Query.
- NEVER write an inline `interface` on the frontend for API response shapes.
  Use shared types from `shared/`.
- NEVER add a new `userId` FK on a column that names a person (assignee,
  owner, creator, reviewer, …). Those target `contacts`. The only `userId`
  FKs are on auth artifacts.
- NEVER use `setInterval()`, `setTimeout()` recursion, or a module-scope
  loop to schedule recurring work. Use the BullMQ infra at
  `backend/src/lib/jobs/`.
- NEVER use `sql.raw(...)`, NEVER concatenate user input into a `sql`
  template literal, NEVER reach for raw `` sql`…` `` when Drizzle's
  typed helpers cover the case.
- NEVER hardcode user-facing text in components, routes, or services —
  not in German, not in English. Every visible string goes through
  `t('ns.key')` (frontend) or `getFixedT(locale, '<ns>')` (backend
  user-facing responses), with the key added to both `locales/de/` and
  `locales/en/` in the same commit.
- NEVER suppress an ESLint or TypeScript error without telling me why in
  the PR description.
 
## Workflow (mandatory)
 
1. `git worktree add ./worktrees/<branch-name> -b <branch-name>` — branch
   prefix is one of `feat-`, `fix-`, `chore-`, `docs-`, `refactor-`,
   `test-`, `hotfix-`.
2. `cd worktrees/<branch-name>/renewa-one && make dev-init`.
3. Work the discipline (brainstorm, design, TDD, verify).
4. Quality gates in Docker: `make typecheck`, `make lint`, `make test`.
5. Bilingual changelog fragments in
   `renewa-one/frontend/changelog/{de,en}/<branch-name>.md` (skip only if
   branch prefix is `chore-`, `docs-`, `test-`, or `refactor-`).
6. Conflict check before push:
   `git fetch origin main && git merge origin/main`. If a new migration
   landed on main, rename your migration to a current timestamp before
   pushing — see "Data" rules above.
7. `git push -u origin <branch-name>` then
   `gh pr create --draft --title "…" --body "…"`. Title ≤ 70 chars.
8. After PR opens, run `gh pr checks <N>` ONCE (no `--watch`). Fix failures
   on the same branch.
9. Mark Ready for Review ONLY when all checks are green.
 
## Self-check before you tell me "done"
 
- [ ] Brainstormed with me (for creative work)
- [ ] Wrote a failing test first (for bug fix or feature)
- [ ] `make typecheck && make lint && make test` — all green
- [ ] `grep -rn "TODO\|FIXME" <changed files>` returns zero, or each has an
      issue number
- [ ] Changelog fragments present (unless branch is exempt)
- [ ] Screenshots saved to `.supporting/verification/<branch>/` (if UI
      changed)
- [ ] PR description names the pattern(s) applied and confirms no
      anti-patterns were introduced
- [ ] No `--no-verify`, no `useState` for server data, no raw
      `useSearchParams`, no inline queries in components, no
      `verify*Ownership`, no `mock.module('@/db')`, no new `userId` FK on
      a person-naming column outside the auth-artifact allow-list, no
      re-hashed `atlas.sum`

Source

Canonical file: docs/claude-contributor-prompt.md on main in renewa-gmbh/bookish-broccoli. When the source is updated, re-ingest this page.