All sections

14In practice

Recipes

Twelve setups that come up repeatedly, each stated as a problem, the mechanism that solves it, and the configuration to paste. Most of them combine three or four layers, because most real requirements do.

1. Onboarding a new contributor

Problem. New joiners spend their first week asking questions the repository could answer.

Mechanism. CLAUDE.md for facts, a user-invocable: false skill for the architecture narrative, a SessionStart hook for the current state.

.claude/skills/how-this-works/SKILL.md:

---
name: how-this-works
description: Explains the architecture, the request path, and the reasoning behind the unusual parts of this codebase. Use when someone asks how something works, why a design is the way it is, or where a responsibility lives.
user-invocable: false
---

## Request path

Ingress → `gateway` (authn, rate limit) → gRPC → the owning service. Nothing
talks to another service's database. Ever.

## Why the ledger is append-only

We tried mutable balances in 2023 and spent four months reconciling. The
double-entry ledger in `pkg/ledger` is the source of truth; every balance you
see anywhere is a projection and can be rebuilt.

## Why there is no shared "common" module

Deliberate. Shared code between services becomes a coordination point, and a
coordination point becomes a release train. Duplicate small helpers instead.

Setting user-invocable: false matters: this is knowledge, not a command, and it should surface when the question is asked rather than sitting in CLAUDE.md costing tokens on every turn.

2. Incident response

Problem. Under pressure, everyone investigates in a different order and the context window fills with logs before anyone has a hypothesis.

Mechanism. A skill with the runbook and live state inlined, plus a haiku-backed triage subagent so the log volume never reaches your window.

See the /incident skill and the log-triage agent. The pairing is the point: the skill carries the order of investigation, the agent carries the volume.

---
name: incident
description: Opens an incident investigation. Use when triaging a production alert or a customer-reported outage.
allowed-tools: Bash(kubectl *) Bash(git log *) Read Grep
---

Recent deploys: !`kubectl get deploy -o wide --sort-by=.metadata.creationTimestamp | tail -10`

1. Delegate the log reading to the `log-triage` agent. Do not read log files
   into this conversation directly.
2. Correlate its finding against the deploy list above.
3. Report cause, evidence, smallest safe mitigation. Nothing else.

3. A monorepo where each team owns its conventions

Problem. One CLAUDE.md cannot serve six teams, and a shared one becomes a negotiation.

Mechanism. A thin root CLAUDE.md, a nested CLAUDE.md per service that the owning team controls, and path-scoped rules for language conventions that are genuinely global.

platform/
├── CLAUDE.md                     # build, layout, cross-cutting only
├── .claude/rules/*.md            # language conventions, org-wide
└── services/
    ├── billing/CLAUDE.md         # owned by the billing team
    ├── ingest/CLAUDE.md          # owned by the ingest team
    └── search/CLAUDE.md

Add a CODEOWNERS entry per nested memory file. It makes the ownership real, and it stops the root file slowly reabsorbing everything.

4. Guarding migrations and generated code

Problem. Prose asking Claude not to edit generated files works until it doesn’t, and the failure is a silent, plausible-looking diff.

Mechanism. permissions.deny for the absolute cases, a PreToolUse hook for the conditional ones, prose for the reason.

{
  "permissions": {
    "deny": ["Write(**/generated/**)", "Edit(**/generated/**)", "Edit(**/*.pb.go)"]
  }
}

The conditional part — “existing migrations are immutable, new ones are fine” — cannot be a permission glob, because it depends on whether the file exists. That is the guard-paths.sh hook.

5. Review that cannot quietly fix things

Problem. You ask for a review and get a patch. The findings you wanted to think about have already been applied.

Mechanism. A subagent whose tools: omits Write and Edit. The restriction is enforced outside the model, so it holds regardless of what the review concludes.

---
name: reviewer
description: Reviews the working diff and reports findings. Use before opening a pull request.
tools: Read, Grep, Glob, Bash(git diff *), Bash(git log *)
model: opus
---

Report findings, ranked by severity. You cannot edit files.

Every finding needs a `file:line`, a one-line failure scenario with concrete
inputs, and a severity. Skip anything already covered by `.claude/rules/`; the
linter has it. If you find nothing, say so in one line.

Pair it with a Review mode output style when you want a whole session in that posture rather than a single delegated pass.

6. A repository with a slow build

