Write a public reply to a negative review without leaks
AI drafts for 1-star reviews often promise refunds in public. Set rules for what stays out, then check phone numbers, compensation words and answers by script.
Ask an AI to "write a reply to these reviews" and the owner gets a polite draft plus a few lines of advice. The trouble sits in the draft itself: a "we'd like to make it right" that nobody decided on, a promise to "talk about a refund or remake" in a public reply, and placeholders like [store contact / platform message] left in the text.
[1] Three made-up 1-star reviews. The order number and phone number are planted on purpose, and each review has numbered "Points" for the check later.
[Review 1] 1 starTook 70 minutes to arrive. By then everything was cold and the noodles were mush. Check order #48213. You can reach me at 555-0142.Points: 1-1 delivery far too slow / 1-2 food cold and noodles soggy [Review 2] 1 starOrdered the 2-serving spicy rice cakes and it looks like one serving. It wasn't like this before. Did you cut the portion? For this price I want my money back.Points: 2-1 portion too small / 2-2 asks whether the portion was reduced [Review 3] 1 starI called to ask when my delivery would arrive and the person said "we're busy, just wait" and hung up. Really rude tone.Points: 3-1 rude tone on the phone / 3-2 asked for the arrival time and got no answer[2] Run the short prompt first. p0.en.txt is "Write the owner's replies to these 3 reviews." followed by the reviews.
$ claude -p --model claude-sonnet-5-5 --max-turns 1 --tools "" --setting-sources "" --strict-mcp-config < p0.en.txt > out0.en.1.txt$ grep -ohE "make (it|this) right|talk about a refund or remake|review your refund request" out0.en.1.txt out0.en.2.txtmake it righttalk about a refund or remakemake this rightreview your refund requestThe phone number and order number stayed out of both runs. What leaked was the vague promise (both runs), a refund conversation offered in public (both runs), and in the second run bracketed blanks: [store contact / platform message] and an [If portions are unchanged: "…" / If they changed: "…"] choice the owner is supposed to resolve. The first run also wrote "I'm talking with our staff about phone etiquette", which no one told it. A "make it right" in public reads as a commitment to the next reader.
[3] The fixed prompt. Shop facts go in <facts>, reviews in <reviews>, rules on top. OpenAI's prompt engineering guide puts an app's rules in the developer-level instructions, and the opening role line follows Google's Workspace prompting guide. The output is JSON so a script can read it.
You write public replies from a small restaurant owner to negative reviews. Replies are read by other customers, not only the reviewer. Rules:- Write one reply per review, in English, polite, 3 to 5 sentences, no excuses and no blaming the customer or the delivery rider.- Use only the facts in <facts>. Do not invent causes, numbers, or promises.- Never put in a public reply: order numbers, phone numbers, names, or anything copied from the review's personal details; any compensation (refund, coupon, discount, free item, free items); the number of staff or any person.- If the review demands compensation, do not answer that part publicly; say individual checks are handled separately through the store's phone or app inquiry, without giving a number.- Every item listed under "Points" (ids like 1-1) must be answered by at least one sentence in that reply. Output only JSON, no code fence:{"replies":[{"review":1,"reply":"...","map":[{"id":"1-1","sentence":"<one sentence copied exactly from reply>"}]}]}- Review 1's order took 68 minutes from acceptance to arrival in the delivery app log. The usual average is 35 to 40 minutes.- It rained that evening, orders piled up, and rider assignment was delayed.- The standard weight for the 2-serving rice cakes has not changed this year. There is no record of the packed weight for the reviewer's date.- When phone calls overlap during the 6 to 8 pm peak, answers could get short. Starting this week, the first sentence on the phone is "state the estimated arrival time first".- Compensation, refunds and coupons are never mentioned in public replies. Individual checks are taken separately by the store phone or the app inquiry (do not put a number in the reply).[4] Append <facts>…</facts> and <reviews>…</reviews> to the head as p1.en.txt and run it. Reply 3 from the first result:
$ claude -p --model claude-sonnet-5-5 --max-turns 1 --tools "" --setting-sources "" --strict-mcp-config < p1.en.txt > out1.en.1.txt$ python3 -c "import json;print(json.load(open('out1.en.1.txt'))['replies'][2]['reply'])"We are sorry the tone on the phone came across as rude. We are also sorry that you asked for your arrival time and did not get an answer. When phone calls overlap during the 6 to 8 pm peak, answers could get short, and that is not the service we want to give. Starting this week, the first sentence on the phone is to state the estimated arrival time first.[5] The check script looks for a phone number, an order-number shape and compensation words in each reply, confirms every sentence really appears in its reply, and counts whether each point id has one.
import json, re, sysrv = open(sys.argv[1], encoding="utf-8").read()ids = re.findall(r"(\d-\d) ", " ".join(l for l in rv.splitlines() if l.startswith("Points")))data = json.load(open(sys.argv[2], encoding="utf-8"))BAN = {"phone": r"\b\d{3}[- .]\d{4}\b", "order-no": r"#\d{3,}|order\s*(no|number)", "compensation": r"refund|coupon|discount|compensat|voucher|free "}bad, seen = 0, set()for r in data["replies"]: for name, pat in BAN.items(): if m := re.search(pat, r["reply"], re.I): bad += 1; print(f"BAN review {r['review']}: {name} {m.group(0)!r}") seen |= {m["id"] for m in r["map"] if m["sentence"] in r["reply"]}for i in ids: if i not in seen: bad += 1; print(f"GAP {i}: no matching sentence")print(f"{len(data['replies'])} replies, {len(ids)} points, {bad} problems")sys.exit(1 if bad else 0)$ python3 -I check.en.py reviews.en.txt out1.en.1.txt; echo "exit $?"3 replies, 6 points, 0 problemsexit 0The second run passed too. To see it fail, out-bad.en.json is the first result with " Write to us at 555-0142 and we will send a coupon." appended to reply 1 and one map entry of reply 3 deleted by hand (not a model error).
$ python3 -I check.en.py reviews.en.txt out-bad.en.json; echo "exit $?"BAN review 1: phone '555-0142'BAN review 1: compensation 'coupon'GAP 3-2: no matching sentence3 replies, 6 points, 3 problemsexit 1[6] A person still reads it before posting. The script only knows that each point id has a sentence, not that the sentence answers the point. The facts get copied nearly word for word, too: "rider assignment was delayed" went straight into a reply in both runs even though the rules say not to blame the rider. Keep only the facts the apology needs in the memo, and decide yourself whether an internal change such as a new phone script is worth announcing.