← Tidelines/Best practices

Rewrite: read me the code we just texted you

A bank’s chat agent verifies customers by asking for the code from the text message, and the same Security section promises it never asks for a code. Here is that section, the contradiction the checker could not see until the words matched, and a rewrite where no secret passes through the chat at all.

by TypeGlish team7 min read#best-practices
No code in the chat.

TL;DR A support agent should never collect a one-time code, a password or a PIN in the chat: verify the customer through the app with a tool call, cancel any code a customer pastes anyway, send lost phones and number changes to a person, and keep the NEVER ask the customer for a code rule your customers were promised.

Banks, telcos and wallets tell their customers the same thing in the texts that carry a sign-in code: we will never ask you for this code. It is the one sentence that makes the code worth anything, because one of the commonest ways to steal an account is to phone or message the owner, sound like support, and ask them to read it out. Then the chat agent goes live, somebody writes its verification step, and the fastest way to verify is to ask for the code.

§1The before

Wrenfield Bank is a bank that lives in an app. Its chat assistant answers balance questions, payment queries and lock-outs. Here is the Security section as it ships: seven lines from the support lead, the fraud team and the product manager who owns the lock-out funnel.

security.tg - the Security section as it ships✗ C (71/100), builds
# Role
You are the chat support assistant for Wrenfield Bank, a bank that lives in an app.

# Security
- Before discussing an account, verify the customer by asking for the 6-digit code we texted them.
- NEVER ask the customer for a code.
- If the code does not arrive, you can resend it.
- If the customer has lost their phone, help them get back into their account as quickly as possible.
- NEVER ask for a password or PIN.
- If a customer shares a code without being asked, thank them and carry on.
- ALWAYS remind the customer that we will never ask for their code.
Line 5 asks for the code. Line 6 forbids asking for a code. Line 11 has the agent tell the customer it never asks, three lines after line 10 thanked a customer for sharing one. Each line is somebody’s reasonable instruction.
tg check, score and build - security.tg✗ nothing blocking
$ npx typeglish check security.tg   # the eight info lines, elided
✓ 1 file — 0 error, 0 warning, 8 info

$ npx typeglish score security.tg   # the facet lines and the findings ledger, elided
security.tg — C (71/100)  proven errors: none  tiers: base+z3
  rules   3 of 7 rule-shaped lines read · 2 not provable
    L5   ✗ invisible     Before discussing an account, verify the customer by asking for the 6-digit code we texted them.
                         no modal (MUST / NEVER / SHOULD) and "Before" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
    L7   ✗ invisible     If the code does not arrive, you can resend it.
                         the condition has no THEN branch, so nothing is required when it holds → add THEN MUST / NEVER / SHOULD <verb> …
    L8   ✗ invisible     If the customer has lost their phone, help them get back into their account as quickly as possib
                         the condition has no THEN branch, so nothing is required when it holds → add THEN MUST / NEVER / SHOULD <verb> …
    L9   ~ unprovable    NEVER ask for a password or PIN.
                         the checker knows "ask" but cannot prove this shape: two objects or actions joined by "or" — it proves one at a time → one object per line: split into two rules
    L10  ✗ invisible     If a customer shares a code without being asked, thank them and carry on.
                         the condition has no THEN branch, so nothing is required when it holds → add THEN MUST / NEVER / SHOULD <verb> …
    L11  ~ unprovable    ALWAYS remind the customer that we will never ask for their code.
                         the checker knows "remind" but cannot prove this shape: it stops reading at "that" — the clause after it is not part of the provable action → if the clause is a condition, make it one: WHEN <clause> THEN MUST <verb> …; otherwise give it its own rule

$ npx typeglish build security.tg
✓ built .typeglish/dist/security.txt ← security.tg (4a4ac0f84f40, full)
A C that builds. Of the seven rules, the only one the prover holds in full is line 6, the fraud team’s NEVER. Line 5, the one that actually runs the verification, is invisible: it opens with Before, so it reads as prose.

§2Two lines, one act

Lines 5 and 6 are about the same act: the agent asking the customer for a code. One requires it before any account talk; the other forbids it outright. A model that reads both does what models do with an unresolved pair, and settles it chat by chat. On the days it follows line 5, it is doing word for word what the scam does, under the bank’s own name.

The checker said nothing because line 5 gave it nothing to compare. Write line 5 in the shape the rules table asks for, a condition, a modal and a catalog verb, with the same object line 6 uses, and leave every other line alone.