Problem. Every long command times out at the default Bash timeout, and Claude concludes the build is broken.

Mechanism. Settings, not prose.

{
  "env": {
    "BASH_DEFAULT_TIMEOUT_MS": "600000",
    "BASH_MAX_OUTPUT_LENGTH": "60000",
    "API_TIMEOUT_MS": "1200000"
  }
}

Then say so in CLAUDE.md, because Claude cannot see the setting and will otherwise pre-emptively suggest running a subset:

- A full `make build` takes about six minutes. That is expected; the Bash
  timeout is configured for it. Do not split it into partial builds.

7. Keeping secrets out of the context window

Problem. An instruction not to read .env is a request. Reading it once puts it in the transcript permanently.

Mechanism. permissions.deny on Read.

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(**/secrets/**)",
      "Read(**/*.pem)",
      "Read(**/id_rsa*)"
    ]
  }
}

Put this in the committed settings.json, not the local one. It protects everyone who clones the repository, and there is nothing sensitive in the rule itself.

8. A spike repository with different rules

Problem. Your global “always write tests first” is correct 95% of the time and wrong in a throwaway prototype, and the contradiction makes both instructions unreliable.

Mechanism. Rewrite the global rule as a conditional rather than trying to override it.

- Write tests before implementation unless the project's CLAUDE.md says
  otherwise.
# spike-recsys

Throwaway prototype. No tests, no error handling beyond crashing, no
abstractions. If this survives, it gets rewritten rather than cleaned up.

This is the single highest-value habit in the whole system. See why overriding does not work.

9. Enforcing conventional commits

Problem. Asking for a commit format in CLAUDE.md gets you the right format most of the time.

Mechanism. A hook, because the check is mechanical.

#!/usr/bin/env bash
# .claude/hooks/commit-format.sh
cmd=$(jq -r '.tool_input.command // empty')
[[ "$cmd" != git\ commit* ]] && exit 0

if ! grep -qE '(feat|fix|chore|docs|refactor|test|perf)(\([a-z-]+\))?!?: ' <<<"$cmd"; then
  echo "Commit message must be conventional: type(scope): subject" >&2
  exit 2
fi
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": ".claude/hooks/commit-format.sh" }]
      }
    ]
  }
}

10. An open source repository with untrusted contributors

Problem. A committed CLAUDE.md is instructions to anyone’s agent, and a forked repository carries its instructions with it.

Mechanism. Keep the committed configuration descriptive rather than imperative, and keep enforcement in CI where it belongs.

Do Don’t
Document the build, the layout, the non-obvious invariants Put credentials, internal hostnames, or ticket URLs in a committed file
Use permissions.deny for paths nobody should edit Rely on prose to prevent anything security-relevant
Keep the CI checks authoritative Assume a contributor’s agent read your CLAUDE.md at all

The reverse also matters: when you fork someone else’s repository, you inherit their CLAUDE.md and their .claude/ tree invisibly. Read both before your first session.

11. A regulated environment

Problem. Some constraints must hold for every engineer, and cannot be something an individual can turn off.

Mechanism. Managed policy: /etc/claude-code/managed-settings.json and /etc/claude-code/CLAUDE.md, deployed by IT.

Property Consequence
Managed settings outrank everything, including CLI flags A deny here cannot be lifted locally
Managed memory cannot be excluded claudeMdExcludes does not apply to it
Managed agents outrank project agents A mandated review agent cannot be shadowed

Keep the managed layer small. Every line in it is loaded by every engineer in every session, and it is the one layer nobody can trim.

12. Reducing permission prompts without going permissive

Problem. Approving git status for the ninetieth time trains you to approve without reading, which is the actual risk.

Mechanism. An explicit allow list for read-only and build commands, an ask list for anything outward-facing, and deny for the rest.

{
  "permissions": {
    "allow": [
      "Bash(git status *)",
      "Bash(git diff *)",
      "Bash(git log *)",
      "Bash(make build)",
      "Bash(make test*)",
      "Bash(go test *)",
      "Bash(npm run test*)",
      "Read(**)"
    ],
    "ask": [
      "Bash(gh pr create *)",
      "Bash(git push *)"
    ],
    "deny": [
      "Bash(git push --force *)",
      "Bash(rm -rf *)",
      "Read(./.env*)"
    ]
  }
}

The point is not fewer prompts. It is that the prompts you do see are the ones worth reading.