All sections

13In practice

Worked example: a polyglot repository

A monorepo with Java services, Go tooling, Python data jobs and a TypeScript frontend. Every mechanism in one place, with the reasoning for each placement.

Layout

platform/
├── CLAUDE.md                          # committed — project facts
├── CLAUDE.local.md                    # gitignored — your machine
├── .gitignore                         # contains CLAUDE.local.md, settings.local.json
├── .claude/
│   ├── settings.json                  # committed — env, permissions, plugins
│   ├── settings.local.json            # gitignored — your overrides
│   ├── rules/
│   │   ├── java.md                    # paths: **/*.java
│   │   ├── go.md                      # paths: **/*.go
│   │   ├── python.md                  # paths: **/*.py
│   │   ├── typescript.md              # paths: **/*.{ts,tsx}
│   │   ├── migrations.md              # paths: **/migrations/**/*.sql
│   │   └── security.md                # no paths: — always on
│   ├── skills/
│   │   ├── api-review/SKILL.md
│   │   ├── release-notes/SKILL.md
│   │   └── deploy/SKILL.md            # disable-model-invocation: true
│   ├── agents/
│   │   ├── log-triage.md
│   │   └── test-analyst.md
│   ├── hooks/
│   │   ├── format.sh
│   │   └── guard-paths.sh
│   └── output-styles/
│       └── review-mode.md
├── services/
│   ├── billing/CLAUDE.md              # lazy — Java service specifics
│   └── ingest/CLAUDE.md               # lazy — Go service specifics
└── web/CLAUDE.md                      # lazy — frontend specifics

Root CLAUDE.md, kept deliberately short

# platform

Monorepo. Java services, Go tooling, Python jobs, TypeScript frontend.

## Commands

| Task | Command |
|---|---|
| Build everything | `make build` |
| Test one service | `make test SVC=billing` |
| Lint everything | `make lint` |
| Local stack | `make dev-env` |

## Layout

- `services/`  deployable services, one directory each
- `tools/`     Go CLIs, not deployed
- `jobs/`      Python batch jobs
- `web/`       TypeScript frontend
- `proto/`     shared schemas. **Generated code is never edited by hand.**

## Rules of the road

- Language conventions live in `.claude/rules/`. They load automatically.
- Cross-service changes need an entry in `docs/decisions/`.
- Never edit anything under `**/generated/`.

@./docs/architecture.md

Note what is not here: no Java style guide, no Go error-handling convention, no test-naming rules. Those are in .claude/rules/, where they cost nothing during a TypeScript session.

.claude/settings.json, committed

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "env": {
    "JAVA_HOME": "/usr/lib/jvm/java-21-openjdk",
    "MAVEN_OPTS": "-Xmx2g",
    "GOFLAGS": "-mod=mod",
    "GOPRIVATE": "github.example.internal/*",
    "PYTHONPATH": "./jobs/src",
    "BASH_DEFAULT_TIMEOUT_MS": "300000"
  },
  "permissions": {
    "allow": [
      "Bash(make *)",
      "Bash(go test *)",
      "Bash(git status *)",
      "Bash(git diff *)"
    ],
    "deny": [
      "Write(**/generated/**)",
      "Edit(**/generated/**)",
      "Read(**/secrets/**)",
      "Bash(git push --force *)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": ".claude/hooks/guard-paths.sh" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": ".claude/hooks/format.sh" }]
      }
    ]
  }
}

.claude/settings.local.json, gitignored

{
  "env": {
    "GITHUB_TOKEN": "ghp_...",
    "JAVA_HOME": "/opt/homebrew/opt/openjdk@21"
  },
  "outputStyle": "Concise"
}

Your JAVA_HOME beats the committed one because local outranks project. Your token never enters git.

A nested memory file

services/billing/CLAUDE.md loads only when Claude opens something in that directory, so it can afford to be specific.

# billing

Spring Boot 3.3 on Java 21. Owns the subscription lifecycle; invoicing lives in
`services/invoicing` and is reached over the internal gRPC API only.

## Non-obvious

- `BillingClock` is injected everywhere. Nothing calls `Instant.now()`.
- Provider webhooks land in `WebhookController` and are idempotent by
  `provider_event_id`. Never make one non-idempotent, and never add a side
  effect outside the transaction.
- `SubscriptionState` is a state machine in `domain/`. New states need a
  transition test; the enum alone is not enough.
- Integration tests use Testcontainers, so they need a running Docker daemon.
  `make test SVC=billing` starts one.

The enforcement layer

Prose says “never edit generated code”. The permissions.deny above makes it true, and the guard-paths.sh hook adds an explanation when it fires. Belt and braces: the prose explains why, the permission guarantees what, and the hook tells Claude what to do instead.

That triple is worth applying to every genuinely load-bearing constraint:

Layer Role Example
Prose Explains the reason, so Claude routes around the constraint intelligently “Generated code comes from proto/; regenerate rather than edit”
Permission Makes the violation impossible Write(**/generated/**) in deny
Hook Supplies the remedy at the moment of failure “Edit the schema in proto/ and run make generate

Where each decision lands

You want Put it in
Personal preference, every project ~/.claude/CLAUDE.md
Personal preference, one project, uncommitted ./CLAUDE.local.md
Team-wide project facts ./CLAUDE.md
Java style guide .claude/rules/java.md with paths:
Go style guide .claude/rules/go.md with paths:
Toolchain env vars .claude/settings.jsonenv
Secrets .claude/settings.local.jsonenv
A repeatable review procedure .claude/skills/api-review/
Something that must never happen permissions.deny or a PreToolUse hook
Noisy investigation work .claude/agents/log-triage.md
A quieter response style outputStyle in settings.local.json
All of the above, shared across repos A plugin

What this buys you

Session Configuration loaded
Editing web/src/App.tsx Root CLAUDE.md, web/CLAUDE.md, typescript.md, security.md
Editing services/billing/**.java Root CLAUDE.md, billing/CLAUDE.md, java.md, security.md
Writing a migration Root CLAUDE.md, migrations.md, security.md

No session ever pays for the other three languages. That is the entire reason the rules directory exists.