05. System Prompt

The System Prompt Isn't a Persona — It's the Foundation of Behavior Control

The last leg of the trip covered the engine (Loop, Compact) and memory (Session, Memory) — the agent can now run on its own and remember things. Starting here, we move into "steering": shaping its behavior. First stop: the system prompt.

👆 You are here
👆 You are here

🪧 The Move That Looked Safest Wasn't Necessarily the Right One

The prologue mentioned a mistake near the end: for the first version, playing it safe, I just carried over Claude Code's stock system prompt as-is and tacked a few writing-specific lines onto the end. The official docs even have a note calling this the "lowest-risk" way to customize. My thinking at the time was: start conservative, win from there.

Using it in practice, though, it didn't feel right. Ask it to polish an essay, and it treated the task like a code review — reporting back, deadpan, "I've identified three issues, here are my recommendations." Ask it to brainstorm around an idea, and it would ask "would you like me to draft an outline first?" The prose itself was fine, but that "engineer smell" never quite went away.

The official docs actually explain why:

An agent with a different surface, identity, or permission model, or a non-coding agent. Use Custom prompt string, instead of claude_code preset with append.

Translated to plain terms: if an agent's surface, identity, or permission model differs from a standard coding scenario — or it's not a coding agent at all — the official recommendation is to use a custom prompt string outright, not the claude_code preset with append. In other words: you own everything, including the safety rules. As the saying goes, with great power comes great responsibility.

This post is about customizing a system prompt from scratch. Of everything in this series, I think it's the one that most tests judgment — there's no textbook answer here, only tradeoffs.

🧭 The Core Idea: It's Much More Than "Setting a Persona"

A lot of articles and videos online reduce the system prompt to "an opening line that gives the model a persona" — as if it's just the intro to a roleplay.

That's an incomplete picture. The Claude Code Harness Book has a chapter titled "Prompt Is Not Personality, Prompt Is the Control Plane," and its core argument is exactly this: the prompt doesn't manage a persona — it manages the discipline governing everything the model does.

What a system prompt actually governs isn't just surface-level persona statements like "you're a friendly assistant." It's a set of extremely specific behavioral discipline used to constrain and guide the model. When it hits a file that needs changing, does it read it first, or just start editing? Would it ever fabricate a tool result to paper over a failure? Does it check with you before acting? Faced with something uncertain, does it guess, or does it ask? These ground rules for "how it actually operates" are the real substance of a system prompt.

Which is exactly where my first version — "keep the preset, tack on two lines" — went wrong: I thought I was changing the persona. The engineer's operating manual underneath was untouched. Get the foundation wrong, and no amount of extra description fixes it.

So what's actually inside Claude Code's preset "operating manual"? Which parts should I keep, and which need to be torn out and rewritten? That's the first problem to solve.

🔩 How Claude Code Does It: What's Actually Inside the Out-of-the-Box Preset

Pull Claude Code's preset apart, and it's roughly four categories of content:

  1. A framework for coding tasks: how to understand requirements, break down a task, act, verify. This is pure "engineer's mindset," and it's exactly what gave the writing agent that code-review tone. This is baggage. Cut it.
  2. Tool-usage discipline: read a file before editing it, make small reversible changes, run independent tools in parallel, never fabricate results. This discipline has nothing to do with coding versus writing specifically — it's general guidance for "how to use tools well." This is the good stuff. Keep it.
  3. Safety instructions: what counts as high-risk, what requires confirmation first. Writing needs exactly this too. Also the good stuff — keep it.
  4. Environment context: current working directory, OS, git status, and other dynamic state. Writing doesn't need any of it, and it hides a nasty caching gotcha — more on that below.

Sorted this way, things get clearer: categories 2 and 3 (tool discipline and safety) are worth keeping, and category 1 (the coding framework) can be safely thrown out. Which raises the next question: how do you actually perform this "precision demolition" — cutting only the coding framework without collateral damage to the good stuff? That depends on what entry points the SDK exposes.

