This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This repo is worked on by both GitHub Copilot and Claude Code. Keep these authoritative:
.github/copilot-instructions.md- canonical, shared conventions. Copilot cannot readCLAUDE.md, and the automated code reviews read that file rather than.claude/, so when conventions change, update it too.docs/architecture/anddocs/usage/- per-area detail (read these instead of re-deriving structure)..claude/rules/- path-scoped rules that load automatically.code-style.mdloads when editing.ts/.tsx,testing.mdwhen editing tests or test tooling,docs-authoring.mdwhen editing markdown,prompt-skill-sync.mdwhen editing a skill or either half of a published audit, andrepo-independence.mdwhen editingpackage.json, a config, a workflow, ordocs/..claude/skills/typescript-code-and-test-standards/- the codebase-agnostic conventions (comments, JSDoc, readability, the test mandate, the mocking policy, the Google style digest), published for reuse elsewhere. The rules files above carry only this repository's deltas and defer to it.
If .claude/ and .github/prompts/ were deleted tomorrow, everything must still build, test, and lint. No package.json script, config, workflow, or page under docs/ may reference or invoke anything in them. The dependency runs one way: agent tooling may name npm run test:jest; package.json may not name .claude/anything. The sole exception is an ignore or exclude glob, which is inert when the path is absent. Agent tooling that needs to be runnable gets a target in .claude/Makefile instead, which is deleted along with the tooling it drives. Full rule: repo-independence.md.
This applies to every file type, not only markdown, because a path-scoped rule can only cover the extensions someone thought to list. A comment, document, or config header states what the code does now. Never narrate the past ("replaces", "used to", "formerly", "for the first time", "unlike the old") and never name a file, flag, or tool that no longer exists: git carries that history, and a reader cannot check a claim against something that is gone. The future belongs nowhere but a TODO. Rationale worth keeping goes in a decision record of its own under docs/, created when the first one is needed, rather than scattered through the files it explains.
npm run dev- dev server at localhost:3000npm run validate- full quality gate (prettier, eslint, tsc, jest, cypress, build, markdownlint)- Individual gates, none of which write:
npm run prettier:check,npm run eslint:check,npm run tsc,npm run test:jest,npm run test:cypress:e2e,npm run build,npm run lint:markdown:check - Run a single Jest test:
- one file:
npx jest src/components/banner/Banner.test.tsx - one case by name:
npx jest -t 'partial test name' - (path aliases resolve in tests via
moduleNameMapperinjest.config.js)
- one file:
- Install with
npm ci. CI runs on Node 24.x (.github/workflows/code-qa.yaml); there are no pre-commit hooks, sonpm run validateis what covers the same ground by hand.
Every change to logic, tests, configuration, or documentation ends with the quality gates run and green. This is not optional and not deferrable.
validatewrites and CI does not. Three of its gates run the fixing variants (prettier,eslint,lint:markdown): each repairs whatever its autofix reaches and exits non-zero on what is left, such as a markdown file that does not open with a top-level heading. The other four (tsc,test:jest,test:cypress:e2e,build) only report. The workflows run the reading variants (prettier:check,eslint:check,lint:markdown:check) and modify nothing, so commit whatvalidaterewrote or CI fails on the rule that rewrite settled.- Confirm the actual exit code (
echo "EXIT: $?") after each gate. The output is long and failures surface at the end, so scrolling it is not a check. - A single gate is never a substitute for the full set. Running
npm run test:jestalone skips type checking, linting, the build, and markdown linting. - The skill publishability check is deliberately not a gate:
npm run validatemust work with no agent tooling present. Run it withmake -f .claude/Makefile check-skills, and hand any semantic divergence between a published audit's two halves to theprompt-skill-syncsubagent, which no script can decide. - If a gate fails, fix the cause and re-run until it passes. Never report work complete, or describe validation as passing, before that point. Report a pre-existing failure honestly rather than presenting it as unrelated and therefore fine.
- Run
npm run prettieragain after any ESLint fix, and finish withnpm run prettier:check:eslint --fixinserts braces inline where Prettier would break the statement across lines. - If
test:cypress:e2efails, quote the actual error. Treat it as an environment limit only when the Cypress binary fails to launch, an Electron or window-server error raised before any spec runs, since Cypress needs a GUI session that a headless agent shell may not have. A failing assertion inside a spec is a real failure. Either way, run the remaining gates (buildandlint:markdowncome after Cypress in the chain) and say plainly that e2e was not run. - Delegate the run to the
validatorsubagent to keep verbose Jest and build output out of this context.
A Stop hook blocks the first attempt to finish while gates are outstanding, and names which ones.
A single-page Next.js App Router portfolio: the whole site is src/app/layout.tsx (metadata, SEO/JSON-LD, providers) plus src/app/page.tsx. Stack is React 19 + TypeScript + Material-UI (Emotion). Portfolio content is static TypeScript data in src/data/ (projects, publications, socials, keywords) imported and rendered directly: there is no CMS, database, or content fetching. State is minimal local React hooks (no Redux/global store). Cross-cutting concerns: Sentry (client/server/edge configs), Firebase analytics, Vercel Speed Insights, and a PWA service worker (public/sw.js registered by src/components/ServiceWorkerRegister.tsx); security headers live in next.config.js. For per-area detail see docs/architecture/.
Two rules trip people up most: use tabs, not spaces, and import via path aliases (@components/...), never relative paths.
The conventions live in two layers. The generic set (comment discipline, JSDoc, readability, the test mandate, one colocated test per source, the mocking policy, and the Google TypeScript Style Guide digest) is in typescript-code-and-test-standards, which detects a project's own configuration rather than assuming one. This repository's deltas (path aliases, MUI sx, Server Components, the closed mock boundary table, the test exemptions, the Google carve-outs) are in code-style.md and testing.md, which auto-load and direct you to the skill. Prettier, ESLint, and tsc enforce what they can.
- Each
.github/prompts/file ships twice: as a single prompt file and as a skill directory. The two carry the same objective, not the same bytes, because only the skill can bundlereferences/,agents/, andassets/. After editing either half, runmake -f .claude/Makefile check-skillsand hand both halves to theprompt-skill-syncsubagent (seeprompt-skill-sync.md). Each half is downloaded alone: the prompt names nothing beside it, the skill names nothing outside itself, and neither names a sibling audit or this repository. - Skills:
/audit-docs,/audit-pr, and/audit-quality(the paired audits);/write-tests(repo procedure for authoring a test);/check-skills(validate the skills and their prompt halves); and/typescript-code-and-test-standards(the codebase-agnostic conventions, whichcode-style.mdalso loads you into on TypeScript and JavaScript files). Plus the built-in/code-reviewand/security-review. - Skills carry one of three states, which
make -f .claude/Makefile check-skillsprints and enforces. Published (audit-docs,audit-pr,typescript-code-and-test-standards) are used outside this repository, so they stay codebase-agnostic and, apart from the TypeScript one, language-agnostic. Installable (audit-quality) can be offered by an installer but is not held to that bar. Internal (check-skills,write-tests) setmetadata: internal: true, which hides them fromnpx skillsdiscovery but not fromgh skill, which reads no visibility field and offers all six. The rule tying it together: every skill carries alicensekey and aLICENSE.txt, because a copied directory is all the recipient gets and the state cannot be relied on to stop the copy. Nothing is vendored here; a third-party skill is fetched on demand withnpx skills add <owner>/<repo> --skill <name>. - Subagents:
validatorruns the local quality gates in its own context and returns a verdict instead of several thousand lines;prompt-skill-syncjudges whether a published audit's two halves still aim at the same outcome, repairs a divergence, and returns a verdict instead of two long files. - Hooks (
.claude/hooks/):markdown-audit-reminderrestates the doc-authoring rules whenever you edit a markdown file;prompt-skill-syncnames the counterpart when you edit either half of a published audit;validate-gatetracks which gates have run and blocks the first attempt to finish while any are outstanding.