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
Show as text
| # | Layer | Note |
|---|---|---|
| 1 | App composes a user message | Untrusted text may already be inlined here. |
| 2 | SDK stamps `client_composed: true` | Only when `verbatim_prompts` is enabled. |
| 3 | Claude Code delivers the text as written | No `@path` expansion or slash commands. |
| · | transform boundary (breakpoint) | |
| 4 | `@/absolute/path` expansion | Default behavior when the flag is off. |
| 5 | Slash-command dispatch | Default behavior when the flag is off. |
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
Show as text
| # | From | To | Message |
|---|---|---|---|
| 1 | Your app | Claude Code CLI | Prompt contains `@/tmp/secret.txt`. The text could have come from a prior turn or a third party. |
| 2 | Claude Code CLI | Claude Code CLI | Default mode expands the local file. The patch says this happens without a tool call. |
| 3 | Claude Code CLI | Your app | Model sees file contents. Even if tools are empty. |
| 4 | Your app | Claude Code CLI | Same prompt with `client_composed: true`. Set by `verbatim_prompts`. |
| 5 | Claude Code CLI | Your app | Model sees the literal `@/tmp/secret.txt`. No hidden file read and no slash-command dispatch. |
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.
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.
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.
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.
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.
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.
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.