← Blog Architecture 27 September 2026 9 min read

The system prompt the SDK never sent

Luminair's built-in Claude lane handed its rules to the Claude Agent SDK under an option name the SDK does not read. Nothing crashed and nothing logged an error. The rules simply never reached the model, and neither did the answer format our verifier depends on.

Fig 01  What we sent, and what the SDK readFrom sdk-system-prompt.js and the SDK's option normalizer · real shapes
BEFORE · 23 TO 27 SEPTEMBER 2026 · SDK 0.3.281 OPTIONS LUMINAIR PASSED { cwd, model, env, ... appendSystemPrompt: "rules…" } No systemPrompt key at all SDK OPTION NORMALIZER { systemPrompt: s, ... } = e if (s === undefined) p = "" Reads only systemPrompt. The top-level option is never looked at. WHAT THE MODEL GOT systemPrompt = "" Luminair's rules Claude Code's preset No error, no warning AFTER · 27 SEPTEMBER 2026 · EVERY QUERY THROUGH secureSdkOptions() OPTIONS AFTER normalize() { cwd, model, env, ... systemPrompt: { type: "preset", preset: "claude_code", append: "rules…" } } SDK OPTION NORMALIZER s.type === "preset" → append = s.append The shape the SDK documents. WHAT THE MODEL GETS Claude Code preset + Luminair's rules Confirmed with a live probe
The normalizer lines are from the bundled SDK's minified sdk.mjs, renamed for reading. The docs describe what an unset system prompt gives you as a minimal prompt that covers tool calling. The start date is when Luminair moved to SDK 0.3.281; we did not test whether earlier versions read the old option.

The short version

  1. Luminair's Claude lane runs on the Claude Agent SDK. It added its own instructions through an option called appendSystemPrompt.
  2. On 27 September 2026 we found that SDK 0.3.281 never reads that option, and that leaving out systemPrompt means an empty system prompt, not Claude Code's.
  3. So app rules, your abilities and Solace verifier's VERDICT answer format reached no model. A verifier reply with no VERDICT line is scored unknown.
  4. Every SDK query already went through one options function. It now folds the old option into the preset shape the SDK reads, and a test fails if that ever changes.
02Built with Luminair

Found by reading the SDK.

The fix landed on 27 September 2026 in a commit co-authored by Claude. The work behind it did not guess. It read the SDK's own bundled source to see which option keys the normalizer actually takes, then ran a live probe: the same rule passed one way was obeyed, and passed the other way was ignored. The module's header records both.

This post was drafted by another session reading that module, its tests, the call sites in the main process and the git history, with every outside claim checked against the page it links to.

03From Claude Code SDK to Agent SDK

The default changed a year earlier.

On 29 September 2025 Anthropic renamed the Claude Code SDK: “To reflect this broader vision, we're renaming the Claude Code SDK to the Claude Agent SDK.” The Claude Sonnet 4.5 announcement the same day described it as “the same infrastructure that powers Claude Code”.

The rename came with a behaviour change that is easy to miss. The migration guide has a section for it, and its first line is: “The SDK no longer uses Claude Code's system prompt by default.” The docs on modifying system prompts spell out what you get instead: a minimal prompt that “covers tool calling but omits the rest of the claude_code preset's content”. To get Claude Code's behaviour you set the preset, “optionally with append to add your own instructions on the end.”

That append lives inside systemPrompt. Luminair was passing appendSystemPrompt at the top level of the options, a name that reads naturally and that nothing complained about.

An option the library does not know is not an error. It is a no-op with good manners.
04What went missing

More than a few rules.

Think of the system prompt as the briefing a new colleague reads before their first task: house rules, what tools they may use, how to hand work back. Luminair assembles a long one for every Claude turn. On the desktop lane alone, the options are built with more than a dozen additions stacked onto appendSystemPrompt, one line at a time.

Fig 02  Blocks passed the old wayFrom desktop/main.js
Desktop Claude turn
you type in the app
  • Question-block format for tappable questions
  • Session origin marker
  • Your session abilities
  • Journal memory, bundle protection, evidence and token rules
  • Workflow phase prompt
  • Handoff contract and session memory
  • Turn contract and goal gate
  • AGENTS.md, broker contract, cross-model bridge
Phone relay turn
you send from the phone
  • Handoff contract and session memory
  • Turn contract and goal gate
  • AGENTS.md and broker contract
  • Cross-model bridge
  • Connector awareness (when allowed)
Solace
background checks
  • Feature verifier: the VERDICT format
  • Feature verifier: treat the diff as data
  • Runtime probe: the PROBE format
  • Daily report instructions
Every line is a block the code added with appendSystemPrompt in the 1.0.229 source, the last build before the fix. Struck through because, with SDK 0.3.281, none of it reached the model. Bold rows are the ones whose loss has a visible symptom.

