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>IdFK columns), the full Human FK Rule (references(() => users.id)now forbidden everywhere, auth-token tables only grandfathered), and the test co-location convention.CLAUDE.mdwins 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—/auditslash command, run before marking PR Ready (PR#1492 added this)eslint.config.js(backend + frontend) — lint gates that block deprecated patternsscripts/git-hooks/pre-commitandpre-push— automated checksscripts/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 viac.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. Theverify*Ownershipvariants are deprecated and bypass RBAC; ESLint blocks them. See RBAC Authorization.
Frontend MUSTs
- Server state → React Query hooks in
frontend/src/lib/queries/. Never inlineuseQuery/useMutationin 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 rawuseSearchParams(lint blocks). See URL State Management. - Global client state → Zustand. Never
useContextfor 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
onDeleteon every FK, index every FK column,NOT NULLunless absence is meaningful, unique constraints for business invariants. - Bilingual fields →
LocalizedText {de, en}viasanitizedLocalizedText()/optionalSanitizedLocalizedText(). Never flatnameDe/nameEncolumns. - Schema changes → edit
backend/src/db/schema.tsonly, thenmake db-generate NAME=<desc>. Never hand-write SQL inatlas/migrations/.atlas.sumis append-only — rename your migration to a current timestamp after merging main, re-runatlas migrate hash; never re-hash from scratch. See Database Migrations. - FK target rule:
contactsis the central person entity; person-naming columns FKcontacts, with only active auth artifacts (sessions, token tables) onusers.id. (CLAUDE.md has since tightened this further — see drift note above.) See Contacts, Users. - User-facing text validation →
strictTextField(N),sanitizedLocalizedText(N). Never rawz.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
filestable andfileIdFK;fileService.upload(stream, …)/download(fileId); the buffer-shapedstorage.tshelpers are deprecated. - Background jobs — BullMQ at
backend/src/lib/jobs/(queue + processor + scheduler); neversetInterval()/setTimeout()recursion. - Drizzle helpers over raw
sql: typed builder by default; rawsql`…`only when Drizzle can’t express it (comment why); neversql.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 BOTHlocales/de/andlocales/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-verifyon commit or push. Fix the root cause. - NEVER edit
backend/atlas/migrations/files oratlas.summanually; never re-hashatlas.sumfrom scratch. - NEVER
mock.module('@/db')ormock.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
userIdFK on a person-naming column. - NEVER
setInterval()/ module-scope loops for recurring work — BullMQ only. - NEVER
sql.raw(...)or user input concatenated intosqltemplates. - 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)
- Worktree branch with prefix
feat-/fix-/chore-/docs-/refactor-/test-/hotfix- cd worktrees/<branch-name>/renewa-one && make dev-init- Discipline: brainstorm → design → TDD → verify
- Quality gates in Docker:
make typecheck && make lint && make test - Bilingual changelog fragments in
renewa-one/frontend/changelog/{de,en}/<branch-name>.md(exempt prefixes:chore-,docs-,test-,refactor-) - Pre-push conflict check:
git fetch origin main && git merge origin/main; rename your migration if a newer one landed git push -u origin <branch-name>thengh pr create --draft. Title ≤ 70 chars.gh pr checks <N>once (no--watch). Fix on same branch.- 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 testall greengrep -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, nouseStatefor server data, no rawuseSearchParams, no inline queries in components, noverify*Ownership, nomock.module('@/db'), no newuserIdFK on a person-naming column, no re-hashedatlas.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.
Related
- Git Workflow — worktree-based branch flow, branch naming, PR rules, quick-lint hook
- Coding Guidelines — TypeScript strict, naming conventions, testing
- Service Layer Pattern, Error Handling Pattern, RBAC Authorization
- React Query Pattern, URL State Management, State Management, Validation Pattern
- Component Decomposition, Component Library
- Database Migrations — Atlas / atlas.sum specifics
- Mock and Config Data — mock vs config split referenced by the prompt
- Contacts, Users — central-identity FK rule context
CLAUDE.md(repo root) (canonical source of truth)