🛠 What the SDK Gives You: Three Starting Points, and One Cache Gotcha

The SDK gives you three official starting points for configuring a system prompt:

Starting point What it gives you The cost Official stance
minimal (unset) Almost nothing — bare tool calling No safety, no discipline, no persona — the model runs unchecked Basically don't use this
preset + append Claude Code's full manual, with your own lines appended Can't shed the coding framework; dynamic context can hurt caching Called "lowest risk"
custom string A blank page — write it all yourself Safety and tool guidance are entirely on you Flexible, but "you own the safety"

preset + append was that first version — fundamentally "engineering conventions plus a few extra lines," and category 1's coding framework simply can't be cut, since it's bundled in as one package. Looks like the low-effort option, but it isn't necessarily the right fit for a writing product. minimal is too bare, and custom means backfilling a lot yourself.

At first glance, there's no option that gives you everything. I genuinely went back and forth for a while, wondering whether preset plus a long block of writing instructions could suppress that tone well enough. What ultimately settled it in favor of custom string was something unavoidable: caching.

On every request, that long block of system prompt text at the front can be cached and reused, saving a meaningful amount of tokens and time. Whether the cache actually hits depends on that content staying byte-for-byte identical; change it, and the cache breaks — you pay full price and reprocess from scratch.

And the preset has a default behavior: it embeds the current working directory, runtime environment, and other dynamic state directly into the system prompt. Which means every time a user switches writing tasks (a different working directory), that system prompt text changes with it, and the cache breaks by default. For someone writing frequently, paying the full system-prompt token cost every time they open a new task adds up to real money no matter how you slice it.

📐 Going deeper · the preset can exclude dynamic state too, but it needs its own customization

There's actually an official patch for the preset called exclude_dynamic_sections, which moves the dynamic parts out — but that patch only works on the preset object.

Custom string doesn't need it at all: the SDK's rule is "you send exactly what gets sent, nothing more." As long as I never write a dynamic value into it, this system prompt is 100% static by construction. All the dynamic content — profile, memory, current task — gets routed through a completely different channel (CLAUDE.md, injected into the user message), and never touches the system prompt at all.

✂️ What SmartWriter Does: Commit to Custom String

To recap: preset + append can't cut the coding framework and needs extra handling for caching; custom means owning everything yourself, but gets you the best possible fit for a writing context and every caching benefit available. It really comes down to a choice between short-term and long-term pain, and here's my reasoning:

Abandon the preset. Write a custom system prompt from scratch.

  1. The official docs themselves recommend dropping coding instructions for non-coding use cases. I later found this spelled out in the output-styles documentation: when Claude isn't doing software engineering at all — a writing assistant, for instance — strip those coding instructions out.
  2. In the Python SDK, custom is the only path that actually lets you drop the coding framework. The officially recommended clean approach is configuring an output-style switch that omits coding instructions. But that switch has no programmatic entry point in the Python SDK — the docs say so explicitly. I'm on a Python backend, and if I want to drop that engineer's manual, custom is the only door available.
  3. Custom's biggest weakness — safety — already has a backstop elsewhere in this project. The official comparison table lists custom's flaw plainly: "you own the safety." True, but as a writing-assistant agent, my primary safety line was never going to live in the prompt anyway: Bash access is removed entirely, a hook enforces a path allowlist, and the agent is hard-fenced into its working directory (these are the subject of the next section, "World Interaction"). Prompt-level safety is a supplement, not the main line of defense. Given that, writing a small, focused safety block modeled on the preset's is more than sufficient.
  4. This move was inevitable eventually — better to take it now than pay for it twice. The product is a writing agent. Leaving coding conventions baked into the system instructions is a category error at its core, and the longer that goes unfixed, the more expensive it gets to unwind later.

