Project-local files always override this baseline:
CLAUDE.md · AGENTS.md · README* · docs/ · code-conventions.md · .editorconfig
- Read all project-local rules before touching code.
- Read the project's
README,package.jsonscripts,composer.jsonscripts, orMakefilebefore running any build/test command. Never guess commands. - Follow existing patterns unless they violate this baseline or a refactor is explicitly requested.
- Keep diffs small, focused, and easy to review.
- Do not introduce new dependencies or tools unless required or explicitly requested.
- For large projects, keep root
CLAUDE.mdlean. Move detailed domain knowledge into separate files (e.g.agent_docs/,.claude/skills/) and reference them from the root file.
PSR-12 is the universal style reference. Where it is PHP-specific, apply its intent to every other language consistently.
| Rule | Value |
|---|---|
| Indentation | 4 spaces — no tabs |
| Max line length | 120 characters |
| Line endings | LF (Unix) |
| Encoding | UTF-8 without BOM |
| File ending | Single newline at end of file |
| Trailing whitespace | Never |
| Imports / includes | Ordered, no unused entries |
| Braces | Consistent, readable — no brace gymnastics |
| Operators / keywords | Consistent whitespace around them |
| One action per line | No clever dense one-liners |
If a language's tooling cannot reproduce PSR-12 brace placement exactly, enforce the closest consistent equivalent and prioritize readability and consistency.
Opening braces for classes, methods, and functions go on their own line — matching PHP PSR-12 brace placement. Control structures (if, for, while, etc.) keep the opening brace on the same line.
// ❌ Bad — opening brace on same line as function/class declaration
class UserService {
public getUser(id: number): User {
if (id <= 0) {
throw new Error('Invalid ID');
}
return this.repository.find(id);
}
}
// ✅ Good — opening brace on own line for class, method, function
class UserService
{
public getUser(id: number): User
{
if (id <= 0) {
throw new Error('Invalid ID');
}
return this.repository.find(id);
}
}This applies to all standalone function declarations, class declarations, and method definitions. Arrow functions and callbacks are exempt — they follow standard JS/TS conventions.
Never trust user input. Always validate input and escape output where applicable.
- Input validation everywhere: Validate all user input on both frontend and backend. Validate type, length, format, and allowed values. Reject invalid input early with guard clauses.
- Output escaping: Escape all user-controlled data before rendering in HTML, JavaScript, SQL, shell commands, or any other output context.
- Secure database queries: In any database context (relational, graph, document, etc.), always use the secure query mechanisms provided by the database driver — prepared statements, parameterized queries, ORM/ODM query builders, or equivalent. For graph databases (e.g. Neo4j), use parameterized Cypher queries. Never concatenate user input into query strings, regardless of database technology.
- Injection prevention: Avoid any form of injection — SQL injection, command injection (never pass user input to shell commands), XSS, LDAP injection, template injection, header injection, etc.
- API input validation: Validate and escape all data received from public APIs and webhooks. External data is untrusted data.
- Frontend + backend validation: If there is a frontend and backend, validate and escape user input on both sides. Frontend validation is for UX; backend validation is for security. Never rely on frontend validation alone.
- Rate limiting: Use API rate limiting mechanisms on all public-facing endpoints (authentication, webhooks, API routes) to prevent abuse and brute-force attacks.
- Token / secret handling: Encrypt sensitive tokens at rest (e.g. OAuth tokens). Never log secrets or include them in error messages. Never commit secrets to version control.
- CSRF protection: Use CSRF tokens on server-rendered HTML forms (login, contact, admin panels). For JSON APIs, CSRF tokens are unnecessary and counterproductive — instead rely on
SameSite=Laxcookies, Origin/Referer header validation,Content-Type: application/jsonenforcement, and custom auth headers (which browsers cannot set cross-origin). Never add token-based CSRF to pure API endpoints — it breaks parallel requests and creates chicken-and-egg problems with SPAs. - Authentication hardening: Log failed login attempts. Use constant-time comparison for secrets and tokens.
| Area | Rule |
|---|---|
| User input | Validate type, length, format, allowed values |
| Database queries | Use secure driver mechanisms (prepared statements, parameterized queries, etc.) |
| HTML output | Escape with context-appropriate encoding |
| Shell commands | Never pass user input; use safe APIs instead |
| API endpoints | Rate limiting on all public routes |
| Webhooks | Verify signatures, validate payload structure |
| Frontend forms | Validate on client AND server |
| OAuth tokens | Encrypt at rest, never log |
| Error messages | Never expose internal details or stack traces to users |
Every variable, parameter, return value, and field must have an explicit, precise type.
Implicit or inferred types are only acceptable where the type is unambiguous and the language makes inference the idiomatic norm (e.g. const x = 1 in TypeScript is number — fine). Ambiguous cases always get an explicit annotation.
| Language | Rule | Example |
|---|---|---|
| TypeScript | No any. No var. const by default, let only when reassignment is needed. Explicit return types on exported functions. |
const count: number = 1; |
| JavaScript | No var. Prefer const. Migrate to TypeScript when feasible. |
const label = 'active'; |
| PHP | declare(strict_types=1); in every file. Native type declarations on params, returns, properties. Docblocks only as supplements. |
function add(int $a, int $b): int |
| C# / .NET | Prefer explicit types to var. Respect nullable reference types. Never suppress nullable warnings without justification. |
int count = 1; |
| Python | Type hints on all function signatures. Use mypy or pyright in strict mode where the project allows. |
def add(a: int, b: int) -> int: |
| Bash / Shell | Declare variable types with declare -i (integer), -r (readonly), -a (array) where applicable. Always quote variables. |
declare -i count=1 |
| SQL | Explicit column types in DDL. Never use SELECT * in application queries. |
created_at TIMESTAMP NOT NULL |
| Vue / Svelte | TypeScript as source language, compiled to JavaScript by the bundler. <script setup lang="ts"> / <script lang="ts">. No TS in HTML templates. |
see Section 8 |
catch (e: unknown)— always narrow safely before usinge.unknown+ narrowing instead ofany.- Generics over loose union types where appropriate.
- Exported / public APIs must always have explicit types.
- No implicit
anyviatsconfig("strict": trueminimum).
Never return null when a neutral empty value exists.
- Return empty arrays
[], empty strings'', or zero values instead ofnull. - Constructor dependencies must always be required (non-nullable). No
?Type $dep = nullfor injected services. - Nullable is only acceptable when
nullcarries a genuinely distinct semantic meaning that an empty value cannot represent.
| Language | Do | Don't |
|---|---|---|
| PHP | function find(): array |
function find(): ?array |
| PHP | private CacheInterface $cache |
private ?CacheInterface $cache = null |
| TypeScript | function find(): Post[] |
function find(): Post[] | null |
| Python | def find() -> list[Post] |
def find() -> Optional[list[Post]] |
- Use
readonlyproperties (PHP 8.1+),const(JS/TS),final(Java/PHP classes not designed for extension). - Prefer constructor promotion with
readonlyfor value objects and DTOs. - Mutate only when genuinely required — prefer creating new instances over modifying existing ones.
// ✅ Good — immutable value object
final class Money
{
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {
}
}Use language-native enums instead of string or integer constants for finite sets of values.
// ❌ Bad
const STATUS_ACTIVE = 'active';
const STATUS_INACTIVE = 'inactive';
// ✅ Good
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
}Never use empty() in PHP. It hides type information and silently coerces values. Always use explicit, type-safe checks instead.
| Type | Do | Don't |
|---|---|---|
| Array | count($items) > 0 / count($items) === 0 |
empty($items) / !empty($items) |
| String | $name !== '' / $name === '' |
empty($name) / !empty($name) |
| Int / Float | $count !== 0 / $count > 0 |
empty($count) |
| Bool | $flag === true / $flag === false |
empty($flag) |
| Null check | $value === null / $value !== null |
empty($value) |
Always use strict comparison (===, !==). Never use loose comparison (==, !=).
// ❌ Bad — hides type, unclear intent
if (!empty($posts)) { … }
if (empty($title)) { … }
// ✅ Good — explicit, type-safe
if (count($posts) > 0) { … }
if ($title === '') { … }This applies to all languages: always use the most specific, type-aware comparison available rather than loose truthiness checks.
Readability over cleverness — always.
- Guard clauses first: Return early to avoid deep nesting.
- Single responsibility: One function does one thing.
- Small functions: A function should fit on one screen (~30–40 lines maximum). If it does not, split it.
- No magic numbers or strings: Use named constants or enums.
- No premature abstraction: Add layers only when they demonstrably reduce complexity.
- Meaningful names: Variables, functions, and classes must reveal intent. No unexplained abbreviations.
- No dead code: Remove commented-out code; use version control instead.
- get vs find naming:
getX()expects the result to exist and throws on failure.findX()is a lookup that may return an empty result (empty array, empty string). Never mix these semantics. - Boolean method names: Methods returning
boolmust start withis,has,can,should, orwas— e.g.isActive(),hasPermission(),canEdit().
// ❌ Bad
function p(u: any, t: string) {
if (u !== null) { if (u.active) { if (t === 'x') { return 42; } } }
}
// ✅ Good
const PERMISSION_DENIED = 403;
function resolvePermission(user: User, type: PermissionType): number
{
if (!user.active) {
return PERMISSION_DENIED;
}
if (type !== PermissionType.Admin) {
return PERMISSION_DENIED;
}
return HttpStatus.Ok;
}Keep cyclomatic complexity low. Aim for ≤ 5 per function, hard limit 10 — if a function exceeds this, refactor before adding more logic.
- Prefer flat over nested.
- Prefer composition to inheritance where it reduces branching.
- Avoid boolean flag parameters — use separate functions or strategy patterns instead.
- Avoid deeply chained calls that obscure control flow.
- Maximum 4 parameters per function/method. Beyond that, introduce a value object, DTO, or options object.
- No mixed return types: A method must always return the same type. Never return
stringsometimes andarrayother times.
// ❌ Bad — boolean flag parameter
function render(component: Component, isAdmin: boolean) { … }
// ✅ Good — separate, explicit functions
function renderAdminView(component: Component) { … }
function renderUserView(component: Component) { … }- Thin controllers / routes: Business logic belongs in services or use-cases.
- DTOs / ViewModels at I/O boundaries: Never expose persistence entities directly to the outside.
- Errors are explicit and meaningful: Never silently swallow exceptions. Log or re-throw with context.
- Constructor discipline: Constructors assign dependencies — nothing else. No business logic, no I/O, no HTTP calls. Pure configuration (e.g. setting up a converter) is acceptable.
- All dependencies required: Every injected service must be non-optional. If a class needs a dependency, it must always receive one — no fallback behaviour on missing services.
- Specific exceptions: Throw domain-specific exceptions (e.g.
PostNotFoundException), not generic\RuntimeExceptionor\Exception. Catch specific exceptions, never barecatch (\Exception $e)unless re-throwing.
- Public APIs and non-obvious functions require a concise docblock explaining why, not what.
- Inline comments explain intent, not mechanics. Do not comment the obvious.
- Keep comments up to date with the code — stale comments are worse than none.
- Only refactor as part of a focused, clearly scoped change.
- Never mix refactoring with feature work in the same commit/diff.
- Leave the code measurably better than you found it (Boy Scout Rule).
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"endOfLine": "lf"
}- Forbid
var - Prefer
const - Forbid
any(allowunknown) - Enforce removal of unused imports and variables
- Require explicit types on all exported / public module boundaries
If a project already has ESLint + Prettier configured, do not fight the existing setup. Adjust it minimally to meet the rules above.
- TypeScript is the source language — it must always be compiled down to JavaScript. No raw TypeScript is ever shipped or embedded directly in HTML.
- Component script blocks use
<script setup lang="ts">(Vue) /<script lang="ts">(Svelte) so the bundler/compiler (Vite, SvelteKit, etc.) handles the TS → JS compilation step. - The compiled output (
.js) is what gets delivered to the browser — never.tsfiles directly. - No plain
.jscomponent source files — TypeScript is always the authoring language. - Props, emits, and exposed values must have explicit TypeScript types.
- Composables (Vue) / stores (Svelte) follow the same strict typing rules as any other module.
- No TypeScript syntax inside HTML template blocks (
<template>/ markup) — types live exclusively in the<script>block.
Decorators are allowed and encouraged where they reduce boilerplate or complexity (e.g. dependency injection, class-based components, ORMs).
Rules:
- Do not mix decorator modes (legacy
experimentalDecoratorsvs. TC39 stage-3) within a project. - Align
tsconfigand bundler settings to the project's chosen decorator mode consistently. - Decorators must not hide critical business logic — keep decorated classes readable without knowing the decorator internals.
- New or changed core behavior requires tests (unit and/or integration as appropriate).
Unit tests must never call real external APIs. Always use mocked, stubbed, or faked responses that simulate the API's behavior — including edge cases and error scenarios.
- Mock or stub every external dependency (HTTP clients, SDKs, database connections, file systems, mail services).
- Use realistic but fictional response data that mirrors the actual API response structure.
- Always cover edge cases with fictional responses: empty results, error codes (4xx, 5xx), rate limits, timeouts, malformed responses, partial data, pagination boundaries.
- The test suite must run fully offline, fast, and deterministically — no network access, no external state.
// ❌ Bad — calls real API
$response = $httpClient->request('GET', 'https://api.example.com/posts');
// ✅ Good — mocked response with realistic structure
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->method('request')->willReturn(
new MockResponse(json_encode(['posts' => [], 'total' => 0]), ['http_code' => 200])
);This rule applies to all languages and frameworks — PHP, TypeScript, Python, Bash, etc.
- Done means:
- Typecheck passes (
tsc,mypy,phpstan, etc.) - Build passes with no new errors
- All tests pass
- No new linter warnings
- Formatting compliant (Prettier / PSR-12 equivalent)
- No secrets, credentials, or debug output committed
- End-to-end tests pass
- All interactive UI elements are within the visible viewport and reachable by the user (no off-screen, hidden, or unreachable controls)
- No N+1 queries or unbounded result sets introduced (see Section 12)
- Migrations are reversible —
up→down→upverified (see Section 13) - Accessibility: keyboard navigable, labels present, contrast sufficient (see Section 16)
- User-visible strings use translation keys, not hardcoded text (see Section 17)
- Commit messages follow Conventional Commits format (see Section 18)
- Typecheck passes (
- If a project uses DDEV (
.ddev/exists): run PHP / Composer / Node commands inside the container viaddev exec …orddev ssh, unless project docs say otherwise. - Do not install global tools on the host if the project provides a containerised equivalent.
Explicit autonomy limits. These override any implicit assumptions.
- Run linter, formatter, and typecheck before considering work done.
- Run tests before committing.
- Work in
src/,tests/,app/,lib/and other source directories. - Read existing tests and copy their style when writing new ones.
- Adding or removing dependencies.
- Database schema changes or new migrations.
- Changing CI/CD pipeline configuration.
- Modifying shared infrastructure, deployment configs, or environment files.
- Deleting files outside the immediate scope of the task.
- Changing authentication or authorization logic.
- Commit secrets, credentials, API keys, or
.envfiles. - Modify
vendor/,node_modules/, or other managed directories. - Edit production configuration files directly.
- Bulk-update lockfiles (
composer update/npm updatewithout a specific package). - Force-push to
mainormaster.
Write code that performs well by default. Fix performance problems when you see them.
- No N+1 queries: Never execute database queries inside loops. Use eager loading, joins, or batch fetches.
- Index awareness: Every
WHERE,JOIN, andORDER BYcolumn should have an index or a documented reason why not. - Limit result sets: Always paginate or limit queries that could return unbounded rows.
- Flag O(n^2) and worse: If a nested loop operates on collections that grow with input, use hash maps, sets, or sorted structures instead.
- Avoid redundant computation: Do not recalculate values inside loops that could be computed once outside.
- Lazy load routes, heavy components, and images that are not immediately visible.
- Code split vendor libraries from application code.
- No unoptimised assets: Compress images, use modern formats (WebP/AVIF), set appropriate cache headers.
- Clean up subscriptions: Remove event listeners, clear timers, and unsubscribe observables in component teardown / destructors.
- Avoid unbounded caches: Every in-memory cache must have a maximum size or TTL.
- Stream large data: Process large files, datasets, or API responses as streams — never load entirely into memory.
- One purpose per migration: Each migration does exactly one thing. Name it descriptively:
add_user_email_index,create_orders_table,drop_legacy_status_column. - Bidirectional required: Every migration must include both
upanddown(or equivalent rollback). If a rollback is genuinely impossible, document why in a comment inside the migration. - No data manipulation in structural migrations: Separate schema changes from data transformations.
- Test migrations: Run
upthendownthenupagain to verify idempotency before committing.
- Explicit column types: Every column has a precise type and nullability constraint.
- Foreign keys enforced: Use database-level foreign key constraints, not just application-level checks.
- Timestamps by default: Include
created_atandupdated_aton every table unless there is a documented reason not to. - Soft deletes: Prefer a
deleted_attimestamp over hard deletes for user-facing data. Hard deletes are acceptable for ephemeral or system-internal data.
- Verify existence: AI tools sometimes hallucinate package names. Always verify a package exists before adding it.
- Evaluate before adding: Check maintenance activity, security history, dependency footprint, and community health. Prefer well-maintained packages with few transitive dependencies.
- Pin exact versions for application dependencies. Use ranges only for libraries intended for redistribution.
- One dependency per commit: Adding a dependency is its own change — never bundle it with feature work.
- Never bulk-update: Always update one package at a time with an explicit version (
composer require vendor/package:^2.1,npm install package@4.2.0). - Read changelogs: Before updating, check the changelog for breaking changes.
- Lockfile discipline: Never manually edit lockfiles (
composer.lock,package-lock.json,yarn.lock). They are generated artifacts.
- Verify unused: Search the entire codebase for imports/usages before removing a dependency.
- Remove completely: Remove from manifest, lockfile (via tool), and any configuration or documentation that references it.
- JSON format: Use structured JSON logging in all backend services. Never use unstructured string concatenation for log messages.
- Correlation IDs: Every request must carry a correlation ID (trace ID) that propagates through all service calls and appears in every log entry.
- Standard fields: Every log entry must include at minimum:
timestamp,level,component,message,correlationId. - Log levels: Use levels consistently —
errorfor failures requiring attention,warnfor degraded but functional states,infofor significant business events,debugfor development diagnostics (never in production).
- Never log secrets: Passwords, tokens, API keys, session IDs, and encryption keys must never appear in logs.
- Sanitise PII: Mask or redact personally identifiable information (email, phone, IP, names) before logging. Log user IDs instead of personal data.
- No request/response body dumps: Log metadata (status, duration, endpoint) — not full payloads.
- Attach context: Error reports must include correlation ID, user ID (anonymised), breadcrumbs (recent actions), and relevant metadata.
- Separate environments: Use distinct error tracking projects/channels for development, staging, and production.
- Source maps: Enable source maps in error tracking for compiled/minified code so stack traces are readable.
- Semantic HTML first: Use native HTML elements (
<button>,<nav>,<main>,<table>,<label>) before reaching for<div>or<span>with ARIA roles. - Keyboard navigable: Every interactive element must be reachable and operable via keyboard alone. Never remove focus outlines (
outline: none) without providing a visible alternative. - Labels and ARIA: Every form input must have an associated
<label>. Usearia-labeloraria-labelledbyonly when a visible label is not possible. - Alt text: Every
<img>must have analtattribute. Decorative images usealt="". Informative images describe the content. - Colour contrast: Text must meet WCAG AA contrast ratios (4.5:1 for normal text, 3:1 for large text). Never convey information through colour alone.
- Focus management: After navigation, modal open/close, or dynamic content changes, move focus to the appropriate element.
- Custom widgets when a native HTML element does the job.
- Hover-only interactions without keyboard/touch equivalents.
- Colour as the sole indicator of state (error = red only, success = green only).
- Disabled buttons without explanation — prefer keeping buttons enabled and showing validation on submit.
- Auto-playing media without user consent.
Apply these rules whenever a project supports or may support multiple languages.
- Key-based translations: Use identifiers (
common.buttons.save), never embed user-visible strings as source text. - No string concatenation: Never build translated strings by concatenating parts. Use parameterised interpolation (
Hello, {{name}}). - Pluralisation: Use the translation framework's plural rules (
_zero,_one,_otheror equivalent). Never useif count === 1to switch strings. - Locale-aware formatting: Use
Intl.NumberFormat,Intl.DateTimeFormat, or equivalent library functions for numbers, dates, and currencies. Never format manually. - CSS logical properties: Use
margin-inline-start,padding-block-end, etc. instead ofmargin-left,padding-bottomto support RTL layouts. - No hardcoded locale assumptions: Date formats, number separators, currency symbols, and text direction must come from locale configuration — never hardcode them.
- Translation coverage: Every user-visible string must have a translation key. Run coverage checks to detect missing keys across locales.
All commit messages follow Conventional Commits:
type(scope): description
[optional body]
| Type | When to use |
|---|---|
feat |
New feature or capability |
fix |
Bug fix |
docs |
Documentation only |
refactor |
Code change that neither fixes a bug nor adds a feature |
perf |
Performance improvement |
test |
Adding or correcting tests |
chore |
Build process, tooling, dependency updates |
style |
Formatting, whitespace — no logic change |
- Subject line: Maximum 50 characters, imperative mood ("add", not "added" or "adds").
- Body: Wrap at 72 characters. Explain why, not what.
- Scope: Optional but encouraged. Use the module, component, or area name.
type/short-description
Examples: feat/user-auth, fix/cart-total-rounding, refactor/order-service.
- Lowercase, hyphens as separators.
- Type prefix matches Conventional Commits types.
- Keep it short but descriptive.
- Never commit directly to
main/master— use feature branches - squash your commits before merge to maintain a clear history (unless project conventions say otherwise)
- use merge strategy instead of rebase most projects are intended for teamwork, so rebase would fail
- One logical change per commit — do not mix unrelated changes
- Delete branches after merge — no stale branches