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.