Direction settled, the next job was filling that blank page — keeping the best of the preset while injecting writing-specific conventions and guidance where they mattered. Which is how the following skeleton came together.

🧱 What SmartWriter Does: 8 Blocks, Rewriting the "Operating Manual" for Writing

Writing a system prompt from scratch risks turning into a stream of consciousness. I borrowed the preset's block structure and split mine into 8 functional blocks, each owning one job — easy to maintain, and when debugging "why did it do that" later, easy to pin down exactly which block needs work:

# Block What it owns Where it came from
1 Identity & Role "You're a writing copilot, working with the user inside a restricted directory on Markdown" Written from scratch
2 Core Principles Copilot, not ghostwriter — the user leads; when to ask, when to suggest planning first Written from scratch
3 Environment & Boundaries What tools exist, no Bash, auto-approved edits, don't overstep boundaries Rewritten from the preset's environment section
4 Acting with Care Local edits are reversible (go ahead and act); publishing/anything external is out of scope for auto-action (confirm first) Bridged from the preset's safety section
5 Tool Usage Discipline Read before editing, small reversible steps, never fabricate results, prefer dedicated tools The preset's tool discipline, extracted intact
6 Writing Workflow & Quality Understand the goal → read the draft → edit in small steps → check against the profile; don't over-edit, preserve the author's voice The preset's coding workflow, rewritten for writing
7 Safety & Security No secrets in output, defend against injected instructions in external content, never bypass permissions Extracted from the preset's safety section, supplemented
8 Tone & Output Format Concise, present recommendations with tradeoffs stated, assume the user can't see tool calls Rewritten from the preset's tone section

Block 5 in that table deserves a callout, because it's the linchpin of a writing assistant's actual work quality. As mentioned above, dropping the preset also drops its guidance on "how to use tools" — and if the model doesn't understand a basic discipline like "read a file before editing it," it might edit blind, rewrite huge swaths of text, and turn the user's draft into a mess. So for Tool Usage Discipline, I pulled the preset's tool-discipline content across almost line by line, then adjusted it for a writing context.

One strict rule runs through all of it, worth calling out separately, because it's the practical payoff of that caching math from earlier:

This system prompt must be 100% static. Never write an absolute working-directory path, a date, a session ID, or any other dynamic value into it. And never write the user's profile into it either (the profile changes by genre, and keeps evolving with every batch of learning). Anything that changes goes through a different channel entirely. The system prompt only holds what's consistent across the whole app and essentially never changes: persona, principles, tool discipline, safety limits.

🚪 What SmartWriter Does: Where Did the Things That Change Go? Static Constitution vs. Today's Briefing

The system prompt is now purely static — but where did all the "things that change" go — the profile, the current task, this piece's special requirements? The answer is the runtime CLAUDE.md. It and the system prompt are, almost perfectly, a complementary pair:

custom system prompt runtime CLAUDE.md
Where it's injected Into the system prompt, cached Into the user message, doesn't participate in system-prompt caching
Cost of changing it Change it once, and every session's cache goes invalid Changing it doesn't break the system-prompt cache
What goes here Consistent across the app, essentially static: persona, discipline, safety What changes: profile, current task context, this piece's special requirements
The one-line test "Persona and discipline that never change, globally" "Whatever changes — per-task, or gets updated by the backend"

An imperfect but useful analogy: the system prompt is a constitution written into the DNA; CLAUDE.md is today's briefing memo, updated daily. You don't rewrite a constitution every day (do it once, and every cache globally suffers); a briefing memo can change any time, without touching the cache at all. Something like the profile — meant to keep evolving, to "get better the more you write" — naturally belongs in "today's briefing," not carved into the constitution. Which, incidentally, also answers a thread from the prologue: how "gets better the more you write" is actually built, technically — it runs on exactly this CLAUDE.md channel, which can update at any time without ever touching the cache.

