Skip to content

subprocess CLI rejects list-form system_prompt (Anthropic API supports it) #899

@webrainsec

Description

@webrainsec

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:

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requestgood first issueGood for newcomers

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions