●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution
Swapping Agent Config Per Session From One Shared Definition
A design pattern for running one base Managed Agent and overriding its model, prompt, tools, MCP servers, and skills per session with agent_with_overrides — with a validated factory and the operational traps I hit along the way.
As an indie developer, I run four blog sites with agents that behave almost identically.
The only differences are part of the system prompt, which MCP connectors they reach, and which skills they load. And yet, for a while, I kept a completely separate agent definition for each site.
Every time I fixed something shared, I had to sync four places by hand. Fix one and forget another, and a single night's batch would run on a stale prompt. Watching near-identical definitions drift apart, little by little, quietly wears you down.
Managed Agents' agent_with_overrides finally let me collapse this. One base agent definition. Then, only at the moment a session is created, I swap the model, system prompt, tools, MCP servers, and skills. No new definitions — just a coat of paint applied at runtime.
This article shares how I shaped that swapping into a form where nothing goes wrong, along with the validation code I actually use.
Why "swap per session" beats "add more definitions"
I had three real options.
Approach
Syncing shared parts
Clarity of the diff
Risk of a permissions accident
Separate definition per site
Manual, prone to drift
Good (independent)
Low, but definitions explode
One definition, branch inside the prompt
None
Poor (bloated)
Medium (blurry boundaries)
One definition, override per session
None
Good (diff is explicit)
Low if designed for it
What decided it was that the diff gathers into one place, as code.
"For this site, only this MCP and this skill" — that difference lines up right before session creation, in a form you can read. Far clearer than burying conditional branches deep inside a prompt.
But this approach has one thing you will trip over first.
The counterintuitive part: overrides replace, they do not merge
At first I assumed an override was "the base plus a diff."
The base agent allows four tools. In one session I wanted to swap one of them for a different tool, so I passed just that one tool in tools.
The result: the other three tools vanished for that session.
A field you specify in agent_with_overrides replaces that whole field. Even for arrays, elements are not merged individually. Pass a single element to tools, and that session has exactly one tool.
What you pass in the override
Result for that session
Omit the field
Inherits the base value as-is
Empty array []
That element becomes "none" (not inherited)
Only some of the elements
Only the passed elements remain (the rest are gone)
Once stated, it is obvious behavior. But if you let the word "diff" pull you along, you end up with a night batch running with silently fewer tools. I only noticed from the logs.
The principle I took from this is simple. Always pass the final shape of the field. If you want partial add or remove, read the base value, compose it on your side, then pass the result. Composing that safely is the job of the factory below.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A complete, validated session factory that makes per-session overrides safe
✦Why overrides replace rather than merge, and the trap that silently dropped my tools
✦How to decide override granularity so one base agent can drive four sites
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
I concentrate both the intent of the swap and its safety net into the code that creates the session. Here is the implementation in TypeScript (Agent SDK).
import Anthropic from "@anthropic-ai/sdk";const client = new Anthropic();// The ceiling of what the base agent may allow. Stop if an override exceeds it.const TOOL_ALLOWLIST = new Set([ "web_search", "bash", "text_editor",]);const MCP_ALLOWLIST = new Set([ "mcp-claudelab", "mcp-gemilab", "mcp-antigravitylab", "mcp-rorklab",]);type SiteOverride = { site: string; systemPrompt: string; tools: string[]; // final shape for this session mcpServers: string[]; // final shape for this session skills: string[]; model?: string;};function assertSubset(name: string, given: string[], allow: Set<string>) { const stray = given.filter((v) => !allow.has(v)); if (stray.length > 0) { // Fail before launch on any unexpected escalation of scope throw new Error( `[override guard] ${name} has unpermitted items: ${stray.join(", ")}` ); }}export function buildSessionAgent(base: string, o: SiteOverride) { // Because overrides replace, validate the boundary on our side first assertSubset("tools", o.tools, TOOL_ALLOWLIST); assertSubset("mcpServers", o.mcpServers, MCP_ALLOWLIST); if (o.tools.length === 0) { // Empty means "no tools." I want to suspect an accident, so log it. console.warn(`[override] ${o.site}: tools is empty. Confirm this is intentional`); } const overridden = { type: "agent_with_overrides" as const, agent_id: base, overrides: { model: o.model, // omit to inherit the base model system: o.systemPrompt, tools: o.tools.map((name) => ({ name })), mcp_servers: o.mcpServers, skills: o.skills, }, }; // One line recording what was swapped, so behavior can be traced later. console.info( `[session] site=${o.site} model=${o.model ?? "(inherit)"} ` + `tools=${o.tools.length} mcp=${o.mcpServers.join("+")}` ); return overridden;}
Three points matter.
First, assertSubset checks the ceiling the base allows, before launch. Because overrides replace, a stray unregistered MCP name could point one session at an unintended target. If it falls outside the allowlist, we fail before the session is created.
Second, we warn instead of swallowing an empty array. "No tools" is a valid setting, but in my experience it is a slip nine times out of ten. Not passing it silently saves next morning's regret.
Third, we record what was swapped in a single log line. Because overrides evaporate with the session, the log is the only trace of "which config did that batch run on."
Composing and calling from the session side
With the factory in place, each site's diff becomes short — and readable.
const SITE_OVERRIDES: Record<string, SiteOverride> = { claudelab: { site: "claudelab", systemPrompt: "You are the editor of a Claude tech blog. Write politely.", tools: ["web_search", "text_editor"], mcpServers: ["mcp-claudelab"], skills: ["article-gate", "templating-gate"], }, rorklab: { site: "rorklab", systemPrompt: "You are the editor of a Rork app-dev blog. Write politely.", tools: ["web_search", "text_editor", "bash"], // Rork uses bash for build checks mcpServers: ["mcp-rorklab"], skills: ["article-gate"], model: "claude-opus-4-8", },};async function runFor(site: string) { const override = SITE_OVERRIDES[site]; if (!override) throw new Error(`undefined site: ${site}`); const session = await client.beta.messages.sessions.create({ agent: buildSessionAgent("agent_blog_base", override), }); return session;}
The base agent_blog_base is the only definition. Shared behavior — banned words, the baseline tone policy — is written once, in that base. What varies per site is now confined to a few rows of this table.
Adding a new site is one more entry in the table. No more cloning a whole definition file and fretting over keeping the shared parts in sync.
When you want a delta, read the base first and build the final shape
"Always pass the final shape" is the right rule, but living with it means that every time you want to add just one tool to the base, you retype the whole final list by hand. Retyped lists drift. Mine did.
So I put a small helper in front of the factory that reads the base's current value and applies the delta for me.
type Delta = { add?: string[]; remove?: string[] };// Apply a delta to the base's current value, returning the final shape and the difffunction applyDelta(current: string[], delta: Delta, label = "field") { const remove = new Set(delta.remove ?? []); // Catches the case where the base renamed something and your remove is stale const ghosts = [...remove].filter((n) => !current.includes(n)); if (ghosts.length > 0) { console.warn( `[delta] ${label}: remove targets missing from base: ${ghosts.join(", ")}` ); } const kept = current.filter((n) => !remove.has(n)); const appended = (delta.add ?? []).filter((n) => !kept.includes(n)); const next = [...kept, ...appended]; return { next, diff: diffOf(current, next) };}function diffOf(before: string[], after: string[]): string[] { const b = new Set(before); const a = new Set(after); return [ ...before.filter((n) => !a.has(n)).map((n) => `-${n}`), ...after.filter((n) => !b.has(n)).map((n) => `+${n}`), ];}
On the calling side, I read the base once at process start and reuse it. Re-reading per session just adds latency to every nightly batch for no benefit.
// Fetch the base once at startup (method names shift between SDK versions)const base = await client.beta.agents.retrieve("agent_blog_base");const baseTools = (base.tools ?? []).map((t) => t.name);const { next, diff } = applyDelta(baseTools, { add: ["bash"] }, "tools");console.info(`[delta] tools ${diff.join(" ") || "(no change)"} -> ${next.join(", ")}`);// Feed next into SiteOverride.tools, then hand it to buildSessionAgent
Here is what five representative deltas produce against a base of ["web_search", "text_editor"].
add bash for verification -> [web_search, text_editor, bash] diff=[+bash]drop search -> [text_editor] diff=[-web_search]swap one for another -> [text_editor, bash] diff=[-web_search +bash]add something already there -> [web_search, text_editor] diff=[]remove something not in base -> [web_search, text_editor] diff=[][delta] tools: remove targets missing from base: computer
Line four matters for a boring reason: adding something that already exists does not duplicate it. The result is the same no matter how many times you run it, which makes the helper safe to put behind generated config.
Line five is the one that earns its keep. If the base renames a tool and your remove still names the old string, the diff comes back empty and nothing happens — meaning a tool you believed you had dropped keeps running. The warning exists to make that visible.
And I always log the diff. With overrides you only ever see the result, so without a record of what was supposed to change, there is nothing to review.
Operational lessons the docs do not cover
After a few weeks of running this, a few things clicked.
Overrides are scoped to the session, so you need your own inventory. The base definition is listable from the Console, but looking at the base tells you nothing about which config each session actually ran on. I aggregate the log above daily, keeping a record of "which site ran on which model, how many times." If you allow swapping, you have to own the record of the swaps. Skip this, and both cost attribution and root-cause analysis fall apart.
Model inheritance is convenient but can become unintended coupling. Omit model and the session inherits the base model. Raise the base model and every site rises at once. Handy, but when you want to test one site on an older model, you must pin it explicitly rather than omit. I split "safe to inherit" from "must be pinned" inside the table, and always write model on the pinned side.
Set override granularity by change frequency, and it settles. Things that change often — parts of the prompt, referenced connectors — go into overrides. Things that rarely change — banned words, the core output-format policy — stay in the base. Drawing that line by frequency removed my hesitation about "where should I fix this." When a frequently touched diff is mixed into the base, the psychological barrier to touching shared parts rises.
A daily inventory is one log file and one small script
I said you have to own the record of the swaps. What that actually takes is modest: a script that folds the [session] lines from the factory by date, site, and model.
One prerequisite — the line needs a date on it. If your runner does not prepend one, add new Date().toISOString() to the front of that console.info.
import { readFileSync } from "node:fs";// Pick up only [session] lines. The date may come from the runner or the line itself.const LINE = /^(?<ts>\d{4}-\d{2}-\d{2})\S*\s+\[session\]\s+site=(?<site>\S+)\s+model=(?<model>\S+)\s+tools=(?<tools>\d+)\s+mcp=(?<mcp>\S+)/;const rows = readFileSync(process.argv[2], "utf8") .split("\n") .map((l) => l.match(LINE)?.groups) .filter(Boolean);const agg = new Map();for (const r of rows) { const key = `${r.ts}\t${r.site}\t${r.model}`; const cur = agg.get(key) ?? { runs: 0, tools: new Set(), mcp: new Set() }; cur.runs++; cur.tools.add(Number(r.tools)); // a wobble on one day stays in the column r.mcp.split("+").forEach((m) => cur.mcp.add(m)); agg.set(key, cur);}console.log(["date", "site", "model", "runs", "tools", "mcp"].join("\t"));for (const [key, v] of [...agg].sort()) { const tools = [...v.tools].sort((a, b) => a - b).join("/"); console.log([key, v.runs, tools, [...v.mcp].join(",")].join("\t"));}// Runs on inheritance = the blast radius of raising the base modelconst inherited = rows.filter((r) => r.model === "(inherit)").length;console.log(`\nRuns on inherited model: ${inherited} / ${rows.length}`);
I cannot publish the production log, so here is the output over two days of identically formatted lines.
Look at the tools column on the first row. 1/2 means the same site ran with two different tool counts on the same day. That wobble is exactly how I first noticed my dropped tools. Override accidents never throw — they only surface as a column that should be uniform and is not.
The mcp column does the same job. mcp-shared appearing for gemilab on the second day was my own change. If it is intentional, you get a dated record of it; if it is not, you find out the next morning instead of the next quarter.
The last line is the number I check before raising the base model. Five of seven runs inherit, so swapping the base moves the behavior and cost of five runs the moment I save it. Knowing that number ahead of time is what made model upgrades stop feeling risky.
Swapping at session creation vs. swapping mid-conversation
Everything above rests on one fixed assumption: the only moment you can swap anything is the instant a session is created.
Halfway through a long research run, when I wanted to drop search because I no longer needed it, my only move was to recreate the session — and recreating it throws away the prompt cache. So I gave up on the idea of narrowing permissions by phase altogether, handed over a wide set of tools at the start, and let the run finish that way.
The mid-conversation tool add/remove beta that landed on 2026-07-28 breaks that assumption. You can swap the tool set between turns and keep the conversation going with the prompt cache intact. It covers Claude Fable 5, Mythos 5, Opus 4.8, and Opus 5. Beta specs move, so please confirm against primary sources before you build on it.
Aspect
Per-session override
Mid-conversation tool change
What you can swap
Model, prompt, tools, MCP, skills
The tool set
When it applies
Only at session creation
Between turns
Prompt cache
Rebuilt, since it is a new session
Preserved
Best fit
Per-site or per-project coloring
Narrowing permissions in stages within one run
The cache surviving does not mean ordering stops mattering. Caching works on prefixes, so anything you might touch mid-run is best kept behind the parts that never move. I order my tool definitions with the swappable ones after the fixed ones for exactly that reason.
The substance of the swap follows the same rule as before: you always pass the final shape of the field, never a delta. So it goes through the same validation function.
// The final tool shape per phase. Swappable entries sit after the fixed ones.const PHASE_TOOLS: Record<"research" | "edit" | "verify", string[]> = { research: ["text_editor", "web_search"], edit: ["text_editor"], verify: ["text_editor", "bash"],};// Build the tool definitions for the next turn, sharing the boundary check// with session creation.function toolsForPhase(sessionId: string, phase: keyof typeof PHASE_TOOLS) { const tools = PHASE_TOOLS[phase]; assertSubset("tools", tools, TOOL_ALLOWLIST); // Leave a trace of which turn changed the permissions console.info(`[phase] ${sessionId} -> ${phase} tools=${tools.join(",")}`); return tools.map((name) => ({ name }));}
Allow web_search while researching, drop it once drafting starts, hand over bash only during verification. Splitting a run into phases used to cost more in cache rebuilds than it saved, so it never paid off. Now it writes out plainly. The reason to narrow permissions and the reason to keep costs down finally point in the same direction.
That said, my unattended nightly batches still stay on per-session overrides. The more something runs without me watching, the more I want its configuration to hold still, because that is what makes failures traceable. Mid-conversation swapping is something I use for long interactive research, and only there for now.
Three things that did not work
I have mostly described what collapsed neatly. Here is what did not.
I kept the allowlist only in code and let it drift from the base.TOOL_ALLOWLIST was hardcoded while I edited the base definition in the Console and removed a tool. The set in code stayed stale, so validation passed while sessions were being created that named a tool the base no longer had. Now I fetch the base at startup, compare it against the allowlist, and warn on any divergence. If you are going to hold a boundary in two places, the design has to include checking that the two places agree.
I used an empty array for fields I had not decided yet. While one site's skill set was still undecided, I left skills as []. An empty array is not inheritance — it is none. Several runs went out without the article quality-gate skill loaded. Undecided means omitting the field entirely so it inherits. An empty array is reserved for "I actually want none of these," and I now say so in a comment.
I pinned a model and forgot to revisit it. I pinned model on one site for a test. Later I upgraded the model everywhere, and the pinned site alone kept running on the old one — the exact case inheritance would have handled for free. Pinning is convenient, but the moment you pin, you take on the duty of reviewing it. I now write the reason for the pin and the date I intend to revisit next to it. A pin I cannot write a reason for is a pin that should have been inheritance.
All three come from the same place. Overrides apply silently, so being wrong does not raise anything. Paying for your own warnings and logs up front turns out to be the faster path.
How to choose
My situational take.
If you have two or three bases with large differences, don't force them into overrides; separate definitions are fine. The cost of validating swaps outweighs the cost of syncing.
But if you effectively have one base, and the differences fit into "part of the prompt, connectors, skills," session overrides shine. My four sites were exactly this shape.
And if you do introduce swapping, build validation and recording in from the start. Overrides are powerful precisely because they change permissions and config silently. Decide the boundary you want to enforce — something like assertSubset — up front, and you can hand the night's automated runs over with peace of mind.
I am still learning the feel of this as I operate it. If it gives anyone juggling several similar agents a nudge toward collapsing their design, I would be glad. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.