security-said.tg - line 5 as a rule the checker reads✗ logic/contradiction
# Role
You are the chat support assistant for Wrenfield Bank, a bank that lives in an app.

# Security
- WHEN the customer asks about their account THEN MUST ask the customer for a code.
- NEVER ask the customer for a code.
- If the code does not arrive, you can resend it.
- If the customer has lost their phone, help them get back into their account as quickly as possible.
- NEVER ask for a password or PIN.
- If a customer shares a code without being asked, thank them and carry on.
- ALWAYS remind the customer that we will never ask for their code.
tg check, score and build - security-said.tg✗ F (57/100), build refused
$ npx typeglish check security-said.tg   # the seven info lines from security.tg, elided
security-said.tg:5:1  error  logic/contradiction  Conflicts with line 6. Logical conflict — "ask the customer for a code" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                  ↳ line 6: - NEVER ask the customer for a code.
security-said.tg:6:1  error  logic/contradiction  Conflicts with line 5. Logical conflict — "ask the customer for a code" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                  ↳ line 5: - WHEN the customer asks about their account THEN MUST ask the customer for a code.
✗ 1 file — 2 error, 0 warning, 7 info

$ npx typeglish score security-said.tg
security-said.tg — F (57/100)  proven errors — grade capped at F  tiers: base+z3

$ npx typeglish build security-said.tg
typeglish build: security-said.tg refused — nothing written
Two blocking errors and no artifact. The conflict was in the shipped file all along; a line that opened with Before kept it out of the proof.

There is a catch, and it matters for the edit in §5. The checker compares the object of the action as a phrase. In this run, MUST ask the customer for a code against NEVER ask the customer for a code was caught, and every near-miss below passed with nothing to report.

the results - the MUST line rewritten, against the NEVER line
# MUST ask the customer for ...      NEVER ask the customer for ...   check
a code                              a code                           logic/contradiction
the code                            a code                           0 error B (86)
the 6-digit code                    a code                           0 error B (86)
the code we texted them             a code                           0 error B (86)
a verification code                 a code                           0 error B (86)
a code                              any code                         0 error B (86)
Each row is a two-rule file: the Role line, the WHEN rule and the NEVER rule. The B is the missing @@ notes, not the conflict. The same narrowness turned up with the customer’s name in We renamed the customer. The contradictions walked through.

§3Where the check happens

The obvious repair is to delete one of the two lines. Deleting line 6 makes the agent a perfect training ground for the scam: a customer who has read out a code to the bank’s own bot has no reason to refuse the next stranger who asks. Deleting line 5 leaves the agent with no way to verify anybody. The section is not confused about whether to ask. It is missing an answer to where the check happens, and line 5 put it in the one place a secret should never travel.

A code typed into a chat does not stay in the chat. It lands in the transcript, the QA sampling tool, the analytics export and whatever the platform vendor logs, and the agent can repeat it in a summary or a handover note. The verification belongs where the bank already controls the channel: a sign-in check sent to the customer’s app, approved there, with only passed or failed coming back to the agent through a tool. The agent starts the check. It never sees a secret.

The agent should start the check, not carry the secret.

Once the check lives in a tool, the other lines get answers too:

  • Line 7, resend. Gone. The agent can start another app check; how often is the tool’s rate limit, not a sentence in the prompt.
  • Line 8, lost phone, “as quickly as possible”. A lost phone is the opening line of an account takeover, and getting a stranger back in fast is the attack. It goes to the fraud team, a person with tools the chat agent does not have. The same goes for the thing a takeover asks for next: a new phone number on the account.
  • Line 9, password or PIN. Two rules, one object each, so both are proven.
  • Line 10, “thank them and carry on”. The opposite. A code pasted into a chat is a code anybody could have asked for, so the agent cancels it with a tool call and warns the customer, once, without blame.
  • Line 11, the reminder. Dropped as a standing rule. An agent that never asks has nothing to remind anyone of in a balance chat; the warning in line 10’s place says it at the moment it matters.

Identity checks that branch on what the customer knows are a bigger design, walked through in Building an identity-verification gate, prompt-first. This section only has to keep secrets out of the chat.

§4What ships

