Skip to main content

From Prose to Guarantees: The Full Claude Code Customization Surface

· 20 min read
Pere Pages
Software Engineer
Claude Code customization surface — from prose instructions to enforced guarantees

Claude Code can be customized along a spectrum: at one end, plain-language instructions you hope the model follows; at the other, hooks and settings the harness enforces whether the model cooperates or not. This post maps that full surface and shows how to actually wire it up on a real project.

The spectrum: from prose to guarantees

Most teams meet Claude Code as a chat box in the terminal and stop there. Then, over a few months, they discover CLAUDE.md and start writing rules into it: never commit on main, no inline styles, keep the locale files in sync, run tests with --run. The file grows. It hits 18 KB. And every one of those rules is still just prose the model reads and chooses to obey — reloaded into every session, competing for attention with everything else in the file.

The uncomfortable truth is that prose is not enforcement. A rule that's deterministic — a branch name, a banned import, a required flag — doesn't need a model to "remember" it. It needs a guarantee. And Claude Code has a whole surface of primitives, each with a different cost and a different strength of guarantee, that most teams never climb past the first rung of.

This post does two things. First, it maps the entire customization surface, not just the handful of primitives everyone quotes. Second, it walks a real (anonymized) migration — a project I'll call Wanderlist, a Next.js + Expo monorepo — that moved its conventions down an enforcement ladder to the cheapest primitive that makes each one reliable. The goal you're aiming for: deterministic invariants become guarantees, and CLAUDE.md shrinks back down to the judgment calls that genuinely need a model.


Part 1 — The map: the whole surface, top down

It helps to see the whole thing before deciding where a given rule belongs. Here's the full surface organized as layers, from what Claude knows down to where it runs. Not everything here is a "primitive" — that word properly means the small extensibility set (skills, subagents, hooks, MCP, and CLAUDE.md, bundled by plugins). The rest is configuration, runtime behavior, and delivery channels. But if you're reasoning about "how do I shape this tool for my project," you want the whole picture.

1. Context — what Claude knows Memory files (CLAUDE.md) across scopes: enterprise, project root, nested subdirectory, and user (~/.claude/CLAUDE.md). Imports via @-references. The distinction that matters: memory is what Claude knows; settings are what Claude can do.

2. Capability — what Claude can do

  • Skills (.claude/skills/): a SKILL.md plus optional supporting files. Auto-invocable (Claude matches on the description) or user-invocable (/skill-name), toggled with disable-model-invocation; allowed-tools and context: fork scope behavior. As of the 2026 releases, the old slash-command system was merged into skills.claude/commands/ still works as a legacy path, and every skill gets a / interface for free.
  • Subagents (.claude/agents/): isolated Claude instances with their own context window, tool access, and optionally their own model and MCP servers. Reach for them for context isolation and parallelism, not for everything.
  • Hooks (in settings.json): shell, large language model (LLM), or HTTP (Hypertext Transfer Protocol) commands fired on lifecycle events such as PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionStart, PreCompact, and ConfigChange. Matchers target tool names; the stdin/stdout protocol lets them block or auto-correct.
  • Model Context Protocol (MCP) servers: external tools over standard I/O (stdio) or a URL (Uniform Resource Locator) with Server-Sent Events (SSE), configured in .mcp.json (project) or ~/.claude/.mcp.json (user), with OAuth or apiKeyHelper auth.
  • Built-in tools and slash commands: /clear, /compact, /context, /agents, /mcp, /hooks, /doctor, /status — the session-hygiene layer.

3. Packaging & distribution Plugins bundle skills + subagents + hooks + MCP + commands into one installable unit, distributed through marketplaces (GitHub, npm, internal registries), namespaced to avoid collisions (/plugin:command). This is a delivery layer, not a new kind of rule.

4. Configuration & governance settings.json, merged across a precedence chain: enterprise-managed → CLI flags → local → project → user (scalars override, arrays concatenate). This is where permissions (allow/deny/ask), permission modes (such as Default, Auto-Accept Edits, Plan Mode, auto, and bypass), the auto-mode safety classifier, sandboxing/network control, environment variables, model selection, attribution, and display (theme, status line, output style) all live.

5. Session & runtime The agent loop, auto-compaction, checkpoints/rewind, background and async agents, worktree isolation, session persistence.

6. Surfaces / channels Interactive command-line interface (CLI), headless CLI (-p, one-shot), VS Code and JetBrains extensions, the desktop app, web (claude.ai/code), mobile, and continuous-integration (CI) paths (GitHub Action, scheduled jobs, pre-commit).

7. Programmatic The Claude Agent software development kit (SDK), for exposing the same loop to your own code.

