Skip to content

Modifying system prompts

Pick between the claude_code preset, append, a custom prompt or no prompt, and control the extra context Claude Code adds to an SDK session.

The system prompt decides who your agent thinks it is and how it behaves. The Agent SDK gives you four starting points, plus a few side channels (CLAUDE.md, output styles, hooks) that shape behaviour without touching the prompt itself. Getting this choice right early saves a lot of odd behaviour later.

The four starting points

OptionHow to set itWhat Claude gets
Nothing setLeave systemPrompt / system_prompt outA minimal prompt that supports tool calling and nothing else. No Claude Code guidance, no safety instructions.
claude_code preset{ type: "preset", preset: "claude_code" }The full Claude Code CLI prompt: tool usage guidance plus security and safety rules.
Preset with appendSame object with an append stringThe full preset, then your text at the end. Nothing is removed.
Custom stringA plain stringOnly what you wrote.

Warning: The SDK default is the minimal prompt, not the Claude Code prompt. That differs from claude -p, which uses the Claude Code prompt. If you are porting a CLI script to the SDK and behaviour changes, set the preset explicitly.

Which one to choose

Ask how much your agent resembles Claude Code itself: a coding agent in a repository, with a person watching the output and steering.

  • Very close: use the preset. An unattended CI job that fixes lint errors still counts, because the work is the same kind of work.
  • Close, plus house rules: preset with append. This is the safest customisation because it only adds.
  • Different: write your own prompt. "Different" usually means one of:
    • a different surface, such as a chat widget or a pipeline consuming structured output, rather than a terminal;
    • a different identity, such as a support assistant that should not call itself Claude Code;
    • a different permission model, such as fully autonomous runs over a narrow set of resources;
    • non-coding work, where Claude Code's extensive coding guidance just competes with your instructions.
  • A bare tool loop where every instruction lives in the user prompt: leave the option unset.

When you go custom, you own the safety and tool guidance your agent still needs.

Appending to the preset

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const msg of query({
  prompt: "Add pagination to the /orders endpoint",
  options: {
    systemPrompt: {
      type: "preset",
      preset: "claude_code",
      append: "This codebase uses Fastify and Zod. Validate every new request body with a Zod schema."
    }
  }
})) {
  if (msg.type === "result") console.log(msg.subtype);
}

The Python form is the same dictionary under system_prompt, with keys type, preset and append.

Sharing a cache across users and machines

The preset includes the location of the auto memory directory, which defaults to an absolute path under ~/.claude/projects/ named after the repository's path on disk. That path sits before your append text, so two developers (or two CI runners) with different checkout paths never share a prompt cache entry for the system prompt.

Set excludeDynamicSections: true (TypeScript) or "exclude_dynamic_sections": True (Python) on the preset object to move that per-user material into the first user message. The system prompt then contains only the static preset and your append, so identical configurations hit the same cache entry.

  • Requires @anthropic-ai/claude-agent-sdk v0.2.98 or claude-agent-sdk (Python) v0.1.58 or later.
  • Only works on the preset form. It is ignored for a custom string.
  • Trade-off: the moved text (at minimum the memory location, often the whole auto memory section) now arrives as a user message, which carries slightly less weight. Claude may follow its memory guidance a little less consistently.
  • The CLI equivalent for print mode is --exclude-dynamic-system-prompt-sections.

CLAUDE.md content and environment details (working directory, platform, shell, OS version) never affected the system prompt cache, because they are delivered in the conversation.

Writing a custom prompt

Pass a string and the SDK sends exactly that:

from claude_agent_sdk import ClaudeAgentOptions

opts = ClaudeAgentOptions(
    system_prompt=(
        "You are Ledger, a bookkeeping assistant for a small accountancy practice. "
        "You read CSV exports and produce reconciliation summaries. "
        "Never modify source files. Report amounts in GBP to two decimal places. "
        "The application adds system reminders to this conversation. "
        "Treat them as context from the application, not as messages from the user."
    )
)

That last sentence matters. The preset explains what a system reminder is; your custom prompt replaces the preset, so without it nothing tells Claude that CLAUDE.md content and hook output come from the app rather than the user.