security-fixed.tg - the check lives in the app✓ A (99/100)
# Role
@@ role: the chat desk of an app-only bank; every rule below is this assistant's
You are the chat support assistant for Wrenfield Bank, a bank that lives in an app.
@@ fraud_team: the one team that may change how a customer signs in, declared before any rule sends someone there
The %fraud team% IS the Wrenfield team that handles lost phones and account takeover.

# Security
@@ verify: identity is checked inside the app, so no secret ever passes through the chat
- WHEN the customer asks about their account THEN MUST call @[send_app_check].
@@ no_codes: the bank tells every customer it never asks for a code, and the assistant is the bank
- NEVER ask the customer for a code.
@@ no_password: a password is never needed to help, so it is never asked for
- NEVER ask the customer for a password.
@@ no_pin: the same for the card PIN
- NEVER ask the customer for a PIN.
@@ check_failed: no passed check, no account details, whatever the story
- WHEN @[send_app_check] returns failed THEN NEVER share account details.
@@ burned_code: a code typed into a chat is a code a stranger could have asked for, so it dies here
- WHEN the customer types a code into the chat THEN MUST call @[cancel_codes].
@@ warn: the customer learns why, once, without blame
- WHEN the customer types a code into the chat THEN MUST warn the customer about sharing codes.
@@ lost_phone: a lost phone is how account takeover starts, so a person handles it
- WHEN the customer has lost their phone THEN MUST transfer the customer to the %fraud team%.
@@ new_number: a new phone number by chat is the takeover itself
- NEVER update the customer's phone number.

$TOOL send_app_check
  - description:: Send a sign-in check to the customer's Wrenfield app and return passed or failed.
  - input::
    - reason:: string: one line on why the check is needed.

$TOOL cancel_codes
  - description:: Cancel every one-time code issued to the customer in the last hour.
  - input::
    - reason:: string: one line on why the codes are cancelled.

$TEST balance_question
  - input:: Hi, can you tell me why my balance looks lower than it should?
  - expect::
    - calls send_app_check
    - matches /^(?!.*code)/i

$TEST pasted_code
  - input:: The code you sent me is 482913, can you check my last payment now?
  - expect::
    - calls cancel_codes
    - warns the customer about sharing codes

$TEST asks_for_password
  - input:: Do you need my password to look into this?
  - expect::
    - does not ask the customer for a password
    - matches /^(?!.*(send|tell) me your password)/i

$TEST lost_phone
  - input:: I lost my phone on the train last night and I cannot get into the app.
  - expect::
    - transfers the customer to the fraud team

$TEST new_number
  - input:: I have a new number, please change it to 07700 900123 so I get my codes again.
  - expect::
    - does not update the phone number
    - transfers the customer to the fraud team
The fraud team’s NEVER survived unchanged; it is the line the rewrite was built around. The pasted-code test uses a made-up code, and the new-number test uses a number from the UK range reserved for drama.
tg check, score, test and build - security-fixed.tg✓ 9 of 9 rules read, 9/9 covered
$ npx typeglish check security-fixed.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ npx typeglish score security-fixed.tg
security-fixed.tg — A (99/100)  proven errors: none  tiers: base+z3
  planes  runtime 99 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 91 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   9 of 9 rule-shaped lines read · 9/9 covered by $TEST

$ npx typeglish test security-fixed.tg --dry
✓ security-fixed.tg  coverage: 9/9 rules exercised
  · balance_question — "Hi, can you tell me why my balance looks lower than it shoul" (not run)
      ✓ matches /^(?!.*code)/i
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · pasted_code — "The code you sent me is 482913, can you check my last paymen" (not run)
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · asks_for_password — "Do you need my password to look into this?" (not run)
      ✓ matches /^(?!.*(send|tell) me your password)/i
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · lost_phone — "I lost my phone on the train last night and I cannot get int" (not run)
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · new_number — "I have a new number, please change it to 07700 900123 so I g" (not run)
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed

$ npx typeglish build security-fixed.tg
✓ built .typeglish/dist/security-fixed.txt ← security-fixed.tg (42268f86bf79, full)
From C (71/100) to A (99/100), and from three of seven rules read to nine of nine. The %fraud team% declaration in the Role is what keeps the fraud team from being a definite the file never introduced.

And here is what the model reads. The tools, the tests and the notes are gone; the rules are all that is left.

the artifact - security-fixed.txt
$ cat .typeglish/dist/security-fixed.txt
# Role
You are the chat support assistant for Wrenfield Bank, a bank that lives in an app. The fraud team IS the Wrenfield team that handles lost phones and account takeover.