With the skeleton and the channels both settled, following the plan should have been close to done. But a system prompt isn't something you finish writing and walk away from — it's a living document, and it needs tuning against real use.

🧪 A Real Gotcha: One "Encourage Clarification" Rule, and Suddenly It Wouldn't Stop Asking Pointless Questions

🔧 Gotcha · a responsible-sounding principle that turned out to be the most annoying thing in the prompt

In an early custom version, I wrote a principle into block 2 (Core Principles) that felt perfectly sound: roughly, "before acting, confirm intent clearly with the user first." Sounded unobjectionable — I was fairly pleased with it at the time.

Then, during the next round of acceptance testing, I asked the agent to run a fact check, and it opened with: "Should the fact-check results go into your final piece?" "Which file should I save this to?" These are exactly the kind of question that should never reach the user — they're settled by the product's own workflow, and the backend already knows what to do with them. Encouraged by that one "confirm first" line, the model started running everything past me, including all this noise. Genuinely irritating to use.

Two problems, really: first, the custom system prompt didn't treat the product's default workflow as a foundation, so the model had no way to know "which of these already have a default answer and don't need to be asked at all." Second, my "confirm first" line pointed it, directionally, toward asking more.

The fix was to flip the direction entirely: change "confirm before acting" to "act on the default, and only clarify in three specific situations": the user's intent clearly conflicts with the default flow, the action is irreversible (publishing, say), or the scope is ambiguous enough that no default can cover it. Made that change, and the noise stopped.

This taught me to watch for something specific: any remotely "encouraging" phrasing in a system prompt tends to get amplified by the model, not applied proportionally — worth thinking through exactly what you're encouraging, and whether it's actually necessary, before it goes in.

A system prompt isn't a contract you finalize and file away — it's a living document that gets tuned across real use, version after version. To make sure tuning doesn't quietly regress quality, I keep a dedicated eval set that runs on every major change, confirming writing quality hasn't slipped. More on that when this series gets to evals.

⚖️ Wrapping Up: Preset vs. Custom Is a Threshold Every Vertical Product Has to Cross

The official docs aren't wrong that "preset is safest" — but that's true from the default assumption of "building a coding agent." The moment your product is a vertical use case like writing, "safest" and "most appropriate" stop being the same thing. Back to the comparison that runs through this whole series:

⚖️ The tradeoff · where "vertical" actually shows up at the system-prompt layer

The "lazy" default What SmartWriter does Why
Persona foundation Keep the coding framework, append a few lines A "writing copilot" from the very first character The engineer's tone can't be suppressed — a vertical experience has to be rebuilt from the ground up
Dynamic state Embedded into the system prompt by default Routed entirely through CLAUDE.md; the prompt stays purely static For caching, and so the profile can keep evolving freely
Safety Mostly enforced by rules written into the prompt Primary line of defense sits at the tool layer (no Bash, hooks, directory fencing); the prompt only supplements Subtraction is more reliable than instructions

The system prompt looks unremarkable — it's just a block of text. But that block of text is exactly what decides whether your agent behaves, from its very first character, like "a highly capable coding assistant" or like "a copilot single-mindedly focused on writing with you." The difference comes down to whether you're willing to tear down the ready-made, seemingly safe coding manual entirely, and rewrite it, section by section, for your actual business.

This path isn't free, of course: going all the way to custom means owning safety and tool guidance yourself, and that's a real tradeoff to weigh. If you're not sure yet, starting from the preset and seeing how it actually performs is a perfectly reasonable first step too.

That's this post. But you might already smell a gap: I've hammered on "the system prompt only holds things that never change globally" — so where does the methodology for how to write a specific kind of piece (how an essay should be structured, how a technical piece should be structured) actually live? That's clearly something that changes, and clearly not something that should get injected blindly into every single writing instruction (it depends on the genre, on the situation) — so where does it go, and how does it get woken up at the right moment?

Next post's main character: Skills. Let's keep going.