Anatomy of the Claude prompt that writes this blog
The actual system prompt we use to generate EdsDev posts, broken down section by section. Voice rules, forbidden words, JSON shape, and the parts that still don't work.
- ↳Banning specific words like 'delve' and 'leverage' does more than any positive instruction about voice.
- ↳Forcing the model to name real tools, prices, and dates kills most AI slop before it starts.
- ↳Returning structured JSON in one shot beats chaining prompts for a single-author blog.
- ↳The model still cheats: it sneaks in rule-of-three lists and 'not just X, it's Y' when you're not looking.
I have rewritten the system prompt behind this blog about nine times since August. Every version sounded more like a LinkedIn post than the last one. The current version is the first one I don’t actively hate, and it works because most of it is a list of things Claude is not allowed to do.
This post is the prompt, taken apart. If you are running any kind of AI content pipeline (Cursor-generated docs, marketing pages, internal wikis), the same shape probably applies to you.
Why one giant prompt instead of a chain
The fashionable answer is to chain: outline agent, draft agent, editor agent, fact-check agent. I tried that. For a single-author blog with a fixed voice, it was worse. Each agent re-introduced the slop the previous one removed, because each agent had to re-derive the voice from scratch.
One prompt, one call, one JSON object. Claude Sonnet 4.5 through the Anthropic API, temperature around 0.7. The whole post (body, FAQs, cover art prompt, tags) comes back in a single response. No orchestration, no LangGraph, no vector store. A Cloudflare Worker hits the API, validates the JSON against a Zod schema, writes the MDX file, opens a PR.
The entire pipeline is maybe 200 lines of TypeScript. The prompt itself is longer than the code that runs it.
The voice section is mostly negative space
Here is the part that does the most work:
WHAT TO AVOID:
- Words: delve, leverage, foster, garner, intricate,
tapestry, landscape (figurative), pivotal, crucial,
seamless, robust, vibrant, transformative, paradigm,
ecosystem (figurative), unleash, elevate.
- Phrases: "a testament to", "stands as", "serves as",
"in today's rapidly evolving", "it's not just X, it's Y".
- Em dashes. Use commas, parentheses, or periods.
- Rule of three lists when two or four would do.
I added every one of those words after seeing it appear in a draft. The list grew like a bug tracker. “Tapestry” showed up in a post about Stripe webhooks. “Pivotal” appeared three times in 800 words about Hetzner pricing. “Seamless” was in literally every draft until I banned it.
The positive instructions (“write like a tired senior engineer”, “vary sentence length”) help a little. The bans help a lot. Claude has strong priors about what “good blog writing” looks like and those priors are LinkedIn-shaped. You have to actively suppress them.
One thing that doesn’t work: telling the model to “be direct” or “avoid corporate tone.” Those words have no edges. “Don’t use the word delve” has a hard edge.
Forcing specificity with examples
The prompt has a rule that every claim needs an anchor: a number, a name, a date, a tool, a price. The example I give in the prompt is:
“We shipped 12 versions of Photo AI Studio in 2024” beats “we ship a lot”.
This single line changed the output more than any other instruction. Without it, Claude writes “we iterate quickly on our products.” With it, the model goes hunting for a specific number to drop, even if it has to invent it (which is its own problem, more on that below).
I also pass a list of real proper nouns the model is allowed to name: Cursor, Claude Code, FAL, Photo AI Studio, Interior AI Designs, Zoltan AI, Hetzner, Cloudflare, Stripe, RevenueCat, Resend, Turnstile. This gives the model a vocabulary of real things to anchor to instead of “a popular AI tool” or “a leading hosting provider.”
The risk: hallucinated specifics. Claude will happily invent a statistic if you ask for one. The mitigation is the sources field in the JSON output, plus an explicit instruction: if you cannot cite a real URL, return sources: [] and skip the claim. This works maybe 85% of the time. I still read every post before it ships.
The JSON shape is the contract
The output schema is the most boring part of the prompt and the most important.
{
title: string, // 40-110 chars
description: string, // 140-220 chars
tags: string[], // 3-5
coverImagePrompt: string, // for FLUX via FAL
takeaways: string[], // 3-5, read aloud by Google Assistant
contentMarkdown: string, // 900-1400 words
faqs: { question, answer }[],
sources: { title, url }[]
}
Few things matter here. The character ranges on title and description are tight enough that Claude actually respects them, loose enough that it doesn’t truncate awkwardly. The word range on the body (900-1400) is paired with an explicit instruction: “if you’re padding to hit 1400, stop.” Without that line, every post was 1380 words and the last 200 were filler.
The takeaways field has a sneaky purpose. I tell the model these get read aloud by Google Assistant for voice search. That single sentence makes the takeaways shorter, more declarative, less marketing-flavored. Whether Google actually reads them aloud is beside the point. The instruction shapes the output.
The cover image prompt is its own little prompt
For cover art I use FAL with a FLUX model. The blog prompt asks Claude to generate a sub-prompt for the image model:
coverImagePrompt: "Editorial illustration of a dissected
mechanical typewriter on a teal drafting table,
exposed gears and red ink ribbon, top-down view,
restrained palette"
The constraints in the prompt are: no text in the image (FLUX is bad at text), restrained palette, warm-red and teal accent (our brand colors), 20-200 chars. Asking Claude to write the image prompt instead of writing one myself means the image always matches the post topic, which I would otherwise forget to do.
What still doesn’t work
Three things the prompt cannot fix.
First, Claude smuggles in the rule of three. I ban it explicitly. It still appears, usually in the second paragraph of a section, in the form “faster, cheaper, and more reliable.” I think this pattern is too deeply baked into the training data to fully suppress.
Second, the model is bad at expressing genuine uncertainty. I ask for lines like “I genuinely don’t know how to feel about this.” What I get back is sometimes that exact line, verbatim, which is its own kind of slop. Authentic doubt is hard to fake.
Third, the model cannot fact-check itself. If I ask for a statistic about LLM coding productivity, it will generate one and confidently cite a plausible-sounding URL that 404s. The sources: [] fallback helps. Reading the post before publishing helps more.
The whole thing in one place
If you want to use a similar setup for your own blog or docs site, the shape that worked for me:
- One prompt, one API call, structured JSON output.
- A long list of banned words and phrases, grown from real drafts.
- Explicit anchor rule: every claim needs a number, name, or date.
- A vocabulary of real proper nouns the model is allowed to use.
- An empty-array escape hatch for sources, so the model doesn’t fabricate.
- A human read before publish. Always.
The prompt is maybe 80% of the quality. The other 20% is me catching the rule-of-three list the model still sneaks in.
If you’re building something similar and want a second pair of eyes on your prompt or pipeline, get in touch.
Common questions
▸Which Claude model do you use and at what temperature?
Claude Sonnet 4.5 through the Anthropic API, temperature around 0.7. I tried 0.3 and the output got mechanical and repetitive across posts. At 0.9 it started inventing tool names. 0.7 is the sweet spot for this prompt. I also set a max_tokens of 8000 to leave room for the full JSON payload including the body, FAQs, and sources array.
▸How do you stop Claude from hallucinating sources?
An explicit instruction: if you cannot cite a real URL for a claim, return sources as an empty array and skip the claim. This works most of the time but not always. The fallback is a manual review before the post ships. I also check every URL with a HEAD request in the pipeline. Dead links get flagged and the post goes back for review.
▸Why not use a multi-agent setup with a separate editor?
I tried it. Each agent re-introduced the slop the previous one removed because each agent had to re-derive the voice from the system prompt. For a fixed-voice blog by a single author, one prompt with strong constraints beat the chain. Multi-agent might still win for longer pieces, research-heavy work, or content with many contributors. For 1000-word opinion posts it was overkill.
▸How long did it take to tune the prompt?
About six weeks of iteration, mostly in evenings. The first version was 400 words and produced LinkedIn slop. The current version is around 1500 words. Most of the growth came from adding banned words after seeing them in drafts. The single biggest improvement came from the anchor rule: every claim needs a number, name, or date. That one line changed the output more than anything else.
▸Can I see the full prompt?
The post above quotes the parts that do the most work: the banned word list, the anchor rule, the JSON schema, and the cover image constraints. The full prompt has more boilerplate around tone and structure but no secret sauce. If you want to compare notes on your own setup, send me an email through the contact page.
- [1]Anthropic API documentation: prompt engineeringdocs.anthropic.com
- [2]
Related posts
Anatomy of the Claude prompt that writes this blog
A line-by-line look at the system prompt we use to make Claude write posts that don't sound like Claude. What worked, what didn't, and why the banned-word list keeps growing.
An AI lead generation agent that books meetings while you sleep
How we build AI lead generation agents that actually book meetings: the workflow, the prompts, the guardrails, and the bits that quietly break at 3am.
Shipping an MCP server so agents can email us
We built a tiny MCP server that lets Claude and Cursor send us email through Resend. Here's the actual code, the auth headaches, and what broke in production.