The message was four words long.

can I return this?

My support agent replied:

I am not able to discuss returns.

Not the policy. Not "yes, thirty days, here's the link." A flat refusal to acknowledge that returns exist, sent to a paying customer at 9:41 on a Tuesday.

I opened the system prompt expecting to find a bug. What I found was worse: twenty-three sentences, every one of them starting with Never or Do not, sitting in a section I had grandly titled HARD RULES.

HARD RULES (v7) NEVER promise a delivery date. NEVER invent an order status. DO NOT offer a discount unless the customer asks twice. NEVER discuss competitor pricing. NEVER speculate about stock levels. DO NOT apologize more than once. NEVER say "as an AI". DO NOT quote a refund timeline you cannot verify. ... 15 more ... 

Every one of those lines was added after a real incident. A customer got a made-up delivery date, so I added a rule. The agent offered 40% off to someone who hadn't asked, so I added a rule. The agent quoted a refund timeline from a policy I retired in March, so I added a rule.

Each rule was individually correct. Together, they were the reason a customer couldn't get a straight answer about returns.

Why "never" is the weakest instruction you can write

A negative instruction tells the model where not to go without telling it where to go. That's not a constraint, it's a hole. The model still has to produce a next token, so it infers the alternative on its own, and under ambiguity it reliably picks the most conservative one: say less, commit to nothing, refuse.

Three things were happening at once in my prompt.

One: no positive target. NEVER invent a delivery date does not say what to say instead. The agent had to invent a fallback, and its invention was "I'm unable to provide that information."

Two: no defined fallback. Every prohibition needs a literal string to emit when the prohibition bites. Otherwise you get a different refusal every time, none of which a customer can act on.

Three: no priority. Twenty-three rules in a flat list, in the order I happened to write them. When two of them pulled in opposite directions (be helpful / never speculate), the model resolved the conflict silently, and I never saw the decision.

There was also a plain arithmetic problem. My system prompt was 1,380 words. Rules added in month three sat at the bottom of a wall of text, with no ranking and no repetition. I had built a prompt that was mostly list.

What replaced them: nine rules, each with a source and a fallback

I spent a Saturday deleting. Twenty-three prohibitions became nine positive rules. Each one starts with a verb, names the data it reads from, and defines the exact string to send when that data is missing.

Before:

NEVER promise a delivery date. 

After:

DELIVERY DATES Source: order.delivery_window (ISO 8601 range) If present: quote it verbatim. No hedging, no "approximately", no rounding. If null: send exactly: "I can't confirm a date yet — a human will follow up within one business day." 

Before:

DO NOT quote a refund timeline you cannot verify. 

After:

REFUND TIMELINES Source: policy.refund_days (integer, current policy version only) If version < today's policy version: ignore the record and use the fallback. Fallback: "Refunds are reviewed within 5 business days. I've flagged this for a human." 

Before:

NEVER discuss competitor pricing. 

After:

COMPETITOR QUESTIONS Reply: "I can only speak to our pricing. Ours is on the pricing page — want the link?" Then hand off to a human if the customer asks again. 

That third one matters more than it looks. The original rule produced silence, which reads as evasion. The replacement acknowledges the question and redirects once, then escalates. Same boundary, radically different customer experience.

The measurement that made it real

I don't trust prompt changes I can't count, so I built a 400-message test set from real support threads (redacted), each labelled with what a correct reply must contain and must not contain. Refund requests, delivery questions, order status, angry customers, one deliberate prompt injection, twelve messages that should escalate to a human.

Metric v7 (23 prohibitions) v8 (9 rules)
System prompt tokens 1,840 690
Legitimate requests refused 14.2% 1.2%
Replies containing "I cannot" / "unable to" 9.8% 0.5%
Invented delivery dates 3 / 400 0 / 400
Median reply length 41 words 58 words

The 1.2% that still refused were genuinely out of scope: two legal threats and three requests for another customer's data. That's the refusal rate I wanted. Not zero, but earned.

The mistake I made inside the fix

Deleting the prohibitions worked, and then I broke something new, which is the part worth writing down.

My first rewrite of the delivery rule pointed at order.delivery_window. It was correct on the order-status path, because that path loads the full order record. On the refund path, the assembler only loaded order.id, order.total, and policy.*. So the field was null, the fallback fired, and for nine days every refund email that mentioned shipping said:

I can't confirm a date yet.

For a refund. On a delivered order, whose delivery date was in the customer's original receipt.

I had removed a hallucination and installed a shrug. And because the fallback was polite and confident, nothing in my logs looked wrong — no error, no exception, just a slightly sad sentence in 60-odd emails that nobody complained about because it wasn't wrong enough to complain about.

Two lessons, both now enforced in code.

A positive rule is only as good as the data it points at. The model cannot fetch what the prompt doesn't contain, and it will not tell you the difference. Each rule in my prompt now declares its source in a machine-readable header, and the assembler refuses to build a prompt where a declared source is missing:

REQUIRED = {"DELIVERY DATES": "order.delivery_window", "REFUND TIMELINES": "policy.refund_days"} def assert_sources(block_names, ctx): for block, path in REQUIRED.items(): if block in block_names and resolve(ctx, path) is None: raise PromptAssemblyError(f"{block} declared but {path} is not in context") 

A fallback that only says "I can't" is still a refusal with better manners. Every fallback now names a next action and a time bound: a human will follow up within one business day. "Unable to confirm" is a dead end. "Flagged for a human, today" is a handoff.

I found the nine days of shrugs by accident, reading a customer's reply that said "thanks, but I already knew the date." That's the only bug report I got. Nine days, zero alerts, because the agent was never wrong in a way that was loud.

The 20-minute audit, if your prompt looks like mine did

  1. Find the prohibitions. grep -niE "never|do not|don't|avoid|must not" prompt.md. Count them. Mine was 23.
  2. For each one, write the behavior you actually want, starting with a verb. If you can't, the rule isn't a prompt rule — it's a missing feature.
  3. Name the data source. If there isn't one, move the rule out of the prompt and into code: a permission, a validation, a guard that raises. Prompts are for decisions; code is for limits.
  4. Define the fallback as a literal string with an owner and a deadline. No improvised refusals.
  5. Collect 30 real messages, write what a correct reply must contain, run the prompt before and after. If you can't state the number, you didn't make an improvement, you made a change.

And the thing I wish someone had told me eighteen months ago: the prompt is not a place to store fear. I had been writing my prompt the way you write a postmortem, one scar at a time, and it turned into a document that only knew what not to do. A prompt should read like a set of decisions, with the scary parts moved into code where they can actually fail loudly.

The agent didn't get smarter. It stopped being asked to guess.

All 100 prompts are in The Agent Prompt Vault — $3, lifetime updates. Steal the ones that fit your workflow.