DeepThinking AI

How do you stop Claude Code from expanding untrusted prompt text?

AI Architect

Key takeaways

  • Claude Agent SDK 0.2.158 adds `verbatim_prompts`, and the implementation stamps each outgoing user message `client_composed: true`.
  • Claude Code expands an `@/absolute/path` token in prompt text into local file contents by default, outside the working directory and without a tool call.
  • `verbatim_prompts` works in `query()`, `ClaudeSDKClient.connect()`, and `ClaudeSDKClient.query()`, including streamed prompts.
  • The option requires Claude Code 2.1.248 or later, and older CLIs warn instead of honoring the flag.
  • Tool permission rules still matter because `verbatim_prompts` changes message delivery, not what tools can run after the message reaches Claude.

Anthropic shipped verbatim_prompts in claude-agent-sdk-python v0.2.158 on 23 September 2026. The release is worth attention because the patch states plainly that Claude Code expands an @/absolute/path token anywhere in prompt text into file contents, outside the working directory and without a tool call, and the new option closes that path by making the CLI deliver the prompt as written.

What does verbatim_prompts change in Claude Agent SDK?

It adds one option with a narrow and important scope. Anthropic’s release note for claude-agent-sdk-python v0.2.158 says ClaudeAgentOptions.verbatim_prompts defaults to False, and when you switch it on, user messages are “delivered to the CLI exactly as written”. The patch shows how: every outgoing user message is copied with client_composed: true, and Claude Code 2.1.248 and later treats that flag as the instruction to skip @path expansion and slash-command dispatch. Anthropic also wired the same behavior into all three SDK entry points it names publicly: query(), ClaudeSDKClient.connect(), and ClaudeSDKClient.query(), including streamed prompts. That breadth matters, because the unsafe case is rarely a single static string. It is usually a prompt your app assembled from prior turns, retrieved text, or user content just before submission, the same territory where session prompt snapshots already made precise transport details matter.

Where `verbatim_prompts` changes the Claude Code prompt path

Where `verbatim_prompts` changes the Claude Code prompt pathDiagram: 6 ordered layers. App composes a user message, then SDK stamps `client_composed: true`, then Claude Code delivers the text as written, then transform boundary (breakpoint), then `@/absolute/path` expansion, then Slash-command dispatch.1App composes a user messageUntrusted text may already be inlined here.2SDK stamps `client_composed: true`Only when `verbatim_prompts` is enabled.3Claude Code delivers the text as writtenNo `@path` expansion or slash commands.transform boundary4`@/absolute/path` expansionDefault behavior when the flag is off.5Slash-command dispatchDefault behavior when the flag is off.
Show as text
Where `verbatim_prompts` changes the Claude Code prompt path. Diagram: 6 ordered layers. App composes a user message, then SDK stamps `client_composed: true`, then Claude Code delivers the text as written, then transform boundary (breakpoint), then `@/absolute/path` expansion, then Slash-command dispatch.
#LayerNote
1App composes a user messageUntrusted text may already be inlined here.
2SDK stamps `client_composed: true`Only when `verbatim_prompts` is enabled.
3Claude Code delivers the text as writtenNo `@path` expansion or slash commands.
·transform boundary (breakpoint)
4`@/absolute/path` expansionDefault behavior when the flag is off.
5Slash-command dispatchDefault behavior when the flag is off.
The important detail is position. `verbatim_prompts` changes how Claude Code interprets the user message before any tool permission policy could matter, so it closes a path that permissions do not see.

Why do Claude Code permissions miss @path expansion?