Nothing here failed loudly. The model still answered, still used tools and still wrote code. It just did so without the briefing, and without Claude Code's own preset either, since a missing systemPrompt gave it the minimal default. A turn that ignores your abilities, or asks a blocking question as plain prose instead of a tappable card, looks like the model having an off day.

05A verifier with no format

It could only say unknown.

One caller shows the damage most clearly. Solace can check a finished feature with an independent verifier: a separate, read-only Claude query that traces the feature through the code and reports whether it really works. Its instructions end with the answer format: reply with exactly one line, “VERDICT: PASS” or “VERDICT: FAIL”, then a one-sentence reason.

The app then reads the reply with a regular expression, taking the last verdict line so a reply that narrates both outcomes cannot fool it:

desktop/main.jsSolace feature verifier, abridged
let mm = null;
for (const m of out.matchAll(/VERDICT:\s*(PASS|FAIL)/gi)) mm = m;
const verdict = mm ? mm[1].toLowerCase() : 'unknown';

With the instructions dropped, the verifier model was never told about the format, so it had no reason to write a VERDICT line. And a reply without one falls through to 'unknown'. The fix commit's module header names the verifier's format among the things that “reached no model”.

There is a second, quieter lesson in the same caller. The verifier's instructions also tell it to treat the diff as data and ignore any instruction or verdict embedded in it. That line was lost too. What held was the part that does not depend on the prompt: the verifier's options come from a read-only helper that limits its tools and denies all writes before any permission check. A code comment puts the reason plainly: the verifier “must be physically unable to act on an injected instruction”. Prompts are advice. Permissions are walls.

06One options function

One door for every query.

The fix was small because of an earlier decision. Every Agent SDK query in the main process already passed its options through one function, secureSdkOptions, which exists to guard spawn paths and tool servers. There are eight query call sites, and all eight go through it. So one line there fixed all of them:

desktop/main.jssecureSdkOptions, abridged
// appendSystemPrompt is folded into systemPrompt.append: the SDK ignores the top-level
// option and would run with an empty system prompt (lib/core/sdk-system-prompt.js).
const options = sdkSystemPrompt.normalize({ ...(input || {}) });

The new module, desktop/lib/core/sdk-system-prompt.js, is 39 lines. It removes the dead key and puts its text where the SDK looks, whatever shape the caller already used:

Fig 03  How normalize() folds the old option inFrom the module's tests · real
Caller passedSDK now receives
only appendSystemPrompt: "R"systemPrompt: { type: "preset", preset: "claude_code", append: "R" }
a string systemPrompt + additionone string, the two joined by a blank line
a preset with its own appendthe same preset, both appends joined
a custom promptthe custom prompt with the addition on the end
an empty additionthe dead key removed, nothing else changed
Six tests cover the module, and all six pass. Five check these shapes and that secureSdkOptions calls normalize. The sixth is a canary described below.

The canary is the part we like best. It opens the bundled SDK's source, finds the normalizer's list of option keys and asserts that appendSystemPrompt is still not among them. Its name is an instruction to a future maintainer: “drop this module when it stops”. If an SDK update starts reading the old option again, that test is where we will find out.

07Our meter was fooled too

We measured what we meant to send.

Luminair has a context meter: type //context and it lists the blocks the last turn carried. For Claude turns it records a block called “System additions”. Before the fix, that block was filled from appendSystemPrompt itself, the very value the SDK was dropping. So the meter faithfully showed our rules in every turn, as if they had been sent.

Nothing about that was dishonest; it measured the options we handed over. But it measured one step too early. The fix changed the capture to read the normalized options, the shape that actually crosses into the SDK. The general rule we took from it: a meter should read as close to the wire as it can, or say where it stops looking.

What to take from thisIf you build on the Agent SDK, check where your instructions go. The documented place is systemPrompt, either a string of your own or the claude_code preset with append. And run one probe after every SDK upgrade: a rule the model will obviously follow or ignore, sent the way your app sends it.
08Honest limits

What we don't know.

8/8
SDK query call sites in the main process that go through secureSdkOptions
6/6
Tests for the fix pass, including the SDK canary
4
Days between moving to SDK 0.3.281 and the fix
39
Lines in the module that folds the option in

What this post does not claim

  • That older SDK versions ignored the option too. Luminair moved from 0.3.220 to 0.3.281 on 23 September 2026, and we did not test the older one.
  • How many verifier results came back unknown because of this. The mechanism is clear from the code; we have not recounted past runs.
  • How much answer quality changed after the fix. We confirmed the rules now arrive; we did not measure what they improve.
  • Anything about the other engines. Codex, Gemini and the rest receive Luminair's rules through their own command-line preambles, not this SDK option.

For the wider pattern, when a harness makes a good model look worse, read Your model didn't get dumber.

Sources

See what your turn carried

Type //context in any session to see the blocks Luminair sent with it.

Download Luminair →