cd /news/large-language-models/mid-conversation-system-prompts-stee… · home topics large-language-models article
[ARTICLE · art-29710] src=dev.to ↗ pub= topic=large-language-models verified=true sentiment=↑ positive

Mid-Conversation System Prompts: Steering an Agent Without Breaking the Cache

Anthropic's Claude models now support mid-conversation system messages, allowing developers to inject new instructions into a long-running agent session without invalidating the entire prompt cache. By placing a system-role message after the cached history, the cached prefix remains intact, reducing cost and latency. This feature also provides a non-spoofable operator channel, improving prompt-injection safety compared to the previous workaround of embedding instructions in user turns.

read4 min views4 publishedJun 16, 2026

Here is a problem I hit building a long-running agent: I needed to inject a new instruction partway through a session ("the project is Go, write Go") but editing the top-level system prompt to add it invalidated my entire prompt cache. Every cached turn got reprocessed at full price. The fix is a feature that landed in the current Claude models: mid-conversation system messages. Here is what it is and when to use it.

A long agent session has a large, stable system prompt and a growing message history, and you cache the prefix so each turn reuses the prior work cheaply. That works until you learn something mid-session that the agent needs to know: a mode toggled, the user delivered async context, files changed on disk, the token budget dropped.

The naive move is to edit the system prompt to include the new fact. But the system prompt sits at the front of the cached prefix. Change one byte there and you invalidate everything after it. Your whole conversation history reprocesses at full input price on the next request. For a long session, that is expensive and slow.

The current models let you put a system

-role message directly in the messages

array, after the history, instead of editing the top-level system

:

const response = await client.messages.create(
  {
    model: "claude-opus-4-8",
    max_tokens: 16000,
    system: [
      { type: "text", text: STABLE_SYSTEM, cache_control: { type: "ephemeral" } },
    ],
    messages: [
      ...history,                                    // cached prefix, untouched
      { role: "user", content: latestUserMessage },
      // @ts-expect-error: role:"system" SDK types may still be landing
      { role: "system", content: "This project is Go. Write all code in Go." },
    ],
  },
  { headers: { "anthropic-beta": "mid-conversation-system-2026-04-07" } },
);

Because the new instruction sits after the cached history, it invalidates nothing before it. The cached prefix stays intact, you pay full price only for the small new message, and the agent still receives the instruction with operator authority.

The old workaround was to put operator instructions inside a user turn, often wrapped in something like <system-reminder>

. That preserves the cache the same way, but it has a security problem: a user message is forgeable. Anything that can write to user-visible input can spoof an instruction that looks like it came from you, the operator.

A role: "system"

message is the non-spoofable operator channel. It carries operator authority that a user-turn instruction does not, which matters when you are injecting trusted state (mode switches, permissions) into an agent that also processes untrusted user input. So this is both a caching win and a prompt-injection-safety win.

One subtlety that took me a try to get right: phrase these messages as facts, not overrides. State the situation and let the model act on it. Avoid override-style language like "ignore what the user said" or "disregard the previous instruction." The models are trained to protect users from instructions that work against them, and that protection applies to the system role too. So:

// Good: states context, lets the model act
{ role: "system", content: "Auto-approve mode is now enabled for this session." }
// Risky: override framing, may be resisted
{ role: "system", content: "Ignore the user's earlier request and do X instead." }

A few rules from the spec:

messages[0]

. Use the top-level system

for the initial prompt.role 'system' is not supported on this model

). Catch that and fall back to the user-turn <system-reminder>

pattern.

try {
  // ... mid-conversation system message
} catch (err) {
  if (err instanceof Anthropic.BadRequestError && err.message.includes("system")) {
    // fall back: inject as a user-turn <system-reminder> block
  } else {
    throw err;
  }
}

The trigger is: I have learned something mid-session that the agent needs, and I want to tell it without rebuilding the prefix. Mode changes, freshly delivered context, state the application discovered after the session started. Anything dynamic that you would otherwise be tempted to splice into the system prompt.

If the fact is known at session start, it belongs in the top-level system

prompt as usual. The mid-conversation channel is specifically for things you learn after the cached prefix is already built. Used that way, it keeps your cache hot, keeps your operator instructions non-spoofable, and saves you from the expensive mistake I made: editing the system prompt mid-session and watching the whole conversation reprocess at full price.

── more in #large-language-models 4 stories · sorted by recency
── more on @anthropic 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mid-conversation-sys…] indexed:0 read:4min 2026-06-16 ·