Because the file read happens before the permission surface the SDK documents. Anthropic’s patch message says Claude Code expands an @/absolute/path token “outside the working directory and without a tool call”, and the end-to-end test proves the point by setting tools=[] and then checking whether the model can still read a planted marker from a temp file. It can, until verbatim_prompts is enabled. Read that next to the Python SDK docs: allowed_tools auto-approves tools, and disallowed_tools blocks them, but those controls only apply once the interaction is already a tool request. The local file expansion path never becomes one. That is the real finding here, and it is the part most launch-post style summaries would skip. If your defense model starts at permission prompts, you are already one stage too late, the same kind of boundary mistake that Anthropic’s auto permission policy forces teams to think about on the server side.

What changes when the same prompt is sent verbatim

What changes when the same prompt is sent verbatimSequence diagram between Your app and Claude Code CLI. 1. Your app to Claude Code CLI: Prompt contains `@/tmp/secret.txt`. 2. Claude Code CLI to Claude Code CLI: Default mode expands the local file. 3. Claude Code CLI to Your app: Model sees file contents. 4. Your app to Claude Code CLI: Same prompt with `client_composed: true`. 5. Claude Code CLI to Your app: Model sees the literal `@/tmp/secret.txt`.Your appClaude Code CLIPrompt contains `@/tmp/secret.txt`The text could have come from a prior turn or a third party.Default mode expands the local fileThe patch says this happens without a tool call.Model sees file contentsEven if tools are empty.Same prompt with `client_composed: true`Set by `verbatim_prompts`.Model sees the literal `@/tmp/secret.txt`No hidden file read and no slash-command dispatch.
Show as text
What changes when the same prompt is sent verbatim. Sequence diagram between Your app and Claude Code CLI. 1. Your app to Claude Code CLI: Prompt contains `@/tmp/secret.txt`. 2. Claude Code CLI to Claude Code CLI: Default mode expands the local file. 3. Claude Code CLI to Your app: Model sees file contents. 4. Your app to Claude Code CLI: Same prompt with `client_composed: true`. 5. Claude Code CLI to Your app: Model sees the literal `@/tmp/secret.txt`.
#FromToMessage
1Your appClaude Code CLIPrompt contains `@/tmp/secret.txt`. The text could have come from a prior turn or a third party.
2Claude Code CLIClaude Code CLIDefault mode expands the local file. The patch says this happens without a tool call.
3Claude Code CLIYour appModel sees file contents. Even if tools are empty.
4Your appClaude Code CLISame prompt with `client_composed: true`. Set by `verbatim_prompts`.
5Claude Code CLIYour appModel sees the literal `@/tmp/secret.txt`. No hidden file read and no slash-command dispatch.
Anthropic's own end-to-end test plants a random marker in a temp file and checks whether the model can see it. With the flag off, it can. With the flag on, the marker no longer reaches the model.

How does client_composed block file expansion and slash commands?

By changing the message Claude Code receives before Claude Code interprets it. The implementation is short enough to read in one pass. A helper called stamp_user_message returns the message unchanged when verbatim_prompts is off, and otherwise returns a copy with client_composed: true, overwriting any caller-supplied value. The transport layer names the contract too: VERBATIM_PROMPTS_MINIMUM_CLAUDE_CODE_VERSION = "2.1.248", and the surrounding comment says that is the first Claude Code version that honors the flag. That pairing matters operationally. This feature is a client transport behavior keyed to a specific CLI capability. Anthropic’s opinion in the code is clearer than the release note alone: if the CLI is too old, you do not have the control, however recent the Python package is. Treat this as a versioned wire contract between SDK and CLI, closer to a protocol flag than to a prompt tweak.

Should you enable verbatim_prompts by default in production?

Yes for any application that relays or reuses text it did not author itself. That includes chat products, coding assistants that quote tool output back to the model, and review flows that fold a prior answer into the next prompt. Those systems are already composing prompts dynamically, and the patch shows that one literal substring can turn that composition step into a local file read. Turning verbatim_prompts on by default is the right posture because it shrinks the implicit behavior surface and forces intentional behavior back into explicit code. The one strong case for leaving it off is local automation that deliberately relies on Claude Code affordances such as @path shortcuts or slash-command dispatch inside SDK-submitted prompts. Even there, I would treat the default as backwards. Apps that want hidden expansion should opt into it deliberately. Production controls age better when convenience features have to opt in.