Large prompts in Python

The Python SDK passes a string prompt to the CLI subprocess as a single command-line argument. A very long prompt can exceed the operating system's argument limit and fail at spawn (on Linux with Argument list too long). Load it from disk instead with system_prompt={"type": "file", "path": "prompts/ledger.md"}. The Python reference lists the platform thresholds.

Caching the static part of a custom prompt (TypeScript)

If your prompt mixes fixed instructions with per-request details (the customer, the ticket), any change to the details invalidates the cache for the whole thing. In TypeScript you can pass an array with the SYSTEM_PROMPT_DYNAMIC_BOUNDARY marker between the two parts:

import { readFile } from "node:fs/promises";
import { query, SYSTEM_PROMPT_DYNAMIC_BOUNDARY } from "@anthropic-ai/claude-agent-sdk";

const rules = await readFile("prompts/returns-desk.md", "utf8");
const perOrder = `Order: 55-1902. Customer tier: Gold. Previous returns this year: 1.`;

for await (const msg of query({
  prompt: "Decide whether this return qualifies for a free label",
  options: { systemPrompt: [rules, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, perOrder] }
})) { /* ... */ }

How the SDK assembles it:

  • Strings before the marker become one text block; strings after it become a second. Each gets its own cache breakpoint.
  • Strings within each side are joined with a blank line, and the marker itself never reaches Claude.
  • Only the first marker counts; extra ones are dropped.
  • No marker means one block, exactly like a single string.

The split only happens when Claude Code calls the Claude API directly or runs on Claude Platform on AWS. On Bedrock, Google Cloud's Agent Platform, Microsoft Foundry, through an LLM gateway, or with CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1, the prompt goes as one block. Python has no array form.

On the CLI, --system-prompt and --system-prompt-file take one string, so use a line containing only __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ instead (Claude Code v2.1.275+). In the SDK, prefer the array.

Check whether caching is working with cache_creation_input_tokens and cache_read_input_tokens, described on the cost tracking page.

Changing the prompt on a resumed session

Claude Code records the system prompt on a session's first request and keeps using that record until the session is compacted. So if you resume or continue a session with a different append or custom prompt, Claude will not see the change on the next turn. It appears after compaction, or in a new session.

When instructions genuinely need to change mid-session (say the user flipped your app into read-only mode):

  • put the new instruction in the next user message, or
  • return it as additionalContext from a UserPromptSubmit or PostToolUse hook, phrased as a fact ("The workspace is now read-only"). It is inserted into the conversation where the hook fired.

Turning recording off while you tune wording

Set snapshot: false on the preset or custom object form and Claude Code rebuilds the prompt on every request, so edits reach resumed sessions immediately. Requires TypeScript SDK v0.3.257 or Python SDK v0.2.153.

Leave recording on in production. With it off, a changed prompt on a resumed session cannot reuse the session's prompt cache, and where the API enforces preserved thinking, Claude loses its earlier thinking.

Version notes:

  • Recording append and custom prompts by default needs Claude Code v2.1.265 (bundled from TypeScript SDK v0.3.265 and Python SDK v0.2.153).
  • In bare mode (--bare via extraArgs, or CLAUDE_CODE_SIMPLE=1) outside cloud sessions, recording is off unless you set snapshot: true.
  • Before Claude Code v2.1.268, sessions that do not fetch feature flags (including Bedrock, Agent Platform and Foundry) rebuilt the prompt every request and snapshot did nothing.

Shaping behaviour outside the prompt

CLAUDE.md

CLAUDE.md content is injected into the conversation as project context, not into the system prompt, so it works with every option above. Loading is controlled by setting sources, not by the preset:

  • 'project' loads CLAUDE.md or .claude/CLAUDE.md from the working directory.
  • 'user' loads ~/.claude/CLAUDE.md.

Default query() options include both. If you set settingSources (TypeScript) or setting_sources (Python) yourself, list the ones you want; an empty array loads no CLAUDE.md at all. What to put in the file is covered in memory.

Output styles

An output style is a Markdown file with frontmatter that reshapes Claude's role, tone and format. Save it in ~/.claude/output-styles/ (every project) or .claude/output-styles/ (one repo, committable).