Read the same seven layers as a pyramid and the point of the section jumps out: the broad, foundational base — layers 1–3 — is where you actually shape conventions, tapering up to a niche apex you rarely touch.

The seven customization layers as a pyramid: a broad foundational base you shape constantly (layers 1–3) narrowing to a niche apex you rarely touch (layer 7).7654321Layer 7 — Programmatic (Agent SDK)Layer 6 — Surfaces / channelsLayer 5 — Session & runtimeLayer 4 — Config & governanceLayer 3 — Packaging (plugins)Layer 2 — Capability (skills, hooks…)Layer 1 — Context (CLAUDE.md, memory)

You will not touch most of layers 4–7 to encode a convention. The decision-making below lives almost entirely in layers 1–3.


Part 2 — The mental model: the enforcement ladder

The map above sorts by location. To decide where a rule belongs, you need a different axis: guarantee strength, ordered by cost. This is the ladder:

When the rule is…Route it to…Its role
judgment / taste, "why we chose this"CLAUDE.md prose / Architecture Decision Record (ADR) · Request for Comments (RFC)record
deterministic, mechanically checkablehookguarantee
a repeatable multi-step procedureskillprocedure
a recurring specialized judgment callcustom subagentreviewer
a large parallel fan-out auditworkflowsweep
a quick manual entry pointslash commandtrigger
a bundle to share across repos/teamsplugindistribution

Two rules make this work in practice.

Route to the cheapest primitive that makes the rule reliable. A deterministic rule dropped into prose is a rule you're hoping the model recalls from an 18 KB file. The same rule as a hook is a rule that cannot be violated without the block firing. That's the whole game: convert hope into guarantee wherever the rule is mechanical enough to allow it.

Primitives are not mutually exclusive. Most decisions get a primary home (the canonical statement) plus enablers layered on top. "Never commit on main" = one CLAUDE.md line (record) + a PreToolUse hook (guarantee). "Add a cached query" = a Caching section in CLAUDE.md (record) + a skill (procedure) + a caching-auditor subagent (audit). Don't stop at the first hit.

The gate before the ladder: the memory admission test

Before any of this, one question decides whether a rule is even a team decision:

Would a different developer on this project need this to know how to do the work correctly?

  • No → it's about how you want the assistant to collaborate or report (process taste, ambition level, communication). That's personal memory, and nothing else.
  • Yes → it's a team decision. It needs a canonical home and maybe enforcement on top. Enter the ladder.

The tell that catches most leaks: a rule dense with code — file paths, symbols, Cascading Style Sheets (CSS) classes, enum values — is almost always a team decision, not personal taste. Those do not belong in a local, un-shared memory store where teammates can't see them. Wanderlist wrote this test into its global ~/.claude/CLAUDE.md so it applies at every memory write, and it caught 14 "memories" that were actually architecture decisions hiding in a personal store (more on that below).


Part 3 — The worked example: migrating Wanderlist down the ladder

Wanderlist's "before" state is probably familiar. A rich set of conventions, almost all of it in writing: an 18 KB root CLAUDE.md, ~17 memory files, dozens of plan docs, structured ADRs. One project skill. Zero hooks, zero custom subagents, zero checked-in project settings — and a local permission allowlist that had accumulated stale absolute paths from before the repo moved directories.

Everything the team had learned was captured. Almost none of it was enforced. Here's how each rung got populated.

Record: CLAUDE.md + ADR/RFC

The record rung never goes away — it's where the why and the judgment-based guidance live. The migration didn't empty CLAUDE.md; it shrank it to the rules that genuinely need a model to interpret them, and split the monolith into nested files so each session only pays for what's relevant:

  • apps/web/CLAUDE.md — web boundaries, caching, slot pattern, route adapters.
  • apps/mobile/CLAUDE.md — Expo/Clerk/NativeWind, including the backend-for-frontend (BFF) parity rule.
  • packages/tokens/CLAUDE.md — single-source-of-truth-in-TypeScript, regenerate-and-check-drift.
  • apps/web/design-system/CLAUDE.md — the tiered component library, CSS layering, and a pointer to the prose single source of truth (SSOT) in docs/.
  • Root keeps only cross-cutting rules. Anything migrated to a hook shrinks to a one-line pointer.

A pure product semantic is the canonical example of something that stays here and nowhere else. Wanderlist has a saved-place state whose meaning is easy to get backwards — call it RECOMMENDED, which means places others recommended to the user (incoming), not places the user recommends outward. That's pure judgment, not mechanically checkable, so it lives in the apps/web/CLAUDE.md domain glossary with the user-interface (UI) copy spelled out. No hook. No skill. Just a clear record.