What still needs guarding after you enable verbatim_prompts?

Everything that happens after the message reaches Claude. The implementation only stamps outgoing user messages, so it does not change system prompts, CLAUDE.md, tool permission policy, sandbox scope, or the behavior of any tool Claude later decides to call. It also does nothing for the basic discipline of separating instructions from quoted data. If your application mixes operator intent and relayed user text into one natural-language blob, the prompt is still hard to reason about, even when the @path token stays literal. That is why I see verbatim_prompts as a boundary repair rather than a full prompt-injection story. It closes one precise transport-layer gap. You still need the rest of the stack: careful prompt composition, explicit file access, and the review of which controls are descriptive versus enforced that this site has already worked through elsewhere. Good security work often looks exactly like this: one quiet feature flag that removes an entire class of accidental behavior.

Do this

Ship Claude Agent SDK prompts without hidden local-file reads

The steps are ordered so you close the hidden read path first, then preserve the intentional behavior you still need.

  1. Enable `verbatim_prompts` anywhere a prompt can contain outside text

    Turn it on for chat products, review agents, ticket flows, or any pipeline that inlines prior model output, tool output, or user text into a new prompt. That removes the pre-permission expansion path before you tune anything else.

  2. Pin compatible versions before rolling the change out

    Anthropic requires `claude-agent-sdk` 0.2.158 and Claude Code 2.1.248 or later. Older CLIs warn instead of honoring the flag, so version drift can leave one environment protected and another exposed.

  3. Replace any intentional `@path` or slash-command shortcuts with explicit code

    If your app relied on Claude Code expanding a file reference inside prompt text, move that behavior into a real file read, an SDK-managed prompt file, or a tool your application calls on purpose.

  4. Keep permission rules and hooks in place after the change

    `verbatim_prompts` only changes message delivery. Once Claude receives the text, normal tool execution and approval logic still determine what the agent may do next.

  5. Separate operator intent from relayed user text

    The safer prompt path still needs a clean instruction boundary. If your application carries end-user text forward, quote it as data rather than blending it into the instruction that tells the agent what to do.

  6. Re-test the exact prompts your production app generates

    Anthropic's tests prove the transport mechanism. They do not prove your composition logic. Capture one real prompt from your app, send it with the flag on, and confirm the literal `@...` text stays literal all the way through.

Frequently asked questions

Does `verbatim_prompts` disable tools or permission prompts?
No. It changes how the SDK sends user messages to Claude Code, so prompt text is delivered literally. Tool execution, approval rules, hooks, and sandbox settings still run the same way after the message reaches Claude.
Does `verbatim_prompts` work with streamed prompts?
Yes. The patch adds the same stamping behavior to streamed user messages and to one-shot string prompts. Anthropic's release note also names `query()`, `ClaudeSDKClient.connect()`, and `ClaudeSDKClient.query()` as covered surfaces.
What breaks when I turn `verbatim_prompts` on?
Any SDK prompt flow that intentionally relied on Claude Code's `@path` expansion or slash-command dispatch now stays literal. If your app used those as shortcuts, move that behavior into explicit file reads, tools, or application code you control.
Should every production app enable `verbatim_prompts`?
Any app that mixes model prompts with text a user, tool, or third party supplied should. A single-user local automation flow that intentionally uses `@path` shortcuts can leave it off, but it should be an explicit choice rather than a default.

Sources

  1. claude-agent-sdk-python v0.2.158Anthropic · 2026-09-23
  2. Add verbatim_prompts option to deliver prompts as writtenAnthropic · 2026-09-17
  3. Python Agent SDKAnthropic
  4. subprocess_cli.pyAnthropic

claude-codeclaude-agent-sdkprompt-injectionpermissionssdk