Claude Code best practices, from a year of daily use
How I've configured Claude Code and the Codex CLI as an operating layer rather than a chat window: adaptive MCP profiles, three-tier subagent routing, verification rules that stop the model grading its own homework, hooks that enforce instead of remind, and why I turned the reasoning effort dial back down. With a starter CLAUDE.md / AGENTS.md you can hand straight to your agent.
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, with the Codex CLI as a second worker alongside it. The specifics are mine; the mechanics transfer, and one section below maps every one of them onto Codex.
Rather than rebuild any of this from a description, hand your agent the files. The rules below are packaged as a fill-in-the-blanks CLAUDE.md / AGENTS.md you can drop into a repo. The working code is public: maven-template (a whole configured repo), organization-ai-skills (30 skills in the open SKILL.md format, installable unchanged on both Claude Code and Codex), cross-model-agent-delegation (the wrappers, in both directions), and n8n-workflows (the automation side). Point your agent at a URL and tell it to read the repo.
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 on your terms.
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=70fires compaction at 70% of the window, so the summary lands while you still have headroom to correct it. On a 1M-context model, setCLAUDE_CODE_AUTO_COMPACT_WINDOW=1000000too, or the math still tracks the 200k default.
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.
Two accounts on one service
If you use both a work and a personal account on the same service, this one costs an afternoon to diagnose. Claude Code identifies a remote MCP server by its exact URL string. Point a local server at the same URL a hosted connector already uses and both collapse into one entry, with the local copy winning: the connector goes inactive in that project, and the only way back is to disable the local one. I hit this on 2026-07-30 with two Notion accounts, both on https://mcp.notion.com/mcp.
Any distinguishing query parameter makes the strings differ, and both stay live:
claude mcp add --transport http notion-personal 'https://mcp.notion.com/mcp?account=personal'
Notion ignores the unknown parameter, and the two answer on separate tool prefixes. If a provider validates the canonical URL and rejects the extra parameter, register the second account through mcp-remote instead (it registers as a stdio command with no url field, so there is nothing to collide on, and MCP_REMOTE_CONFIG_DIR gives each account its own token directory), or use a provider token over stdio, where one token maps to exactly one workspace.
Three things bite in practice. Run claude mcp add inside the project that needs the second account so it writes local scope rather than loading everywhere. OAuth consent authorizes whichever account the browser is already signed into, so do it in a private window and then read something only that account can see to confirm which one you got. And keep the server name lowercase and hyphenated: every non-alphanumeric character becomes an _ in the generated tool names, so Notion (Personal) surfaces as mcp__Notion__Personal___*.
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. Deviating means passing a different class, not different flags. Both wrappers, plus the guard hook that blocks a raw codex exec from the Bash tool, are in cross-model-agent-delegation.
Have the wrapper write the worker’s output to a file and print OUT=<path> as its last line. Piping a long CLI response through head or tail silently truncated a real finding here once.
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.
Worth knowing before you write your own: -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.
Make it check its own work
If I could keep one thing from this setup it would not be any of the context machinery above. It would be this: give the model a way to observe the result, then require it to use that way. When Claude can close the feedback loop itself, it iterates until the output is right. When it can’t, it guesses and reports the guess with the same confidence it would report a verified fact.
The rule in my global config is four sentences:
Give yourself a way to observe the result, then use it. If a check exists (test suite, bash command, browser, simulator, log, live app), run it and report what you actually saw. A claim that something works is not evidence that it does. This applies to delegated work too: re-run the check yourself, and say which you did, observed or reported.
That last distinction does most of the work. A subagent saying “tests pass” and you having watched eight tests pass are different facts, and treating them as one is how a bad assumption survives three sessions. Making the model label which one it has costs a word and changes what you trust.
What counts as a way to observe depends on the surface: a browser for anything with a frontend, a test suite or a dry run for code, rendering the file for a document, re-querying the source for a number, a human pressing the button for anything involving hardware. The failure is not having no check available. It’s having one and not wiring it up.
Adversarial review, and the trick that makes it terminate
Self-review doesn’t work. A model that just wrote something defends its own choices, and asking the same context to double-check produces agreement dressed as scrutiny. What works is a fresh context with an explicitly skeptical framing, on a different model family where possible.
Three tiers, cheapest first: a single critic agent pinned to a strong model and high effort, for plans and architecture calls; a panel of named personas critiquing independently and then reconciled, for consequential spending, hiring, or positioning decisions; and a cross-model review through the delegation wrapper, told to refute rather than to bless. A different model family catches different bugs.
Two refinements matter more than the framing.
The first is attack the plan, not the diff. Reviewing the approach before anything is built catches architectural mistakes while they still cost nothing.
The second fixes the failure mode that makes people abandon adversarial review after a week. An agent told to “find the problems” will always find problems, including invented ones, and will never signal that it’s finished. You get an endless review cycle and you learn to ignore the output. The fix is to change the instruction:
State what must be true for this to be correct, then check those things.
Have the reviewer enumerate the conditions the work rests on (facts that must hold, people who must act, numbers that must land in a range) and mark each supported, unsupported, or unknown. That converts an open-ended hunt into a bounded checklist with a stopping criterion. A condition nobody can challenge is a green light, not a failure to find something.
Turn the effort dial down
I ran the highest reasoning effort setting for months on the theory that reasoning is free on a subscription. That was wrong twice over.
Subscription reasoning isn’t free, it’s rate-limited, and the weekly window is the real budget. More importantly, maximum effort makes ordinary work worse rather than merely slower. On current frontier models it over-plans bounded tasks, adds scope nobody asked for, and revises correct first answers into wrong ones. The behavior people describe as a model “going off the rails” is often just too much effort applied to something that didn’t need it.
Set a moderate default and raise it per task. Pin higher effort in the definitions of the specific agents that need depth, where it survives changes to the global default, so your adversarial reviewer stays sharp while a documentation grep stops thinking for thirty seconds.
The same logic applies to your instructions. Guardrails written to stop an older model’s failure modes read to a newer one as constraints to satisfy, and satisfying them costs turns. Audit periodically: open a fresh session, hand it your CLAUDE.md and your skills, and ask what’s unnecessary or redundant. Expect to cut a quarter of it. If a session feels like it’s flailing on something simple, suspect your config before you suspect the model.
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 work spanning sessions, the design detail that matters is write the log before the work, not after. The obvious approach is a summary at the end of a session, and it fails in exactly the case it exists for: a session killed by compaction, a rate limit, or a stray close never reaches an end to summarize at. So entries go in ahead of the decision they record, before anything hard to reverse, before anything long, and whenever a fact took real effort to establish. A rewritten top block (goal, current state, next steps, open questions) sits over an append-only log where each entry carries decision, why, evidence labeled observed or reported or assumed, and whether it’s reversible. Failed approaches get an entry too, with a “do not retry unless” line. Nothing is ever edited out, including the wrong calls: a wrong decision plus its correction is the record that stops a third attempt.
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.
Turn a routing decision into a skill
An agent with five search tools connected will use whichever one it thought of first, forever. Mine used to reach for the most expensive fetch tool on plain static pages, and for a keyword search when the question was semantic.
The fix is a skill that fires before any search or fetch and carries two tables: which search tool for which situation (cheap bulk default, semantic search when keyword search returns junk, cited synthesis when you want an answer rather than links, deep research only when 30 seconds of latency is acceptable) and which fetch tool (a fast markdown reader by default, a rendering scraper only when the page is a JS app or behind anti-bot). Plus an escalation rule: try the cheap one, escalate when the result comes back empty or garbled.
Two details make it work. The description field has to name the trigger words, because that’s all the model reads until the skill fires: “search”, “look up”, “research”, “scrape”, “fetch this page”. And the skill should record the dead ends, not just the preferences. Mine names the sites where the general-purpose scrapers are blocked outright and points to the platform-specific path instead, which saves a cycle of failed fetches every time.
The skill is public in organization-ai-skills as search-and-scrape. Any routing decision you keep making by hand is a candidate for the same treatment.
Running the same setup on Codex
Almost none of this is Claude-specific. I run the Codex CLI as a second worker on the same machine, with its own global instruction file, its own skills, and its own subagent roles. The vocabulary differs; the structure is the same.
| Concept | Claude Code | Codex CLI |
|---|---|---|
| Always-loaded instructions | CLAUDE.md, global and per repo | AGENTS.md, global (~/.codex/) and per repo |
| On-demand knowledge | Skills (.claude/skills/*/SKILL.md) | Skills (~/.codex/skills/), same SKILL.md format |
| Auto-loaded project rules | .claude/rules/*.md | ~/.codex/rules/*.rules |
| Bundled skills and commands | Plugins, via marketplaces | Plugins, via marketplaces (it reads Claude ones too) |
| Subagents | .claude/agents/*.md, model in frontmatter | ~/.codex/agents/*.toml, model and effort in the role file |
| External tools | MCP servers in ~/.claude.json | MCP servers in ~/.codex/config.toml |
| Permissions | permissions with allow / ask / deny | approval_policy plus sandbox_mode |
| Reasoning effort | effortLevel, /effort per task | model_reasoning_effort, or per call |
| Deterministic enforcement | Hooks: PreToolUse, PostToolUse, SessionStart | No pre-tool gate as of codex-cli 0.144.1; use wrapper scripts and notify |
Skills are the portable layer. SKILL.md is an open format, so one directory installs on both: npx skills add <owner>/<repo> on the Claude side, codex plugin marketplace add <owner>/<repo> on the Codex side, same files. Instructions and permissions are what you write twice, and a symlink from CLAUDE.md to AGENTS.md collapses even that.
Two asymmetries are worth planning around. The missing pre-tool gate means anything you’d enforce with a hook has to live in a wrapper the model is told to call, which is weaker: a rule again, not a gate. And Codex subagent routing has a trap of its own. Which spawn tool a Codex orchestrator gets depends on the parent model, and one of the two variants hides the model and reasoning_effort fields by default, silently running every worker on the expensive model. Same failure as Claude’s model: inherit, different mechanism. Verify what your workers actually ran on rather than what the role file says.
Audit borrowed advice against what you already run
I reviewed a batch of ten well-regarded community recommendations on 2026-08-03. Four were already implemented here in some form, three were actively counterproductive for this setup, and three were worth adopting. That ratio is worth internalizing before you copy anyone’s config, including this one.
The most-cited item in the batch was a plugin for having Claude orchestrate Codex. Its own author’s write-up reported burning 9M Claude tokens plus 1.2M Codex tokens on a single feature. The 104-line wrapper described above does the same division of labor for a fraction of that. Popularity tracks novelty, not fit.
Two checks before adopting a pattern: read the numbers in the post that recommends it, and ask whether you already solve that problem another way. Be equally suspicious of benchmark tables with no cited source.
If you’re starting today
Do these five, in this order, and skip everything else until they’re running.
- Write a short instruction file (
CLAUDE.mdfor Claude Code,AGENTS.mdfor Codex) with your communication rules and your confirm-before-sending list. Resist making it long. Download the fill-in-the-blanks starter in either flavor, hand it to the agent, and have it fill the bracketed placeholders by asking you. - Add a verification rule and give the model something to verify against. A test command, a browser, a render step: whatever closes the loop for the work you do.
- 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 everything above, see /skills and /tools, or point your agent straight at the repos: maven-template, organization-ai-skills, cross-model-agent-delegation, n8n-workflows.