●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Holding the Line on Claude's Output Shape With <output_format> — A Pattern From My Indie App Copy Pipeline
How I keep multilingual App Store copy from drifting across 100+ locales by leaning on the <output_format> tag, with the prompts and validators I actually run.
As a one-person operation I have been shipping iOS and Android apps for years, and at some point the store copy grew past what I could keep maintaining by hand. The hard part was never writing the copy. The hard part was making sure the copy that came back from the model still matched the contract I needed.
People keep asking me what changed when I started using Claude. The honest answer is not "I write faster." It is "I learned how to make the model honor a shape more strictly than a human would." The tool that made that possible was the <output_format> tag. This article walks through how I got there, and the prompts I run today for the indie app copy pipeline.
Four Years of Tuning, and Why I Now Lead With output_format
In the early days I would hand Claude a paragraph and ask it to polish or translate. The output was usually fine, but the variance was unbearable across batches:
A trailing "Hope this helps!" would appear out of nowhere.
Paragraphs would shuffle into a different order than I had asked for.
Localizations would silently exceed App Store byte limits.
An English keyword in the source would survive untranslated in the target locale.
Any one of these is recoverable. A hundred of them in parallel is a half-day lost to cleanup. The mental flip that fixed this was deciding that the shape of the output is more important than the body of the prompt. I tell Claude what the answer should look like before I tell it what to do.
Writing prompts taught me to decide the joinery before cutting the wood: settle the shape of the output before writing a word of instruction. Reordering those two steps is the single change that cut the most rework out of my pipeline. Pick the shape; then say what to do.
Why Plain Generation APIs Drift
Claude's responses are, at their core, a flat text stream. Tools and structured outputs sit on top of that, but the body the model produces does not natively contain a frame that says "title starts here, subtitle there." Three failure modes show up in production over and over:
Length drift. A "keep it under 150 characters" instruction will still produce 180 characters sometimes.
Ordering drift. You ask for A, B, C; you get B, A, C.
Language drift. A multilingual generation accidentally returns English when the source string contained English brand names.
If you patch these downstream with regex, you end up with an unmaintainable parser. I burned a full day doing that before I shifted to the tag-first approach.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A concrete pattern for taming Claude's silent output drift with structural tags
✦Real prompts I use to localize indie app store copy across 100+ locales
✦The mindset shift from 'write a prompt' to 'write a shape, then a prompt'
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The Minimum Shape I Use for Wallpaper App Store Copy
Here is the actual prompt structure I run for one of the wallpaper apps in my catalog. I have stripped product names but the geometry is real.
# claude_app_copy.pyimport anthropicclient = anthropic.Anthropic()SYSTEM_PROMPT = """\You are an assistant that rewrites iOS / Android store copy so it reads as if a \native speaker of the target locale wrote it. You MUST follow the structure \described in <output_format>. Do not add preambles, commentary, emoji, or \Markdown decoration.\"""USER_TEMPLATE = """\<app_meta> <name>{name}</name> <category>{category}</category> <target_locale>{locale}</target_locale> <max_subtitle_chars>30</max_subtitle_chars> <max_promotional_chars>170</max_promotional_chars> <max_description_chars>4000</max_description_chars></app_meta><source>{source_text}</source><output_format><subtitle>...</subtitle><promotional_text>...</promotional_text><description> <hook>...</hook> <features> <item>...</item> <item>...</item> <item>...</item> </features> <closing>...</closing></description></output_format><rejection_rules>- If subtitle exceeds 30 characters, rewrite it to fit instead of returning it long.- Do not place emoji, Markdown, or URLs inside description.- Even if the source contains English, the output must be in target_locale.- Forbidden closings: "Hope this helps", "In conclusion", "Thanks for reading."</rejection_rules>"""def run(name: str, category: str, locale: str, source: str) -> str: resp = client.messages.create( model="claude-sonnet-4-6", max_tokens=4096, system=SYSTEM_PROMPT, messages=[{ "role": "user", "content": USER_TEMPLATE.format( name=name, category=category, locale=locale, source_text=source, ), }], ) return resp.content[0].text
Three design choices matter. First, the <output_format> block contains placeholder slots, not a worked example. Slots are read as "fill this in," and Claude treats them as load-bearing. Second, length limits live in <app_meta> and get referenced again from <rejection_rules>. Stating the same limit twice signals "this is firm." Third, <rejection_rules> is its own tag. Constraints buried in prose are skipped; constraints inside a tag are held.
Five Mistakes I Made on the Way Here
Each of these is a Before/After from my real prompts. They are the lessons that made the pipeline stable.
1. I Was Describing the Shape in Prose
Before:
Please return in this format:subtitle: ...promotional_text: ...description: ...
The tagged version is honored maybe three times as often as the prose version. Claude treats tagged regions as shape-preserving zones in a way it does not for inline instructions.
2. I Was Stuffing Rules Into the Shape Tag
When length limits and forbidden phrasing all lived inside <output_format>, the model could not tell which lines were examples and which were rules. Splitting <app_meta> and <rejection_rules> out as separate tags cut length violations from 20% of outputs down to under 3%.
3. I Was Saying "Don't Do X"
Negative phrasing in prose is weak. Phrasing the same constraint as a declaration inside <rejection_rules> is much firmer. The model reads it as a contract, not a polite ask.
4. I Was Passing Source Text Bare
Wrapping the source in <source>...</source> measurably reduces leakage of English brand keywords into the localized output. Marking the boundary "this is raw material, not a template" stops the vocabulary from bleeding through.
5. I Was Setting max_tokens Too High
Setting max_tokens to 8192 invites verbosity. If your description ceiling is 4000 characters, max_tokens around 2500 keeps the model from wandering. This is not documented anywhere I have found; it is a calibration I picked up from running batches.
Parallel Batches Across 100+ Locales
App Store Connect supports more than 100 locales. Shipping one app worldwide means at least 100 distinct store copies. I run these as a nightly batch during the JST off-peak window (02:00 local). Without <output_format>, the batch falls apart.
# the unsafe versionlocales = ["ja", "en", "ko", "zh-Hans", "de", "fr", ...] # 100+ itemsresults = await asyncio.gather(*[run_async(loc) for loc in locales])
Three failure modes show up specifically at parallel scale. First, output shape drifts per locale: some return <subtitle>, some return subtitle:. Second, length limits hold in some languages and break in others. Japanese hits 28 characters cleanly; German overshoots at 35. Third, sparser source content invites hallucination: Claude will invent feature names or supported OS versions to fill space.
The pattern I now ship looks like this:
Add a one-line locale hint to the prompt. For Korean, "the Korean App Store rejects hyperbolic feature claims more often than other locales, so keep modifiers restrained." This single sentence cuts rejection rates noticeably.
Adjust max_tokens per locale. German and Finnish need around 3500 because of word length; Japanese and Chinese are fine at 2000.
Run a local validator after generation. About 50 lines of Python checks subtitle length and forbidden phrases, and queues offending locales for automatic re-run.
The validator paid for itself within a week. App Store Connect rejections fell to roughly a quarter of what they were. Each rejection costs a few days of round-trip latency, so for an indie developer this works out to about 40 days of recovered time in a year. Forty days is a lot.
How This Plugs Into App Store Connect
Inside App Store Connect each localization has its own subtitle, promotional_text, and description slot. I no longer touch those fields by hand. The batch writes results to Cloudflare R2, I review a dashboard built on top of the diffs, and fastlane deliver does the push.
# pipeline overviewdef run_pipeline(app_id: str, locales: list[str]) -> None: source = load_source_copy(app_id) drafts = {loc: generate_copy(app_id, loc, source) for loc in locales} for loc, draft in drafts.items(): if not validate(draft): drafts[loc] = regenerate(app_id, loc, source, hint="length violation") write_to_r2(app_id, drafts) # human review, then fastlane deliver
The deliberate choice here is not auto-shipping Claude's output to fastlane. The <output_format> is solid, but I still want my own eyes on every locale. My apps are used by people I will never meet, and I want the final pass to stay within reach of my own hands rather than handing it off entirely to automation.
Writing Rejection Rules That Actually Hold
One small craft note about <rejection_rules>. Good guidelines, the kind found in any careful style guide, do not say "be respectful." They say "do not use a tone that talks down to the viewer." They use specific verbs.
Apply the same rule to Claude:
<rejection_rules>- Do not end with "Hope this helps", "In conclusion", or "Thanks for reading."- Do not insert emoji from U+1F300 onward.- Do not insert calls-to-action that assume a URL: "Click here," "Learn more."- If a length limit is exceeded by even one character, trim until it fits.- Never mix languages: if the source contains English keywords, the output must still be the target locale.
A concrete forbidden verb beats a polite ask every time. Pair each forbidden verb with the recovery action ("trim until it fits"), and the model retries internally instead of returning a violation.
Keeping the Review Loop Short
After every batch I look at a small Cloudflare Pages dashboard that shows me only the diff between the previous shipped copy and the new Claude draft. Markdown tables turned out to be the wrong format here; a Git-style diff is much faster to scan.
[ja]- old promotional_text: Free wallpapers, download now!+ new promotional_text: A quiet evening companion: handpicked washi-textured wallpapers.[en]- old promotional_text: Best wallpaper collection ever!+ new promotional_text: A quiet evening companion: handpicked papercraft wallpapers.
This dashboard sounds optional but I have come to consider it essential. Indie developers track eCPM and retention as diffs in AdMob already; treating store copy as a diff makes it possible to feel which changes correspond to download bumps. It took me twelve years to land on this workflow.
Designing the Error Path Around the Shape
When the API returns intermittent errors, you want retries that respect the shape you already produced. Holding the <output_format> makes "rewrite only the broken field" cheap.
def regenerate_partial(app_id: str, locale: str, broken_field: str) -> dict: """Ask Claude to rewrite only the broken field.""" prompt = f"""\<previous_output>{json.dumps(load_draft(app_id, locale), ensure_ascii=False, indent=2)}</previous_output><broken_field>{broken_field}</broken_field><instruction>Rewrite only the field named in broken_field. Do not modify any other field.</instruction><output_format><{broken_field}>...</{broken_field}></output_format>""" # ... invoke
Replacing only the broken field saves tokens and stabilizes the output. I call this the "partial rewrite pattern." It generalizes to AdMob ad copy, in-app tutorial strings, and onboarding text.
Closing — Shaping the Output Is Protecting the Voice
The <output_format> tag is a small technique. But after four years in production, I think it carries a meaning beyond the technical. I do not use AI in the creation of my fine art itself. The store copy that helps those works and my apps reach people, however, I do let Claude help me with. Shaping the output is how I protect the voice I want the work to keep.
If you want one concrete next step, pull out a single prompt you already run, and split its constraints out of the body into a separate tag. That alone will change how the output drifts. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.