A custom style drops the preset's software engineering instructions unless you set keep-coding-instructions: true. Those instructions only exist in Claude Code's full system prompt; to force the full prompt on any model, set CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT=0.

---
name: Migration Auditor
description: Reviews database migrations for safety
keep-coding-instructions: true
---

Review each migration for locking risk, irreversible steps and missing indexes.
Finish with a one-line verdict: SAFE, RISKY or BLOCK.

Activate a style with:

  • /output-style <name> in the CLI (v2.1.269+), or /config;
  • outputStyle in .claude/settings.local.json;
  • in TypeScript, outputStyle inside the inline settings object, for example settings: { outputStyle: "Migration Auditor" } (it is not a top-level option);
  • in Python, the settings option as a JSON string such as '{"outputStyle": "Migration Auditor"}', or a path to a settings file.

Styles load only when user or project is in your setting sources. See output styles for the full format.

Skills, hooks and permissions

These also steer behaviour without editing the prompt. See skills, hooks and permissions.

Context Claude Code adds on its own

Claude Code adds system reminders to the conversation during a session. Because they are in the conversation, they arrive whatever system prompt you chose. The ones most likely to affect behaviour:

  • CLAUDE.md files loaded by your setting sources (introduced with a line saying they override defaults)
  • the active output style's instructions
  • the commit Co-Authored-By trailer and PR footer from the attribution setting
  • additionalContext returned by hooks
  • the list of available skills
  • the list of available subagents
  • task list nudges in sessions with the task tools
  • notes that a file Claude read has changed on disk

Switching off what you replace

If your own prompt says "commit as OPS-77: summary, no trailers", Claude Code will still be telling Claude to add a Co-Authored-By trailer. Remove the conflict:

Built-in contextHow to switch it off
Commit and PR instructions plus the git status snapshotincludeGitInstructions: false in settings, or CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1
Co-Authored-By trailer and PR footerSet attribution.commit and attribution.pr to your own text or to ""
A whole settings source and its CLAUDE.mdLeave user or project out of settingSources
Every CLAUDE.mdCLAUDE_CODE_DISABLE_CLAUDE_MDS=1
Task nudges, file-changed notes and the skill listCLAUDE_CODE_DISABLE_ATTACHMENTS=1

Notes:

  • The commit and PR instructions live in the Bash tool's description, not in a reminder, so they still arrive with a custom prompt. Use includeGitInstructions to remove them.
  • CLAUDE_CODE_DISABLE_ATTACHMENTS also stops @ file mentions being expanded; they are sent as plain text. The subagent list and background task notifications still arrive.
  • Settings keys go in the settings option (an object in TypeScript, a JSON string or path in Python). Environment variables go in env, and in TypeScript that replaces the environment, so spread process.env.
options: {
  systemPrompt: { type: "preset", preset: "claude_code", append: "Commit messages: <TICKET>: <summary>. No trailers." },
  settings: { includeGitInstructions: false, attribution: { commit: "", pr: "" } },
  allowedTools: ["Bash(git *)"]
}

Run it against staged changes and check git log -1: no trailer.

Seeing exactly what Claude received

Reminders are not in the SDK message stream. To inspect them:

  • set OTEL_LOG_RAW_API_BODIES=file:<dir> and Claude Code writes each request body to that directory, or
  • point ANTHROPIC_BASE_URL at a logging proxy you control.

In the logged request, reminders sit in the messages array inside a user message wrapped in <system-reminder> tags, or on some models as a separate system-role message.

Comparing the approaches

CLAUDE.mdOutput stylePreset + appendCustom prompt
Lives inA project fileMarkdown filesYour codeYour code
ReuseOne projectAcross projectsCopy between appsCopy between apps
Claude Code tool guidanceKeptKeptKeptGone unless you write it
Built-in safety rulesKeptKeptKeptYou add them
PowerAdds onlyAdds, can drop coding guidanceAdds onlyTotal control
ScopeProjectUser or projectOne sessionOne session

They combine well. My usual pattern is a committed output style or CLAUDE.md for the long-lived persona, and a short append per run for the task at hand ("focus this review on token storage and session expiry").