Claude Code best practices, from a year of daily use
How I've configured Claude Code as an operating layer rather than a chat window: adaptive MCP profiles, three-tier subagent routing, hooks that enforce instead of remind, a cost-based statusline, and the permission model that replaced blanket bypass.
Most Claude Code advice is about prompts. Almost none of it is about the thing that actually decides whether a session goes well: what is in the context window, and what it cost to put there. Every connected MCP server loads its tool definitions every turn. Every enabled plugin loads its skills and commands the same way. A full stack of 25 MCP servers and a dozen plugins is a permanent tax, and in any given directory most of it is dead weight.
This is what I run after about a year of using Claude Code as the interface to email, calendar, research, writing, automation, and code. The specifics are mine; the mechanics transfer. The code behind most of it is in maven-template and the cross-model agent delegation kit.
Context is the scarce resource
Everything below is one idea applied in different places. Model quality is rarely the bottleneck. What breaks a long session is a context window filled with tool schemas you never call, search results you read once, and file contents that mattered for two turns.
Four levers, in order of how much they buy you:
- Load fewer tools. Per-directory MCP and plugin profiles.
- Load knowledge on demand. Skills instead of always-on rules.
- Let something else do the reading. Subagents and CLI offloading return summaries, not raw material.
- Compact later.
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=75triggers compaction at 75% so you keep headroom before the summary lands.
Where each kind of knowledge belongs
Four places, and putting something in the wrong one is the most common configuration mistake.
CLAUDE.md is always in context, every turn, forever. It should hold only what must be true in every session: communication style, safety rules, the routing preferences that change default behavior. Keep it short enough to read in one sitting.
Rules files (.claude/rules/*.md) auto-load per project. Split project instructions across small focused files instead of growing one enormous CLAUDE.md. Easier to maintain, easier for the model to parse.
Skills load on demand. This is where domain knowledge goes: org context, brand guidelines, program details, procedures. The model reads only each skill’s name and description at startup, then pulls the body in when it decides the skill applies. That progressive loading is why a hundred installed skills cost almost nothing until one fires. My rule: if it’s reference material, it’s a skill. If it changes how the model behaves in every session, it’s a rule.
MCP servers are the expensive tier, because their tool schemas are unconditional. Which is the next section.
Adaptive MCP profiles
A SessionStart hook resolves a profile from the working directory, then toggles MCP servers (the disabled flag in ~/.claude.json) and plugins (enabledPlugins in settings.json) to match. Resolution order: an explicit directory-to-profile map, then auto-detection from file signatures (package.json plus tsconfig.json, pyproject.toml, next.config.*, Cargo.toml, go.mod), then a CLAUDE_PROFILE environment override.
A few profiles I run:
| Profile | Loads | Off |
|---|---|---|
| coding | firecrawl, taskmaster, gemini, context7, playwright, TS/Py LSP | comms, content, n8n |
| content | parallel-search, jina, firecrawl, exa, perplexity, n8n-mcp, linkedin, apify | coding, comms |
| default | parallel-search, gemini | everything else |
| none | nothing | all |
The n8n MCP and its two skill plugins are only useful when building workflows, so they load in one profile and nowhere else. A coding session stops carrying Outlook, Asana, and LinkedIn schemas it will never call.
Two things to know. Changes apply on the next session, because MCPs and plugins load at startup. And language servers are real per-session overhead, so I keep TypeScript, Pyright, Kotlin, and Swift off unless the profile says otherwise.
The same pruning logic applies to plugins generally. Carrying a plugin you don’t use costs context every session. I keep about eleven enabled and the rest installed-but-off. One trap: official Anthropic marketplaces auto-update at startup, but GitHub-sourced marketplaces freeze at the version cached when you added them. That’s how a community plugin goes stale while everything around it stays current. Set "autoUpdate": true on its extraKnownMarketplaces entry, or run /plugin marketplace update <name> by hand.
Subagents are compression, not parallelism
The usual framing is that subagents make things faster. They do, sometimes. The reason to reach for one is that a task will generate intermediate noise you don’t want in the main context. A researcher runs five searches and returns 300 words. The orchestrator ingests 300 words instead of five result sets.
Spawn when the work produces noise: three or more web searches, three or more integration API calls, long content that needs research first, any genuinely independent subtask where only the summary matters.
Handle it directly when the task is small (one or two searches, a single API call), when you’ll need to make several decisions mid-flight, or when you’re revising something that already exists.
Pin the model, every time
Ad-hoc subagents default to model: inherit, which means an Opus session burns Opus on grep work unless something says otherwise. Custom agents in ~/.claude/agents/ should pin a tier in frontmatter; built-in agent types have no pin and need model passed at call time.
Three tiers beat two:
- Haiku for mechanical retrieval and formatting. Task CRUD, short messages, dependency scans. Structured calls where nothing has to be worked out.
- Sonnet as the default. Research, review, extraction, docs, test writing, bug tracing. It follows structured prompts (output formats, brand rules, tool preferences) as reliably as anything above it.
- Opus only via a call-time override, for deep architectural judgment and long-form prose where quality actually changes the output.
Over dozens of dispatches a day, the haiku tier is real money on top of the sonnet-versus-opus split. Anthropic benchmarked the same shape on BrowseComp: a strong orchestrator with Sonnet workers hit 86.8% at $18.53 per problem against 90.8% at $40.56 for all-strong, and all-Sonnet managed 77.8%. Most of the accuracy, less than half the cost, and the split is what buys it.
Never split research from writing
The agent that gathers the information should produce the output. Handing research results to a separate writing agent is a telephone game: the second agent works from a summary of a summary and loses the specifics that made the research worth doing. A content agent researches and drafts in one context. A research agent gathers and synthesizes in one context.
Offload reading to another model
CLI models read files directly and return only a summary. That is a context transaction, not a quality judgment: when a CLI call reads fifteen files and hands back 200 words, your session ingests 200 words.
Route it deterministically rather than letting the model pick flags each time. My Codex calls go through a wrapper that takes a task class (commit, implement, explore, ingest, review, hardest) and pins model, reasoning effort, and sandbox from that class. The model classifies; the script owns every flag.
There’s a second reason to run a cross-family worker. Claude quota and ChatGPT quota are separate pools, so routing self-contained review and implementation work to a GPT worker makes your Claude limits last longer. You also get a real review bonus: a GPT model reviewing Claude’s work catches errors two same-family models share.
One gotcha worth the paragraph: -c 'mcp_servers={}' does not disable MCP servers on the Codex CLI. TOML table overrides merge, so an empty table is a no-op. The switch that works is --ignore-user-config, which also drops the plugin cold-start.
Hooks enforce; instructions remind
An instruction in CLAUDE.md is a preference the model usually follows. A hook is a thing that happens. Anything you actually need to be true belongs in a hook.
The ones earning their place here:
SessionStartloads MCP secrets from a.env, then runs profile detection. The secrets script unsetsANTHROPIC_API_KEYandANTHROPIC_AUTH_TOKENbefore servers connect, because a stray key in.envsilently bills the API instead of using your subscription.PreToolUseon large reads blocks an unscoped read of any text file over 64 KB and returns three options: grep to locate and read a range, delegate to a cheap subagent for a summary, or read it in successive chunks. Choose by task. The range read is right when you’re editing; delegation only wins when a summary suffices and the file is big enough that its raw text would otherwise re-send every turn.PreToolUseon clipboard writes runs a prose linter over anything headed forpbcopyand blocks the copy until it’s clean. Catching a style violation before the text leaves is worth more than catching it after.
Permissions: auto plus an allow-list
I ran bypassPermissions for a while. It’s the wrong default even when nothing goes wrong, because novel commands run silently alongside routine ones.
The replacement is defaultMode: "auto" with three lists. allow holds around 150 pre-approved patterns (git, gh, npm, the MCP tools I call constantly) and runs without a prompt. ask holds the destructive set: rm -rf, rm -r, git push --force, git reset --hard, git clean -f. deny hard-blocks chmod -R 777.
Day-to-day friction is the same as bypass, because the common patterns are covered. The difference is that anything genuinely new surfaces a prompt. The allow-list grows on its own: every “always allow” answer appends a pattern.
If you drive sessions from the phone, mirror ask and deny into remote-settings.json. Remote-initiated runs should inherit the same guardrails as the ones you start at the desk.
Separately, and regardless of mode: confirm before external actions. Sending mail, posting to a channel, modifying tickets, publishing. The cost of an errant message is asymmetric.
Watch the meter
On a subscription with weekly caps, the failure mode is burning the week’s budget by Thursday and not noticing until you hit the wall. A custom statusline turns that into a number you glance at:
Opus 5 | xhigh | 142k 14% | Session 23% · 3h12m | Week 61% · 4d6h
Two design notes. Compute the percentages from cost in USD, not raw tokens: cache-read tokens bill at roughly a tenth of fresh tokens, so token-based divisors drift badly as a conversation grows while cost stays stable. And project the burn rate to the end of the window rather than reporting the current total, so the gauge tells you whether to ease off now.
Calibrate by snapshotting your usage tool’s cost and the reported percentage at the same moment, then re-calibrate every few weeks. Caps move without announcement.
Persistence is the part people skip
State files, session logs, and auto-memory are the single largest productivity gain in this setup, and the least discussed. A short current.md with active priorities, open threads, and recently completed items means a new session opens with the model already knowing where things stand. No re-explaining.
For memory specifically, two rules keep it from rotting. Write down only what the repo doesn’t already record: code structure, past fixes, and git history are all better retrieved than remembered. And treat every memory as point-in-time. If a memory names a file, a flag, or a config value, verify it against the live system before acting on it.
If you’re starting today
Do these four, in this order, and skip everything else until they’re running.
- Write a short
CLAUDE.mdwith your communication rules and your confirm-before-sending list. Resist making it long. - Turn off every MCP server you haven’t called this week. Add them back per directory.
- Pin
model: sonneton every subagent definition you have, and only override upward when you can name why. - Convert your most-repeated instruction into a hook. You’ll immediately see which of your other instructions were being ignored.
The whole discipline is one question asked repeatedly: is this in the context window because it’s needed right now, or because it was easier to leave it there? For the packaged versions of the skills and delegation patterns above, see /skills and /tools.