Guarantee: hooks — the biggest untapped lever

This was the highest-leverage rung for Wanderlist, and it's the one most teams skip entirely. Every hook maps 1:1 to a rule that already existed in CLAUDE.md as prose — the hook just makes it real. All of them block on violation rather than warning, because the highest-value rules (commit-on-main, env leaks, locale desync) are exactly the ones a soft warning gets routinely ignored on.

PreToolUse — block before the action lands:

GuardTriggerAction
Never commit on maingit commit / merge --no-ff / commit --amend / cherry-pick / rebase while on main/masterBlock ("branch first"); honor an explicit "commit on main anyway" escape hatch
Test watch-mode trapnpm test without -- --runBlock, suggest -- --run
Build database trappnpm build (which chains a prisma migrate deploy)Block, suggest npx next build from apps/web/

PostToolUse (Edit/Write) — grep-validate the file just written, block with a fix hint:

  • Inline styles: style={{ in a web .tsx → block.
  • Env access: process.env. outside lib/config.ts (with a small allowlist for NODE_ENV, generated files, tests) → block.
  • Third-party adapter boundary: a raw external application programming interface (API) fetch(...) outside its designated adapter module → block.
  • Deep barrel imports where a module index.ts exists (excluding an explicit barrel-exempt allowlist) → block.
  • Framework-specific footguns: a responsive variant inside a Tailwind @apply, or JavaScript XML (JSX) in a .ts (non-.tsx) file → block.

PostToolUse — the locale-sync guard (the most-missed one). On any edit to messages/*.json, structurally diff the locale files (en, ca, es) and block on shape divergence or invalid JavaScript Object Notation (JSON). This enforces on every edit an invariant the single existing skill only checked during component removals.

Stop / SubagentStop. If web files changed this session, lint the changed files and block on failure — a fast proxy for "the task isn't done until it passes," keeping the full build as a manual step.

The one non-obvious design decision: hooks block in interactive sessions but honor a soft-off env flag in CI / non-interactive runs. A hard block with no override in a headless context wedges your automation with no human to approve the escape hatch. So a APP_HOOKS_SOFT=1 flag (or auto-detecting CI) downgrades any block to a warning in those contexts. Interactive per-rule escape hatches stay as the local override; the env flag is for headless only.

Procedure: skills — codify the repeatable, invariant-heavy operations

The existing remove-component skill already proved the point: an operation touching barrels + i18n + connected wrappers + build is dramatically more reliable as a skill than as "the steps are…" prose. Several siblings had the same shape and no skill. So:

  • add-component — scaffold the folder (content + styles + story + test + barrel), the pure/connected split, seed internationalization (i18n) keys in all locales, and target the story at the pure variant with a folder-mirroring title.
  • add-cached-query — a cached read with its matching cache-tag and invalidation in the same module, sorted-array cache keys, and the mutation-wiring checklist.
  • new-route-or-action — the Server-Action-vs-API-route decision, plus scaffolding a thin shell over a lib/<domain> resolver that returns a discriminated union and logs once at the mapping site.
  • add-i18n-key, promote-to-shared, new-plan / new-adr, cost-check — the rest of the recurring procedures.

Rule of thumb from this migration: if you'd otherwise write "the steps are…" in CLAUDE.md prose, it's probably a skill.

Reviewer: custom subagents — recurring judgment that a regex can't catch

Some rules need reasoning, not a grep. Those became project-tuned reviewers in .claude/agents/:

  • boundary-auditor — module-boundary violations: deep-barrel imports, same-module siblings imported by alias, feature code leaking into the shared component layer.
  • caching-auditor — every mutation traced to its invalidation; unsorted keys; time-to-live (TTL) values where cache tags belong.
  • cost-auditor — per-item query fan-out, eager-vs-lazy loading — operationalizing a real central-processing-unit (CPU) spike postmortem.
  • mobile-parity-checker — verifies mobile doesn't lead web (available data ≠ permission to render).

Note the deliberate overlap with hooks: the shared-layer import rule got both a fast per-edit PostToolUse hook and coverage in boundary-auditor. The hook is the immediate guard; the subagent is the deep sweep. That's not redundancy — it's the same invariant enforced at two different latencies.

Sweep: workflows — the large, parallel, fan-out audits

Several planning docs described inherently fan-out work: sweep every module / route / query, dedup, verify, rank. Those map to multi-agent workflows — fan the relevant auditor across the tree, collect, adversarially verify, produce a ranked report. Wanderlist has five (cost, module boundaries, i18n completeness, mobile-BFF parity, Storybook coverage). They're opt-in and billed — invoked on demand, built after the reviewer subagents they depend on exist.

Honest caveat on this rung. "Workflow" isn't a first-class shipped primitive with an official directory the way skills and subagents are — it's an orchestration pattern you reify (parallel subagent fan-out / agent teams). If you name a .claude/workflows/ convention, document that it's a team convention mapping onto the real mechanism, so nobody greps for a path the harness doesn't natively know about.

Trigger: slash commands — thin manual entry points

Quick wrappers whose logic lives in the skill/subagent/workflow they call: /web-build, /web-check, /ci-local, /tokens, /cost-audit, /boundary-audit. Keep them thin.

Caveat on this rung too: since slash commands merged into skills, treat the distinction as conceptual (a thin trigger vs. the procedure itself), not as two separate directories. .claude/commands/ still works, but new work belongs in .claude/skills/ with a / interface.

Distribution: plugins — only when the bundle needs to travel

Plugins are the layer you reach for when the same skills + hooks + agents need to be shared across repos or teammates. Wanderlist didn't need one yet — everything lives in one monorepo. That's the right call: don't package for distribution until distribution is the actual problem.

Integration: MCP — and why deferring it was correct

Here's a place where knowing the decision router keeps you from over-building. MCP wires in external tools (a Sentry connector for live errors, a read-only Postgres connector for schema introspection). Those are genuinely useful — but they're a capability integration, not a place a convention lands. Wanderlist deferred MCP until database-first / cost work makes live introspection worth the setup. A rules-migration RFC correctly has no MCP rung. (This is the one thing the surface map must include but the decision router rightly ignores.)

Config & hygiene: the unglamorous prerequisite

Two things had to happen for the rest to bite:

  1. A checked-in .claude/settings.json carrying the hook definitions plus a curated allowlist of safe read-only and build/test commands. Previously there was none — all config was personal and local.
  2. Permissions hygiene. Prune the local settings to machine-local entries, delete the stale absolute paths, and tighten a broad Bash(git *) allow into specific safe verbs — so that git commit still routes through the hook instead of being blanket-approved. A too-broad allow silently defeats the guard you just built.

Memory: formalize the escalation path

Finally, the memory store itself got a policy: when a personal memory encodes a deterministic rule, promote it to a hook or skill and leave the memory as a one-line pointer. Applying the admission test surfaced 14 "memories" that were actually team decisions — product semantics, architecture rules, design-system invariants — sitting in a local store invisible to teammates. They were moved out to their proper homes (CLAUDE.md, ADRs, hooks, skills) and only genuinely personal collaboration preferences stayed behind.


Part 4 — Rollout: the order that actually works

You can't do all of this at once, and the sequence matters because some rungs depend on others:

  1. Blocking hooks + checked-in settings.json. Biggest correctness and context-cost win; do it first.
  2. Permissions hygiene. Required for the commit hook to actually fire. Quick.
  3. Skills — the highest-frequency procedures first.
  4. Nested CLAUDE.md split. Shrinks every session's context.
  5. Subagents + slash commands. Cheap, compounding.
  6. Workflows. High value, opt-in — build once the reviewer subagents exist.
  7. MCP. Deferred until it earns its setup.

Notice the shape: enforcement and hygiene first (they pay back immediately and reduce token cost), procedures next, and the heavyweight fan-out tooling last, once its dependencies are in place.


Three things to keep honest about the mechanics

The ladder is a great conceptual model, but a few of its rungs are cleaner in theory than in the current tooling:

  • Workflow is a pattern, not an official path. Reify it as a team convention over agent-teams / subagent fan-out; don't imply a native .claude/workflows/ primitive.
  • Slash commands merged into skills. Keep the conceptual split (trigger vs. procedure) but not the directory split.
  • "Memory" is overloaded. In one framing it means user-scope CLAUDE.md; in another it's a separate non-git personal store. Pick one vocabulary across your docs so a reader always knows which "memory" you mean.

Takeaways

The core move is small and repeatable: for every rule, route it to the cheapest primitive that makes it reliable — and layer enablers on top of a single canonical home. Deterministic rules become hooks (guarantees, not hopes). Repeatable procedures become skills. Judgment calls that a regex can't make become subagents. Fan-out audits become workflows. And CLAUDE.md gets to shrink back down to what it's actually good at: the judgment and the why.

Run the admission test before you write anything down, so team decisions never leak into a personal store nobody else can see. Do the hooks and permissions hygiene first, because they pay back immediately and quiet the context. And don't build the distribution or integration layers (plugins, MCP) until sharing or integration is the real problem in front of you.

The tool has a large surface. You don't have to use all of it. You have to put each rule on the right rung.