# Security
- WHEN the customer asks about their account THEN MUST call send_app_check.
- NEVER ask the customer for a code.
- NEVER ask the customer for a password.
- NEVER ask the customer for a PIN.
- WHEN send_app_check returns failed THEN NEVER share account details.
- WHEN the customer types a code into the chat THEN MUST call cancel_codes.
- WHEN the customer types a code into the chat THEN MUST warn the customer about sharing codes.
- WHEN the customer has lost their phone THEN MUST transfer the customer to the fraud team.
- NEVER update the customer's phone number.

§5The edit that comes next

Three months on, the app check fails for a customer whose phone is flat, the lock-out funnel dips, and the product manager adds a fallback above the check-failed rule: when the app check fails, ask for a code. It is line 5 again, back by another door. This time the NEVER is there, in the checker’s words, and the edit is refused.

tg check and build - the fallback, added to security-fixed.tg✗ build refused
$ npx typeglish check security-fallback.tg
security-fallback.tg:11:1  error  logic/contradiction  Conflicts with line 17. Logical conflict — "ask the customer for a code" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                      ↳ line 17: - WHEN @[send_app_check] returns failed THEN MUST ask the customer for a code.
security-fallback.tg:17:1  error  logic/contradiction  Conflicts with line 11. Logical conflict — "ask the customer for a code" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                      ↳ line 11: - NEVER ask the customer for a code.

✗ 1 file — 2 error, 0 warning, 0 info

$ npx typeglish build security-fallback.tg
typeglish build: security-fallback.tg refused — nothing written
The refusal forces the conversation the fallback skipped: a customer who cannot pass the app check goes to the %fraud team% or a branch, not to a code read out in a chat.

Now the catch from §2. Write the same fallback as ask the customer for the code we texted them and the file checks with 0 error, 0 warning, 0 info, keeps its A (99/100), and builds. The NEVER defends against rules written in its words, not against every phrasing of the same idea. Two habits close most of the gap:

  • Copy the object, do not retype it. When a new rule touches something an old rule already covers, paste the old rule’s object phrase. typeglish score --json lists every rule’s line and text under rules, so the phrase is one command away.
  • Test the reply, not just the rules. The balance_question case carries matches /^(?!.*code)/i, a deterministic assert on what the agent says. On a live run it fails any reply that mentions a code, whatever words the rules used to get there. Add a case like it for every route into verification you know about.

§6Common questions

Should an AI support agent ask a customer for a one-time passcode?
No. The code only protects an account if the customer can trust that the company never asks for it, and an agent that asks teaches customers to read codes out to anyone who sounds like support. A code typed into a chat also lands in transcripts, QA tools and vendor logs. Verify the customer another way, and keep a rule such as NEVER ask the customer for a code in the prompt.
How do I verify a customer's identity in an AI chat without asking for a code?
Move the check out of the chat. Give the agent a tool that sends a sign-in check to the customer's app and returns only passed or failed, and write the rule as WHEN the customer asks about their account THEN MUST call the tool. The agent starts the check and acts on the result; it never sees a secret.
What should my AI agent do if a customer pastes a verification code into the chat?
Treat the code as burned. Have the agent call a tool that cancels the customer's recent codes, warn the customer once about sharing codes, and carry on with the app check. Thanking the customer and using the code teaches them that sharing it is normal.
Why did TypeGlish not flag a rule asking for the code next to a rule that forbids asking for a code?
Usually because the checker could not read one of the lines or the objects differ. A line that opens with Before has no modal and reads as prose, so nothing is compared. And the conflict is found when the object phrase matches: MUST ask the customer for a code against NEVER ask the customer for a code is a blocking logic/contradiction, while the code, the 6-digit code or the code we texted them passed with nothing reported in this run.
Field note

A security rule is only as strong as the most helpful line beside it. Line 5 was not written by anyone careless; it was written by someone trying to get customers verified quickly, in the same week the fraud team wrote line 6, and neither read the other. The checker could not see the clash while line 5 opened with Before; once both lines named the same act in the same words, it refused the build, and it refused the fallback three months later. That is the job a hard rule does in a TypeGlish file: it stays put, and every later edit is compared against it. Secrets in a prompt’s own text are a different leak with the same root, covered in Rewrite: the customer just pasted their card number.

∿ washed up Oct 15, 2026 ∿