All sections

04Foundations

Rules

Path-scoped instructions in .claude/rules/. A rule with a paths glob costs nothing until Claude touches a matching file, which makes it the correct home for coding conventions in any repository with more than one language.

What they are

Markdown files in .claude/rules/ with optional YAML frontmatter containing a paths: glob list. If paths: is present, the rule enters context only when Claude touches a matching file. If absent, the rule behaves exactly like CLAUDE.md, always loaded.

That single field is the difference between a convention that costs you tokens on every turn forever and one that costs nothing until the moment it is relevant.

Layout

your-repo/
├── CLAUDE.md                    # cross-cutting: build, layout, gotchas
└── .claude/
    └── rules/
        ├── java.md
        ├── go.md
        ├── python.md
        ├── typescript.md
        ├── sql.md
        ├── terraform.md
        ├── tests.md
        └── security.md          # no paths: — always loaded

Worked examples

Go

---
paths:
  - "**/*.go"
---

# Go conventions

- Wrap errors with `fmt.Errorf("doing X: %w", err)`. Never drop the cause.
- Table-driven tests with `t.Run` subtests; `t.Parallel()` where safe.
- `context.Context` is always the first parameter. Never store it in a struct.
- Exported identifiers get doc comments beginning with the identifier name.
- No naked returns in functions longer than five lines.

Java

---
paths:
  - "**/*.java"
---

# Java conventions

- Constructor injection only. No field-level `@Autowired`.
- Repository lookups return `Optional<T>`, never null.
- Checked exceptions do not cross service boundaries; wrap in domain types.
- New metrics register through the shared `MeterRegistry`, not ad hoc.
- Prefer `record` for DTOs. No Lombok in new code.

TypeScript

---
paths:
  - "**/*.{ts,tsx}"
---

# TypeScript conventions

- `strict` is on. No `any`; use `unknown` and narrow.
- Discriminated unions over objects full of optional fields.
- Named exports only. No default exports.
- Zod schemas are the source of truth for external data shapes.

SQL and migrations

Narrow globs let a rule speak with much more authority than a repo-wide one, because it only ever appears when it is unambiguously relevant.

---
paths:
  - "**/migrations/**/*.sql"
---

# Migration conventions

- Forward-only. Never write a down migration; the rollback path is a new
  migration.
- Every `ALTER TABLE ... ADD COLUMN` is nullable or has a default. A table lock
  on a hot table is an outage.
- Index creation uses `CREATE INDEX CONCURRENTLY`, which means it cannot run
  inside a transaction. Put it in its own migration file.
- Filename is `NNNN_verb_noun.sql`, zero-padded to four digits.

Terraform

---
paths:
  - "**/*.tf"
  - "**/*.tfvars"
---

# Terraform conventions

- Modules take explicit `providers`. Never rely on provider inheritance.
- Every resource gets the shared `local.common_tags`.
- No `count`. Use `for_each` so a removal does not re-index the rest.
- State is remote and locked. Never suggest `-lock=false`.
- Anything holding data has `prevent_destroy = true` in its lifecycle block.

Tests only

A glob can target a role rather than a language, which is often more useful.

---
paths:
  - "**/*_test.go"
  - "**/*.test.ts"
  - "**/test_*.py"
---

# Test conventions

- One behaviour per test. If the name needs "and", split it.
- Name tests for the behaviour, not the method: `rejects_expired_token`, not
  `testValidate2`.
- No sleeps. Use the injected clock or a fake.
- Assert on the observable outcome, never on the number of calls to a mock,
  unless the call itself is the contract.

Always-on: security

Omitting paths: deliberately is the right call for a small number of constraints that must be in context regardless of what is being edited.

# Security constraints

- Never log request bodies, auth headers, or anything from `internal/pii`.
- Secrets come from the environment. Never from a committed file.
- Any new outbound HTTP call needs an explicit timeout.
- Do not add a dependency that is not already in the lockfile without saying so
  explicitly in your summary.

Keep the unscoped set very small. Every line here is paid for on every turn of every session, exactly like CLAUDE.md.

Glob gotchas

The most common reason a rule “does not work” is that the glob never matches.

Glob Matches Usually what you meant?
*.go main.go at the repository root only No
**/*.go Every Go file at any depth Yes
src/** Everything under src/ Often
**/*.{ts,tsx} Both extensions Yes
**/migrations/**/*.sql Only SQL inside a migrations directory Yes, for scoped rules
!**/vendor/** Negation is not supported; use narrower positives n/a

If a rule is not appearing in /context after Claude has read a matching file, the glob is the first thing to check, and the missing **/ prefix is the overwhelmingly likely cause.

Scope and merging

Location Applies to
~/.claude/rules/ You, in every project
.claude/rules/ This project, committed, whole team

User rules load before project rules, giving project rules higher priority. But, and this is the recurring theme, both still load. “Higher priority” here means “read later”, not “replaces”. See rules are additive with a soft ordering.

The design principle that follows: your personal rules directory should hold preferences that no project would contradict. Authoritative technical conventions belong in the repository’s committed rules, where every contributor gets them.

When a rule should be something else

Symptom Better mechanism
The rule is a procedure you invoke, not a standing constraint Skill
The rule must hold 100% of the time and can be checked by a script Hook
The rule is really “never touch these files” permissions.deny in settings
The rule describes one directory rather than one file type A nested CLAUDE.md in that directory
The rule is 300 lines of reference material A skill; the body loads only on demand