Summary
SubprocessCLITransport._build_command at src/claude_agent_sdk/_internal/transport/subprocess_cli.py:209-220 only accepts system_prompt as one of:
str
None
{"type": "file", "path": ...} (SystemPromptFile)
{"type": "preset", "append": ...} (SystemPromptPreset)
When a caller passes the list-of-content-blocks form (which is valid at the upstream Anthropic Messages API per the system parameter docs), the transport raises:
AttributeError: 'list' object has no attribute 'get'
File ".../subprocess_cli.py", line 215, in _build_command
if sp.get("type") == "file":
before any model call. The crash happens at command-build time; the user-facing ClaudeAgentOptions(system_prompt=...) doesn't validate the shape, so the failure is delayed and noisy.
Use case
Anthropic's prompt_caching documentation specifically recommends passing system as a list of content blocks so cache_control can be attached to a trailing block and yield layer-independent caching of role vs. injected reference material. We use this pattern to layer (1) a stable role system prompt with (2) per-call retrieval-style skill content. The cache_proxy in our stack handles the list form correctly; only the SDK's bundled CLI transport blocks it.
Worked example we hit today (paid validation, $0.46 burned before crash):
options = ClaudeAgentOptions(
system_prompt=[
{"type": "text", "text": role_system_prompt},
{"type": "text", "text": injected_skill_content},
],
...
)
async for msg in query(prompt=user_prompt, options=options):
...
# AttributeError at SubprocessCLITransport._build_command
Suggested fix
_build_command could:
-
Detect isinstance(sp, list) and serialize via a new flag, e.g. --system-prompt-json <path> pointing at a temp file containing the JSON-encoded list. The bundled claude CLI would parse that and forward as-is to the API.
-
Alternatively, accept the list form on the existing --system-prompt-file path with content auto-detection (string vs. JSON array).
Either keeps the wire format Anthropic-compatible and avoids the silent client-side flatten that today's only workaround (concatenate to string) forces.
Workaround we're using
We collapse the list to a single concatenated string at the runtime layer and document the cost (loss of layer-independent caching) in our ADR. This regresses option 4 ("structured 2-block") to option 3 ("concatenated string") in our chosen design. We'd revert the regression as soon as the SDK forwards list-form.
Reproduction
claude-agent-sdk 0.1.66, bundled CLI 2.1.119:
from claude_agent_sdk import ClaudeAgentOptions, query
options = ClaudeAgentOptions(
system_prompt=[{"type": "text", "text": "hello"}, {"type": "text", "text": "world"}],
model="claude-opus-4-7",
)
async for msg in query(prompt="hi", options=options):
print(msg)
# AttributeError: 'list' object has no attribute 'get'
Related
- Upstream Anthropic API reference:
system parameter accepts string | content_block[] per Messages API docs.
- Caching guidance: prompt caching recommends placing
cache_control on the LAST block of each cacheable layer.
Summary
SubprocessCLITransport._build_commandatsrc/claude_agent_sdk/_internal/transport/subprocess_cli.py:209-220only acceptssystem_promptas one of:strNone{"type": "file", "path": ...}(SystemPromptFile){"type": "preset", "append": ...}(SystemPromptPreset)When a caller passes the list-of-content-blocks form (which is valid at the upstream Anthropic Messages API per the
systemparameter docs), the transport raises:before any model call. The crash happens at command-build time; the user-facing
ClaudeAgentOptions(system_prompt=...)doesn't validate the shape, so the failure is delayed and noisy.Use case
Anthropic's
prompt_cachingdocumentation specifically recommends passingsystemas a list of content blocks socache_controlcan be attached to a trailing block and yield layer-independent caching of role vs. injected reference material. We use this pattern to layer (1) a stable role system prompt with (2) per-call retrieval-style skill content. The cache_proxy in our stack handles the list form correctly; only the SDK's bundled CLI transport blocks it.Worked example we hit today (paid validation, $0.46 burned before crash):
Suggested fix
_build_commandcould:Detect
isinstance(sp, list)and serialize via a new flag, e.g.--system-prompt-json <path>pointing at a temp file containing the JSON-encoded list. The bundledclaudeCLI would parse that and forward as-is to the API.Alternatively, accept the list form on the existing
--system-prompt-filepath with content auto-detection (string vs. JSON array).Either keeps the wire format Anthropic-compatible and avoids the silent client-side flatten that today's only workaround (concatenate to string) forces.
Workaround we're using
We collapse the list to a single concatenated string at the runtime layer and document the cost (loss of layer-independent caching) in our ADR. This regresses option 4 ("structured 2-block") to option 3 ("concatenated string") in our chosen design. We'd revert the regression as soon as the SDK forwards list-form.
Reproduction
claude-agent-sdk0.1.66, bundled CLI 2.1.119:Related
systemparameter acceptsstring | content_block[]per Messages API docs.cache_controlon the LAST block of each cacheable layer.