Guidelines for Claude Code and other coding agents working on projects.
- Don't Repeat Yourself (DRY) — extract shared logic into reusable functions/modules
- Single Responsibility Principle — each function/module/class does one thing well
- Use design patterns wisely — don't over-engineer; apply patterns when they solve a real problem
- Favor composition over inheritance
- Keep functions pure where possible — predictable inputs/outputs, minimal side effects
- Prefer explicit over implicit — no magic values, no hidden state mutations
- Validate at boundaries — sanitize and type-check all input from users, APIs, files, env vars, and message queues; trust internal calls
- Fail fast — throw early with clear, actionable errors rather than letting bad state propagate
- Use runtime validation for external data — schema validators (e.g. Zod) for API payloads, config, and parsed JSON; don't rely on TypeScript types alone
- Distinguish operational vs programmer errors — operational (network down, bad input) → handle and recover; programmer (bug, invariant broken) → crash loudly
- Never swallow errors silently — log with context, rethrow, or escalate; an empty
catch {}is almost always a bug - Guard against
nullandundefined— use optional chaining, nullish coalescing, and explicit checks at boundaries; prefer non-nullable types internally - Handle the unhappy path explicitly — timeouts on external calls, retries with backoff for transient failures, circuit breakers where appropriate
- Make invalid states unrepresentable — discriminated unions over boolean flags, branded types for IDs, exhaustive
switchwithneverchecks - Treat external data as untrusted — escape/sanitize before rendering, parameterize queries, validate file paths against traversal
- Prefer immutability —
readonlytypes, avoid mutating function arguments, return new objects instead of mutating in place - Idempotent operations where possible — safe to retry; critical for webhooks, queue consumers, and migrations
- Defense in depth — don't rely on a single layer (e.g. UI validation + API validation + DB constraints)
- Principle of least privilege — minimal scopes for tokens, narrowest possible types, smallest possible API surface
- Assert invariants — use assertions or guard clauses for conditions that must hold; document the "why" when non-obvious
- Done is better than perfect — ship incremental value, iterate later
- Functions can have impact on other functions — consider side effects and coupling when making changes
- Code and product quality is always important — clean code, readable diffs, meaningful naming
- Bring value for the user — every feature and fix should serve a real user need
- Keep dependencies minimal — only add packages that solve a clear problem
- Write code for the next developer — clear naming, comments where "why" isn't obvious
- Small, focused commits — one logical change per commit with a descriptive message
- Refactor as you go — leave code better than you found it (Boy Scout Rule)
- Unit tests — for pure functions and small code blocks with clear input → output
- Integration tests — for API routes; test the full request/response cycle including middleware, auth, and database
- E2E tests — out of scope for now
- Write tests before fixing bugs — reproduce the bug first, then fix it
- Test the unhappy path — invalid inputs, missing data, auth failures, network errors
- Keep tests independent — no shared mutable state between test cases
- Use descriptive test names that explain the expected behavior
- Coverage targets — aim for meaningful coverage, not a number for its own sake:
- Business logic / pure functions / utilities: ≥ 85% — these are cheap to test and high-value
- API routes / services: ≥ 75% — cover happy path + key error/auth branches
- UI components: ≥ 50% — focus on logic-heavy components, hooks, and reducers; skip trivial presentational components
- Overall project target: ≥ 70% for UI-heavy projects, ≥ 80% for backend-only projects
- Coverage is a floor, not a ceiling — 100% coverage with weak assertions is worse than 70% with meaningful ones
- Don't chase coverage on generated code, type definitions, or thin wrappers — exclude them from the report
Apply defense in depth — multiple independent layers, no single point of failure. Security is everyone's responsibility, not a final checklist.
- Never trust input — validate, sanitize, and escape data crossing any trust boundary (user, network, file, IPC)
- Least privilege — minimal permissions for tokens, service accounts, DB users, file access, container capabilities
- Secure by default — dangerous behavior should be opt-in, never opt-out
- Don't roll your own crypto — use vetted libraries (
argon2,bcrypt,jose, platformcrypto); never invent hashing or encryption - Fail closed — on error or unexpected state, deny access rather than allow
- Log security events — auth failures, permission denials, rate-limit hits — but never log secrets, tokens, passwords, or full PII
- Keep dependencies current — outdated packages are the most common attack vector
- Threat-model new features — for any feature touching auth, payments, or user data, ask "what's the worst an attacker could do here?" before shipping
- Never commit secrets —
.env, API keys, tokens, certs belong in a secret manager (1Password, AWS Secrets Manager, Vault, Doppler), not the repo - Add
.env*to.gitignore; commit.env.examplewith placeholder values and inline docs - Rotate secrets on a schedule and immediately on suspected exposure
- Prefer short-lived tokens (OAuth, JWT with short TTL) over long-lived API keys
- Scope credentials narrowly — read-only where possible, per-environment, per-service
- Run a secret scanner in CI and as a pre-commit hook (see Automated Scanning below)
- Authentication — use battle-tested libraries (Auth.js, Clerk, Lucia, Passport); never build your own login flow
- Authorization — check permissions on every request, server-side; never trust the client to enforce access
- HTTPS only — enforce TLS 1.2+, set HSTS, redirect HTTP → HTTPS
- Security headers — CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy (use Helmet on Node)
- CORS — explicit origin allowlist; never
*for authenticated endpoints - CSRF —
SameSite=Lax/Strictcookies + CSRF tokens for state-changing requests from browsers - SQL injection — parameterized queries / ORMs only; never string-concatenate SQL
- XSS — rely on framework auto-escaping (React, Vue, Svelte); sanitize raw HTML with DOMPurify; avoid
dangerouslySetInnerHTML/v-html - SSRF — validate and allowlist outbound URLs; never let user input drive server-side fetches unfiltered
- Rate limiting — per-IP and per-user; protect login, signup, password reset, and expensive endpoints
- File uploads — validate type & size, store outside web root, regenerate filenames, scan for malware
- Sessions —
httpOnly,secure,sameSitecookies; rotate session IDs on login and privilege change
- Pin TLS certificates for high-trust API calls
- Store secrets in OS keychain / Keystore — never in plaintext config or
localStorage - Validate deep links and URL handlers — they're an attack surface
- Code-sign all release builds; verify signatures on update channels
- Obfuscate / minify production builds, but treat client code as public — never embed secrets
- Validate and sanitize any path, command, or shell argument before passing it to
exec,spawn, or the filesystem - Avoid
shell: truein child processes — use argv arrays - Be careful with
eval,Function(), dynamicrequire, and YAML/JSON deserializers on untrusted input
- Encrypt sensitive data at rest (DB-level or column-level for PII, secrets, tokens)
- Encrypt all data in transit (TLS 1.2+, mTLS for service-to-service where feasible)
- Minimize PII collection — only collect what you need; delete what you no longer need
- Hash passwords with
argon2id(preferred) orbcrypt(cost ≥ 12) — never MD5, SHA1, or plain SHA256 - Mask sensitive values in logs, error messages, and analytics events
Run these in CI and locally. Catching issues before merge is cheaper than catching them in production.
GitHub-native (enable in repo settings):
- Dependabot — automated dependency updates + vulnerability alerts
- CodeQL — GitHub's SAST engine; add via workflow template (Settings → Code security → Set up CodeQL)
- Secret scanning + push protection — blocks committing known secret patterns; free for public repos, available on Advanced Security for private
GitHub Actions add-ons:
- Semgrep (
returntocorp/semgrep-action) — fast, customizable SAST with broad language coverage - gitleaks (
gitleaks/gitleaks-action) — secret scanning across full git history - trufflehog (
trufflesecurity/trufflehog) — secret detection with credential verification - Trivy (
aquasecurity/trivy-action) — container, IaC, and dependency scanning - Snyk (
snyk/actions) — dependency, container, and IaC scanning (free tier available) - OWASP ZAP (
zaproxy/action-baseline) — DAST against a deployed preview environment
Pre-commit / pre-push (local — wire via husky + lint-staged):
gitleaks protect --staged— block secrets before they leave the dev machineeslintwitheslint-plugin-security— flag common Node.js security anti-patternsbun audit/npm audit --audit-level=high— fail on high/critical vulns on pre-pushtsc --noEmit— many security bugs are also type errors
GitLab → GitHub equivalents:
| GitLab template | GitHub equivalent |
|---|---|
Jobs/SAST.gitlab-ci.yml |
CodeQL workflow + Semgrep Action |
Jobs/Secret-Detection.gitlab-ci.yml |
Secret scanning (native) + gitleaks-action |
Jobs/Dependency-Scanning.gitlab-ci.yml |
Dependabot + Trivy or Snyk Action |
- Enable strict mode in
tsconfig.json—"strict": true - Prefer
constoverlet, avoidvar - Use proper TypeScript types — avoid
any; useunknownwhen the type is genuinely unknown - Handle promises properly — always
awaitor.catch(), never leave unhandled rejections - Use early returns to reduce nesting
- Prefer
async/awaitover raw promise chains - Use named exports over default exports for better refactoring support
- Keep environment config in one place — use a validated config module, not scattered
process.envcalls - Use proper error types — extend
Error, include context, distinguish operational vs programmer errors - Prefer standard library and Node.js built-ins before reaching for npm packages