| name | claude-sdk-system-prompt |
| description | System prompts in the Claude Agent SDK โ string vs file vs preset, the claude_code preset and its append field, dynamic prompts via per-turn rebuilding, and how setting_sources composes user/project/local instructions. Read when designing the agent's voice and operating principles, when migrating an existing prompt into the SDK shape, or when choosing between a static and a dynamic prompt. |
System prompts in the Claude Agent SDK
The SDK accepts the system_prompt field in three different shapes. Pick by use case, not by what looks shortest:
| Shape | Use when | Example |
|---|
| String | The prompt is short and stable | system_prompt="You are a helpful assistant." |
{"type": "file", "path": "..."} | The prompt is long and edited often | system_prompt={"type": "file", "path": "prompt.md"} |
{"type": "preset", "preset": "claude_code", "append": "..."} | You want Claude Code's tool-using personality plus your domain instructions | (below) |
File mode โ the right default for production
Inline string prompts grow until they're unmaintainable. Move to a file as soon as the prompt is more than 5 lines:
system_prompt={"type": "file", "path": "my_agent/prompt/system_prompt.md"}
Benefits:
- Edit the prompt without touching code
git diff shows prompt changes clearly
- Reviewers can read it without scrolling through Python
- Markdown rendering in editors makes structure visible
The SDK reads the file every time the option is constructed, so you can edit it and restart the process. Hot-reloading mid-session requires rebuilding ClaudeAgentOptions and starting a fresh ClaudeSDKClient.
The claude_code preset
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": "You're a SQL specialist. Prefer CTEs over subqueries. Always validate schema before writing migrations.",
}
This gives Claude:
- The
claude_code system prompt (proactive, tool-using, error-handling personality, autonomy norms)
- Your
append text added at the end
Use this when your agent should act like Claude Code โ multi-step planning, tool use without permission-asking, terse status updates. Don't use it when you want a different personality (a customer-service voice, a teaching voice, a strict review voice). The base personality is opinionated and bleeds through.
Dynamic prompts โ rebuild per turn
The SDK doesn't support "dynamic system prompt at runtime" as a first-class feature (no InstructionProvider like ADK). The workaround is to rebuild ClaudeAgentOptions and start a fresh client per turn:
async def run_turn(prompt: str, user_id: str):
options = ClaudeAgentOptions(
system_prompt=build_dynamic_prompt(user_id),
)
async with ClaudeSDKClient(options=options) as client:
await client.query(prompt)
async for msg in client.receive_response():
yield msg
You lose session continuity (each turn is a fresh session) โ that's the cost of dynamic prompts. For multi-turn dynamic agents, use the SDK's session_id + resume to thread the conversation across these per-turn rebuilds.
A common middle ground: keep the base system prompt static and inject per-turn context via a UserPromptSubmit hook (see claude-sdk-hooks). Faster, simpler, no session juggling.
setting_sources and how skills layer in
options = ClaudeAgentOptions(
system_prompt=...,
setting_sources=["user", "project", "local"],
)
This loads CLAUDE.md files and .claude/skills/ directories from those layers โ exactly like Claude Code does. user means ~/.claude/, project means the project root's .claude/, local means the current directory.
The system prompt you set via system_prompt is composed with whatever Claude finds in those sources. So you don't have to embed your skills' content in the system prompt โ drop SKILL.md files into .claude/skills/<name>/ and they're discovered.
For nuvel-scaffolded SDK agents, setting_sources=["project"] is the sensible default โ it picks up the project's .claude/ without leaking the user's home-directory settings into a server deployment.
Writing a good system prompt
Three sections, in order:
- Identity โ who the agent is, what it's for, who's talking to it.
- Operating principles โ concrete behaviors. "Use tools to take action, don't ask permission." "Cite tool outputs." "Surface ambiguity in one sentence; don't block."
- Guardrails โ things to never do, in declarative form. "Never delete user data without explicit confirmation."
Avoid the three patterns that hurt prompts:
- Long lists of "do this" โ Claude weights the last item. Pick 3-5 principles, ordered by importance.
- Telling Claude its capabilities ("you can use tools, you can read files") โ the SDK already tells it. Redundant text dilutes the prompt.
- Personality without purpose ("you are a friendly assistant who loves to help") โ describes vibes, not actions. Replace with verbs.
Common mistakes
- Inlining a 200-line prompt as a Python string. Use file mode.
- Using
claude_code preset for non-Claude-Code agents. The personality is too strong; users feel like they're talking to a coding agent.
- Dynamic prompts via Python f-strings rebuilt every turn. Works but breaks session continuity. Use
UserPromptSubmit hook injection for per-turn context, or session_id/resume to bridge.
- Hardcoding tool names in the prompt. Claude already sees the tool list; mentioning specific tools in the prompt creates drift when you add/remove them.
Quick reference
system_prompt={"type": "file", "path": "prompt/system_prompt.md"}
system_prompt={"type": "preset", "preset": "claude_code", "append": "..."}
ClaudeAgentOptions(
system_prompt={"type": "file", "path": "..."},
setting_sources=["project"],
)