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 |